メニューボタン
IBMi海外記事2023.08.09

入出力操作を減らしてRPGを高速化する、パート2

Gregory Simmons 著

レガシー コードが残っているという話題になると、含み笑いをしたり、顔をしかめたりしがなら話す人は多いようです。普通は、あまり良くない意味合いで話題に上るのがほとんどです。しかし、忘れてはならないのは、レガシー コードになるのは、それが問題なく動いているからこそだということです。レガシー コードは、長年にわたって、明けても暮れても黙々と業務を遂行しているうちに、空気のような存在になってしまっています。きしむ車輪は油を差してもらえる、と言いますが、レガシー コードにも、少しは油を差してあげてもよいのではないでしょうか。

こうしたコードにまつわる問題は、問題なく動いているうちに、いつの間にか数十年が過ぎ去り、技術は進化しているということです。数十年先には、こうした「レガシー コード」という固定観念的なイメージにぴったり当てはまるプログラムは、少し残っているだけでしょうか。そうしたプログラムが何千も残っている可能性の方が高いような気もします。

CTOの執務室を訪ねて、こうしたレガシー コードの問題にすぐにも取り組みたいと申し出たとしたら、激励の言葉は掛けてもらえるでしょう。しかし、それは最優先プロジェクトということにはならないでしょう。だからこそ、こうしたレガシー コードの問題は、ほんの少しずつ取り組んで行く必要があるわけです。Michael Feathers氏が彼の著書『 Working Effectively with Legacy Code(レガシーコード改善ガイド)』の中で用いた比喩が私のお気に入りです。そこでは、レガシー コードだらけのソース ライブラリーが、ぬかるんだ野原に喩えられています。そして、ところどころに生えている何枚かの草の葉が、何本かの良質のプログラムだということです。誰でも、自分のソース コードはきちんと手入れされたサッカー フィールドのようだと思いたいものですが、実際のところは、私と同じ状況、すなわち、ぬかるんだ野原でしょう。ソース ライブラリーには、読者の皆さんが入社する何年も前、さらに言えば、皆さんが生まれる前に書かれた、何千とは言わないまでも、何百ものプログラムがあります。誰でも、完璧に手入れの行き届いた芝生のフィールドに喩えられるようなソース ライブラリーであることを夢見てもよいのでしょう(そして夢見るべきです)が、Feathers氏は、草の葉1枚ずつに集中することを推奨しています。

そうした草の葉1枚相手の仕事をする機会が実際にあったときには、道具ベルトにはそれに適した道具を用意しておく必要があります。先日、「 入出力操作を減らしてRPGを高速化する(パート1)」という記事で、単純な読み取りループがあるRPGプログラムを取り上げて、それをSQLカーソルでアップグレードする手法を紹介しました。ここでは、それをもう少し先に進めて、そうした読み取りループ内での他のファイル アクセスをどのようにしたら処理できるかについて見て行こうと思います。

では、単純なRPGプログラムを見てみましょう。

1	**FREE
2	Dcl-f AcctMstr  Usage(*Input) Keyed;
3	Dcl-f AcctMsExt Usage(*Input) Keyed;
4	Dcl-f AcctMsOpt Usage(*Input) Keyed;

5	Dcl-pi *n;
6	  inBranch Packed(3:0);
7	End-pi;

8	setll branch AcctMstr;
9	reade branch AcctMstr;

10	dow not %eof(AcctMstr);
11	  Chain (branch:acct) AcctMsExt;
12	  If %Found(rAcctMsExt);
13	    Chain (branch:acct) AcctMsOpt;
14	    //... Do Important Stuff
15	  Endif; 

16	  reade branch AcctMstr;
17	EndDo;

18	*Inlr = *On;
18	Return;

1行目: **Free - このプログラムは100%フリーRPGになります。クールな若者たちは皆、そうしています。

2行目: AcctMstrは、このプログラムのプライマリー読み取りファイルです。

3行目: AcctMsExtは、プライマリー ファイルとマッチさせるファイル、または、レコードの処理には必要ないファイルです。

4行目: AcctMsOptは、branch/accountキー用に存在する場合に、そのデータを使用するファイルです。

5行目~7行目: 前回の記事の読者からのコメントで、実際にはプログラムのプロトタイプを指定する必要はないというご指摘がありました。それを踏まえて、コードを3行省いて、プロシージャー インターフェースを指定するのみとしました。このことを思い出させてくれたコメントに感謝します。古い習慣はなかなか直らないものです。

11行目および12行目: これは、従来のRPGのchainとIfの組み合わせです。ただし、ここでは、以前に読み取ったレコードに関心を持ち続けるためには、マッチがなければならないことをRPGプログラムが指示しています。

13行目: ここに、もうひとつchainがありますが、このchainの後には%Found組み込み関数が続かない点に注目してください。このファイルからのデータはオプションです。

読み取りループをSQLカーソルに置き換えただけだった、このRPGプログラムの1つ目のバージョンでは、入出力が265,411回から11回へ減りました。今回見ているプログラムでは、それぞれの読み取り命令に2つのchain命令があるため、このプログラムへの影響は3倍になります。つまり、ここでは、このプログラムは265,411回の入出力操作を実行しているのではなく、796,233回の入出力操作を実行しています。

では、同じプログラムですが、SQLカーソルを使用して、一度に3つすべてのファイルからデータを取得するバージョンを見てみましょう。

1	**FREE
2	Ctl-Opt Option(*SrcStmt:*NoDebugIO);
3	Ctl-Opt DftActGrp(*no);

4	Dcl-ds t_acctMstr  Extname('ACCTMSTR')  Qualified Template End-ds;
5	Dcl-ds t_acctMsOpt Extname('ACCTMSOPT') Qualified Template End-ds;

6	Dcl-ds dsAcctInfo;
7	  account Like(t_acctMstr.ACCOUNT);
8	  branch  Like(t_acctMstr.BRANCH);
9	  coOwner Like(t_acctMsOPT.COOWNER);
10	End-ds;

11	Dcl-ds dsAcctInfoAry Likeds(dsAcctInfo) Dim(25000);

12	Dcl-s index Uns(10);
13	Dcl-s rows  Uns(10) Inz(%Elem(dsAcctInfoAry));

14	Dcl-pi *n;
15	  inBranch Like(t_acctMstr.branch);
16	End-pi;

17	If Prepare_For_Data_Pull(inBranch);

18	  Dow Pull_Data();

19	    For index = 1 to rows;
20	      dsAcctInfo = dsAcctInfoAry(index);
      //... Do Important Stuff
21	    EndFor;

22	  EndDo;

23	EndIf;

24	Exec SQL Close AcctInfoCsr;

25	*Inlr = *On;
26	Return;

1行目: このプログラムは、引き続き、完全フリー フォーマットになります。

2行目: 一部のプログラムには、この制御オプションがない場合もあるので、私は通常、それを追加しています。

3行目: 私はプロシージャーで常にOn-Exit演算子をコーディングするようにしているため、指定しないと、DFTACTGRPは*YESになります。そのため、このプログラムを変更して、デフォルトの活動化グループ内で実行されなくなるようにする必要があります。

4行目および5行目: これらは、入力パラメーターと、後で使用するデータ構造を定義するのに使用するテンプレートです。

6行目~10行目: これは、後でフェッチする配列のそれぞれの次元を定義するのに使用されるデータ構造です。

11行目: これは、それぞれの結果行がどのようになるかを定義する配列です。AcctMsOptファイルから1つのフィールドだけを追加することで、16MBの制限を超えるまでに全体的な配列のサイズが増えることはありませんでした。そのため、一度に最大25,000件のレコードをフェッチし続けることができます。

13行目: 変数rowsは、11行目で定義した配列の次元数に初期化されます。これを後で実際に取り出した数に調整します。

17行目: このプロシージャーは、カーソルを宣言してオープンします。

18行目: このプロシージャーは、フェッチを実行して、繰り返し処理を行う配列に取り込みます。

20行目: この配列の次元をデータ構造に移動します。データ構造が修飾されていないため、ファイルからの必要とされるすべてのフィールドがメモリー内に置かれることに注意してください。

24行目: 必ずSQLカーソルをクローズするのを忘れないようにしてください。いつもは、On-Exitステートメント内で行いますが、この例では、このプログラムをリニア メイン プログラムに変換しないので、ループ終了時に、カーソルをクローズします。

1	// Prepare and open the cursor
2	Dcl-Proc Prepare_For_Data_Pull;

3	Dcl-Pi Prepare_For_Data_Pull Ind;
4	  p_branch Like(t_acctMstr.branch);
5	End-Pi;

6	Dcl-s result Ind Inz(*On);

7	Exec SQL Declare AcctInfoCsr INSENSITIVE Cursor For
8	  Select a.account, a.branch, e.ActAge,
                 Coalesce(o.CoOwner,'')
9	  From AcctMstr a
10	  Inner Join AcctMsExt e on a.account = e.account and
                                  a.branch  = e.branch
11	  Left Outer Join AcctMsOpt o on a.account = o.account and
                                       a.branch  = o.branch
12	  Where a.branch = :p_branch
13	  Order By a.account
14	  For Read Only;

15	Exec SQL Open AcctInfoCsr;

16	If sqlCode < 0;
17	  result = *Off;
18	Endif;

19	Return result;

20	On-Exit;
21	End-Proc Prepare_For_Data_Pull;

1行目: プロシージャー名は、ある程度自らを説明するようなものにするべきですが、私は常に、プロシージャーがどのような処理を行うのかについての簡単な1行のコメントを追加するようにしています。少なくとも、プロシージャーと前の行を見た目にも区別しやすくなるはずです。

7行目: デフォルトでは、SQLカーソルは insensitive (データが変更された場合に、カーソルはその変更を反映しません。フェッチ発生時点でのデータのスナップショットであるに過ぎません)として定義されますが、私がどうしても insensitive を指定するようにしているのは、そのつもりだったことが、私自身にも私のコードを読む他の誰にとっても明確になるからです。

8行目: 「重要な処理を行う」ために必要となるフィールドを選択します。選択するフィールドに注目してください。

  1. 「a」という別名を指定したAcctMstrからの2つのフィールド。
  2. 「e」という別名を指定したAcctMsExtからの1つのフィールド。このファイルはInner Joinを使用するため、「e」フィールドには常にデータが含まれ、nullにならないことを期待できます。
  3. 「o」という別名を指定したAcctMsOptからの1つのフィールド。このファイルはLeft Outer Joinを介してアクセスされるため、マッチするレコードがある場合も、ない場合もあります。したがって、「o」フィールドはnullである場合もあります。そのため、必ず、これらのフィールドをCoalesce節でラップするようにします。Coalesceステートメントは、最初のパラメーターがnullであるかどうかをテストします。その結果、nullでない場合はその値を返し、nullの場合は、2つ目のパラメーターとして指定したものを返します。いい感じではないでしょうか。したがって、このケースでは、このアカウントに共同所有者がない場合は、ブランクを返すだけです。
1	// Pull the data for the report
2	Dcl-Proc Pull_Data;

3	Dcl-Pi Pull_Data Ind;
4	End-Pi;

5	Dcl-s result Ind Inz(*On);

6	Clear dsAcctInfoAry;

7	Exec SQL
8	  Fetch Next From AcctInfoCsr For :rows Rows Into :dsAcctInfoAry;

9	If sqlstate <> '00000';
10	  result = *Off;
11	Else;
12	  Exec SQL GET DIAGNOSTICS :rows = ROW_COUNT;
13	EndIf;

14	return result;

15	On-Exit;
16	End-Proc Pull_Data;

このプロシージャーは、簡単に言えば、利用できるだけの多くのレコード(最大25,000件)を取得します。すべてのデータ レコードを1冊の本と考えてみれば、SQLカーソルは、何ページか読んだ後で、読み終えたページに指を挟んでおけるようにするものです。そのため、それ以降のこのプロシージャーの呼び出しは、次のレコードのバッチ(最大25,000件)を取得します。

6行目: プログラムがそのプロシージャーを呼び出すのは1回だけだと分かっていても、フェッチしたデータを入れる先を必ずクリアしておくようにするのは、良い習慣と言えるでしょう。後で、同僚の開発者にプログラムを修正してもらわなければならない状況が起こらないとも限りませんし、いつ起こるかも分からないからです。

7行目および8行目: カーソルを定義した、Prepare_For_Data_Pullプロシージャーでは、15行目でそれをオープンしたことを思い出してください。これで、データ パスがオープンしました。ここでは、データ構造配列に次のデータ ブロックをフェッチすることができます。

9行目~13行目: そのフェッチで何か問題が生じた場合は、*Offを返します。そうでない場合は、rows変数を、実際にフェッチした行数に調整します。

この変換後のプログラムでは、入出力操作は796,233回から11回に減りました。計算好きの方に向けて言えば、入出力操作は99.9986%減少ということになります。パート1の記事では、減少率は99.9959%でした。確かに、前回記事に比べて入出力が0.0027%減ったというだけでは、CTOに喜んではもらえないでしょう。しかし、目的は、そのやり方を示すことです。ともかく、これで、次の草の葉を処理するのに自由に使えるツールが2つ加わったことは確かです。

あわせて読みたい記事

PAGE TOP