こんにちは!メインフレームの世界へようこそ。
JavaやCOBOLといった他のモダンなプログラミング言語の経験がある方にとって、歴史あるPL/I(Programming Language One)のコードを初めて目にする瞬間というのは、少し独特な緊張感があるものですよね。「なんだか文字だらけで難しそう……」と感じてしまうかもしれませんが、大丈夫です。基本のルールを一つずつ紐解いていけば、決して怖い言語ではありません。むしろ、非常に表現力が豊かで、エンジニアの意図をダイレクトに実行してくれる頼もしい相棒なんですよ。
さて、今回はそんなPL/Iの数あるユニークな仕様の中でも、初学者が思わず「おや?」と二度見してしまう、SUBSTRビルトイン関数の左辺値としての挙動について、こっそり裏側の仕組みまで含めて優しく解説していきたいと思います。
—
そもそもPL/Iには「予約語」がない?(ちょっとした豆知識)
本題に入る前に、PL/Iの非常にユニークな大前提に少しだけ触れておきましょう。
JavaやCOBOL、C言語などでは、`if`や`while`、あるいはデータ型名などは「予約語(Keyword)」とされていて、変数名として使うことはできませんよね。しかし、PL/Iには原則として予約語というものが存在しません。
どういうことかと言うと、PL/Iのコンパイラは、その単語が置かれた「文脈(Context)」を見て、それが変数名なのか、命令(キーワード)なのかを賢く判断しています。
例えば、以下のような宣言もPL/Iではエラーになりません。
1
DCL IF FIXED BIN(31); / 「IF」という名前の数値を宣言 /
IF = 10; / 変数IFに10を代入 /
ちょっと驚きですよね。「文脈で判断する」というこのおおらかで柔軟な設計思想こそが、PL/Iの大きな特徴です。だからこそ、変数名を自由に命名できる反面、今回取り上げるような「関数の左辺値」といったトリッキーな書き方も、構文上はすんなり通ってしまうわけなんです。
—
主役登場:SUBSTRを「左辺」に置くとは?
文字列の一部を切り出すときに使う `SUBSTR` 関数。
Javaの `substring()` や COBOLの `MOVE A(1:3) TO B` のような感覚で、他の言語を触ってきた方なら「文字列から一部分を抜き出すもの」と覚えているはずです。
通常は、こんなふうに右辺(代入される側)に使いますよね。
1
/ 通常の右辺での使用例 /
DCL WORK_STR CHAR(10) INIT(‘IBM_MAINFRAME’);
DCL OUT_STR CHAR(3) INIT(”);
OUT_STR = SUBSTR(WORK_STR, 5, 3); / 5文字目から3文字分を切り出して代入(結果は ‘MAI’) /
ここまではごく自然で、他の言語と同じ感覚です。
しかし、PL/Iの懐の深さ(あるいは恐ろしさ)はここから。なんと、この `SUBSTR` を代入文の「左辺(代入する先)」に置くことができるのです。
百聞は一見にしかず、実際のコードを見てみましょう。
1
/ SUBSTRを左辺に置いた驚きのコード /
DCL TARGET_STR CHAR(15) INIT(‘COBOL_IS_GOOD’);
/ 7文字目から3文字分を、別の文字列で「上書き」する /
SUBSTR(TARGET_STR, 7, 3) = ‘WAS’;
PUT SKIP LIST(TARGET_STR); / 結果はどうなる? /
お分かりでしょうか?
`TARGET_STR` の中身は最初 `COBOL_IS_GOOD` でしたが、7文字目(`_IS_` の部分)から3文字分が `’WAS’` に置き換わり、結果として画面には `COBOL_WAS_GOOD` と出力されます。
一見すると、「変数の特定の部分をピンポイントで書き換えられて便利じゃん!」と思いますよね。COBOLの `STRING` 命令や、C言語の `memcpy` のような処理が、こんなに短いコードで書けてしまうのは確かに魅力です。
—
幕の裏側で何が起きている?(メモリとオーバーヘッドの話)
さて、ここからがシステムアーキテクトとしての腕の見せ所、そしてメインフレームのパフォーマンスを語る上で最も重要なポイントです。
「便利だから」といって、この `SUBSTR` の左辺値代入をバッチ処理の中で何百万回、何千万回とループさせていないでしょうか?
実は、この構文の裏側では、コンパイラとランタイムが私たちが想像している以上に、健気に、そしてちょっと大掛かりな裏方作業を行っています。
1. 一時領域(ストリング・テンプ)の生成
JavaやC言語の感覚で「変数のメモリ上のその場所を直接書き換えているんだろう」と思ったら大間違い。PL/Iの厳格なデータ制御において、非整列データや複雑なオフセット指定が絡む場合、コンパイラは安全のために舞台裏でこっそり「一時的な作業用エリア(一時領域)」をメモリ上に切り出します。
2. データコピーの嵐(オーバーヘッド)
左辺に置かれた `SUBSTR` を処理するため、ランタイムは次のような手順を踏みます。
1. 元の変数から必要な部分を一時領域にコピーする。
2. 一時領域に対して新しい値を書き込む。
3. 一時領域のデータを、元の変数の正しい位置へもう一度コピーして戻す。
……お気づきでしょうか?
本来なら1回の代入で済むはずが、裏側で「コピーして、書いて、また戻す」という無駄なメモリの往復運動(オーバーヘッド)が発生しているのです。
3. 最適化とコンパイラの苦悩
現代のIBM Enterprise PL/Iコンパイラは非常に優秀なので、変数の長さや定義が固定で明確な場合、余計な一時領域を作らずにダイレクトな機械語(ストレージ・アラインメントを考慮したインストラクション)に最適化してくれることもあります。しかし、可変長文字列(Varying String)などが絡んできたりすると、途端にランタイムルーチンが呼び出され、CPUサイクルの消費が跳ね上がります。
基幹システムの夜間バッチなどで、何万件ものレコードを高速に処理しなければならない場面において、この「知らぬ間に発生しているメモリコピー」は、じわじわとCPU時間を削る隠れたパフォーマンス・キラーになり得るのです。
—
実務で安全に使いこなすためのアドバイス
「じゃあ、SUBSTRの左辺値なんて怖くて使えないよ!」と思われたかもしれません。でも、必要以上に怯える必要はありませんよ。要は「適材適所」です。
- マスタメンテナンスや、1件ごとの画面入出力などの小規模なデータ加工
コードが非常にすっきりと書け、保守性も高まるため、積極的に使っても全く問題ありません。可読性は正義です。
- 数百万件を処理する高速な夜間バッチのループ処理
もしパフォーマンスネック(CPU時間の高騰)が疑われる場合は、`SUBSTR` の左辺値代入を避け、あらかじめ文字列を分割して定義し直す(リデフィニションを活用する)か、構造体(`STRUCTURE`)のオーバーレイを使ってメモリのレイアウトを工夫することを検討してみてください。
レガシーシステムの移行や改修では、「動けばいいや」ではなく、「そのコードがハードウェアのメモリ上でどう振る舞っているか」を少しだけ想像してあげることが、素晴らしいシステムアーキテクトへの第一歩です。
PL/Iは、私たちがきちんと意図を伝えてあげれば、メインフレームのパワーを限界まで引き出して応えてくれる、本当に奥が深い言語です。
怖がらずに、一つずつその挙動を楽しみながらコードを書いていきましょうね!
