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

入出力操作を減らしてRPGを高速化する

Gregory Simmons 著

RPGプログラムを高速化する最も簡単な方法の1つは、おそらく、プログラムが実行する必要がある入出力操作の回数を減らすことでしょう。この記事では、データセットの処理へ移行するためのある単純な手法について見て行きます。

単純なRPGプログラムがあります。実を言えば、「単純な」RPGプログラムを書く機会を得るということはあまりないのですが、この例では、どのようにコードを変換するのか分かりやすくなるように、RPGプログラムから関係のない部分を削ぎ落して読み取りループだけになるようにしてあります。

1     Dcl-f AcctMstr Usage(*Input) Keyed;
2     Dcl-pr entry ExtPgm('RPGRPT1');
3       n Packed(3:0);
4     End-Pr;

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

8     setll branch AcctMstr;
9     reade branch AcctMstr;

10     dow not %eof(AcctMstr);
11       //... Do Important Stuff
12       reade branch AcctMstr;
13     EndDo;

14     *Inlr = *On;
15     Return;

このプログラムでは、branchが、渡されたbranchに等しい、AcctMstrファイルからのすべてのレコードを読み取る必要があります。また、このプログラムは、他のファイルからのデータのフォーマットおよび収集も実行する必要がありますが、ここでは、最も単純な変更に取り組むこととして、読み取りループを、SQLカーソルの繰り返し処理に置き換えてみましょう。

こうした標準的な読み取りループをSQLカーソルに置き換えることによって、入出力操作の回数を数千回(さらには数百万回)から1回に減らせるケースがたくさんあります。ここで役に立つのは、プログラムで実行される入出力操作の回数と、プログラムがそのタスクを実行できる処理速度との間に直接的な相関関係があることです。

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

3     Dcl-ds t_acctMstr Extname('ACCTMSTR') Qualified Template End-ds;
4     Dcl-ds dsAcctInfo Extname('ACCTMSTR') End-ds;
5     Dcl-ds dsAcctInfoAry Likeds(dsAcctInfo) Dim(25000);

6     Dcl-s index Uns(10);
7     Dcl-s rows  Uns(10) Inz(%Elem(dsAcctInfoAry));

8     Dcl-pr entry ExtPgm('RPGRPT1A');
9       n Like(t_acctMstr.branch);
10     End-Pr;

11     Dcl-pi entry;
12       inBranch Like(t_acctMstr.branch);
13     End-pi;

14     If Prepare_For_Data_Pull(inBranch);

15       Dow Pull_Data();

16         For index = 1 to rows;
17           dsAcctInfo = dsAcctInfoAry(index);
18             //... Do Important Stuff
19         EndFor;

20       EndDo;

21     EndIf;

22     Exec SQL Close AcctInfoCsr;

23     *Inlr = *On;
24     Return;

1行目: 一部のレガシー プログラムでは、こうしたCTL-Optが設定されていないため、それを追加します。

2行目: 私はプロシージャーで常にOn-Exit演算子をコーディングするようにしているため、指定しないと、DFTACTGRPは*YES になります。そのため、このプログラムを変更して、デフォルトの活動化グループ内で実行されなくなるようにする必要があります。この記事では、活動化グループについての詳細に立ち入る時間はありません。活動化グループの機能の詳細について確認する必要がある場合は、参考になる良い記事がいくつかありますので、そちらを参照してください。

3行目: 入力パラメーターを、私のファイルで定義されているのと同じように定義するために使用するテンプレートです。

4行目: このデータ構造を、その上のテンプレートと同じように定義している点に注意してください。そのデータ構造のLikeDsではありません。これは意図的に行っています。その定義でLikeDSを使用する場合、そのデータ構造は自動的に修飾されるからです。このプログラムが以前と同じように動作するようにしたいため、AcctMstrの全レコードがメモリー内にあり、修飾付き参照としてそれらのフィールドへの参照を変更する必要がないようにします(たとえば、AccntとdsAcctInfo.Accntなど)。

5行目: 次元数をできるだけ大きくします。それでもデータ構造配列は16MBのサイズ制限の範囲内です。

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

9行目および12行目: 定義を、ハードコーディングされた値から、ファイル内の対応するフィールドと同じように定義されるものへ変更している点に注目してください。参考情報: 可能な場合は必ず、変数は、対応するファイルで定義されている「ように」定義するようにします。そのフィールドの長さが変更された場合/ときに、プログラムで必要となるプログラマーのキータッチは少なくなり、再コンパイルを必要とするシナリオは多くなります。

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

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

17行目: 上述したように、この配列の次元を、修飾されていないデータ構造に移動することで、AcctMstrファイル内のすべてのフィールドがメモリー内に入れられています。

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

// Prepare and open the cursor

25     Dcl-Proc Prepare_For_Data_Pull;

26     Dcl-Pi Prepare_For_Data_Pull Ind;
27       p_branch Like(t_acctMstr.branch);
28     End-Pi;

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

30     Exec SQL Declare AcctInfoCsr INSENSITIVE Cursor For
31       Select *
32       From AcctMstr
33       Where branch = :p_branch
34       Order By accnt
35     For Read Only With NC;

36     Exec SQL Open AcctInfoCsr;

37     If sqlCode < 0;
38       result = *Off;
39     Endif;

40     Return result;

41     On-Exit;
42     End-Proc Prepare_For_Data_Pull;

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

31行目: いつもはSelect *を使用することはありません。代わりに、必要なフィールドのみを選択するようにしています。ただし、この例では、読み取りステートメントを置き換えているため(これにより、使用されたすべてのフィールドがメモリーに入ります)、それをシンプルにしています。これにより、ファイルからのすべてのフィールドがメモリー内に置かれるようになるため、他のどのコード変更も必要なく、それらのフィールドを使用し続けることができます。できれば、必要とされる、ファイルからのすべてのフィールドを調べたいところですが、やはり、今回はシンプルにしています。

35行目: シンプルなselectステートメントの間に想定外のコミットメント境界がオープンされた悪夢のような例について耳にして以来ずっと、それぞれのSQLステートメントごとにどのようなコミットメント制御を使用したいかを必ず明示するようにしてきました。

36行目: カーソルをオープン。何かデータを取り出す前に必要。

41行目: 私は必ず、プロシージャーでOn-Exit演算子をコーディングするようにしています。それが何も行う必要がないとしても、それは常にそこにあるため、将来のプログラマーの目に留まり、潜在的にプログラマーにこう尋ねます。「ここに入れるものはありませんか」

42行目: 私は必ず、End-Procステートメントでプロシージャー名をコーディングするようにしています。こうしておくと、上方へスクロールするときに、スクロール先のプロシージャーを見つけるもうひとつの手掛かりになります。

43     Dcl-Proc Pull_Data;

44     Dcl-Pi Pull_Data Ind;
45     End-Pi;

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

47     Clear dsAcctInfoAry;

48     Exec SQL
49       Fetch Next From AcctInfoCsr For :rows Rows Into :dsAcctInfoAry;

50     If sqlCode < 0;
51       result = *Off;
52     Else;
53       Exec sql GET DIAGNOSTICS :rows = ROW_COUNT ;
54     EndIf;

55     return result;

56     On-Exit;
57     End-Proc Pull_Data;

46行目および51行目: 私のプロシージャーの多くでは標識を返すため、プロシージャーがうまく動作したかどうかが分かります。もうひとつのアイデアは、このプロシージャーに、取り出された行数を返させることでしょう。皆さんや、皆さんのショップの同僚の開発者が一番納得がいくと感じるものであれば、どちらもお勧めです。

47行目: フェッチからrowsを受け取る配列をクリアします。確かに、データがいつあるかないかはrows変数を使用すれば分かるため、おそらく配列をクリアしないで済ませることもできるでしょうが、いずれにしても、それをクリアする方が、より安全/クリーンな感じがします。

48行目および49行目: ここで、1度で最大25,000行を取り込む1回の読み取りが処理されます。

53行目: ここでrows変数を、取得された実際の行数にセットします。また、SQLERRD(3)を使用することもできますが、先日、それは(常に)信頼できるわけではないと人づてに聞きました。私自身はそれが不正確だと思ったことはありませんが、いずれにせよ2つの選択肢があるということです。

私がテストしたところでは、入出力の回数は、265,114回の読み取りから11回 へと減りました。1度に25,000件のレコードを取り込む読み取りが10回と、残りの15,114件のレコードを取り込む読み取りが最後に1回です。これで、このプログラムにおける、このファイルの入出力は99.9959%減少しました。また、スピードの達人の同僚向けに言えば、元のプログラムの実行時間は21.87秒でしたが、カーソル見直し後は、なんと0.41秒に短縮しています(98.1253%の時間短縮)。

それでは、また、次回の記事で。楽しいコーディングを。

あわせて読みたい記事

PAGE TOP