Fortran および Cコンパイラは、並列化しても安全が損なわれることなく、かつ並列化することが効果的であると判断されるループを対象として、自動的に並列化を行います。パフォーマンス解析ツールであるループツールは、これらのコンパイラによって作成されるループタイミングファイルを読み取ります。ループツールにはグラフィカルユーザインタフェース (GUI) が用意されています。また、ループツールのコマンド行バージョンとしてループレポートも利用できます。
この章は、以下の項目から構成されています。
ループツールおよびループレポートには以下の機能があります。
直列/ 並列を問わず、すべてのループのタイミングを測定します。
ループタイミングの表を作成します。
コンパイル中にコンパイラからヒントを収集します。
ループツールはループルーチンのグラフを表示し、どのループが並列化されたかを示します。ループのグラフ表示画面から任意のループのソースコードへと直接移動することにより、ループツールの中でソースコードを直接編集できます。
ループレポートは、グラフィカルな表示方法ではなく、ASCII ファイルとして、ループ実行時のレポートを作成します。
ループツールおよびループレポートを使用するための基本的な 4 つの手順を示します。
環境変数を設定する。
ループ解析用のタイミングファイルを作成するためのオプションを使って、プログラムをコンパイルする。
プログラムを実行して、タイミングファイルを作成する。
そのタイミングファイル上でループツールまたはループレポートを起動する。
この項の例は Fortran (f77 と f90) コンパイラを使用していますが、表示されているオプション (-xparallel、-Zlp など) は、C コンパイラでも利用できます。
-Zlp オプションをつけて実行可能ファイルをコンパイルする前に、使用するマシンに搭載されたプロセッサの数を環境変数 PARALLEL に設定します。
以下のコマンドでは、psrinfo というシステムユーティリティを使用しています。逆引用符の内部に注目してください。
% setenv PARALLEL '/usr/sbin/psrinfo | wc -l'
このコマンドはシェル起動ファイル (.cshrc または .profile) に入れることができます。
ループタイミングファイルを生成するには、自動的にコードを並列化し、最適化するコンパイラオプション (-xparallel と -xO4) を使って、プログラムをコンパイルします。 また、ループツールやループレポート用にコンパイルを行うには、 -Zlp オプションを追加します。これらのオプションを使ってプログラムをコンパイルすると、Sun WorkShop はループツールおよびループレポートが処理するためのタイミングファイルを生成します。
これら 3 つのコンパイルオプションの使用例を以下に示します。
% f77 -xO4 -xparallel -Zlp source_file
いずれの例も FORTRAN 77、Fortran 90、および C プログラムに適用されます。
ループの確認および並列化に使用できる便利なオプションがほかにも多数あります。
表 3-1 ループタイミングファイル生成用オプション|
オプション |
効果 |
|---|---|
|
-o program |
実行可能ファイル名を program に変更します。 |
|
-xexplicitpar |
DOALL プラグマの付いたループを並列化します。 |
|
-xloopinfo |
ファイルをリダイレクトして、stderr にヒントを出力します。 |
ループツールおよびループレポートでは、各種のコンパイラオプションを組み合わせることができます。
コンパイル時に自動並列化を行うための代表的なスイッチは、 -xparallel と -x04 です。ループツールとループレポート用のコンパイルを行うには、-Zlpを追加します。
% f77 -x04 -xparallel -Zlp source_file
-xO3 または -xO4 と -xparallel は併用できます。-xO3、-xO4 のいずれも指定せず、-xparallel のみ指定する場合、コンパイラは -xO3 を使用します。表 3-2 には、特定のオプションについての最適化レベルオプションの追加方法をまとめてあります。
表 3-2 最適化レベルオプションと暗黙指定|
入力 |
変換結果 |
|---|---|
|
-xparallel |
-xparallel -xO3 |
|
-xparallel -Zlp |
-xparallel -xO3 -Zlp |
|
-xexplicitpar |
-xexplicitpar -xO3 |
|
-xexplicitpar -Zlp |
-xexplicitpar -xO3 -Zlp |
|
-Zlp |
-xdepend -xO3 -Zlp |
これ以外に、-xexplicitpar と -xloopinfo というコンパイルオプションがあります。
Fortranコンパイラオプション -xexplicitpar は、 プラグマ DOALL とともに使用されます。ソースコード中で、あるループの前に DOALL を挿入すると、並列化のための明示的なマークが付けられます。-xexplicitpar を指定してコンパイルすると、そのループが並列化されます。
以下のコードは、ループの並列化を明示的にマークする方法を示したものです。
subroutine adj(a,b,c,x,n)
real*8 a(n), b(n), c(-n:0), x
integer n
c$par DOALL
do 19 i = 1, n*n
do 29 k = i, n*n
a(i) = a(i) + x*b(k)*c(i-k)
29 continue
19 continue
return
end
-Zlp を単独で使用すると、-xdepend と -xO3 が追加されます。-xdepend オプションは、コンパイラに対してループを識別するときに必要なデータ依存関係の解析を実行するよう指示します。-xparallel オプションには -xdepend も含まれますが、-xdepend は -xparallel の暗黙指定 (トリガー) ではありません。
-xloopinfo オプションは、プログラムのコンパイル時に、ループに関するヒントを stderr (ファイル記述子2のUNIX標準エラーファイル)に出力します。このヒントには、ルーチン名、ループの開始地点を示す行番号、ループの並列化の有無、並列化されていない場合はその理由などが示されます。
以下の例では、ソースファイル gamteb.F のループに関するヒントをファイル gamtab.loopinfo にリダイレクトしています。
% f77 -xO3 -parallel -xloopinfo -Zlp gamteb.F 2> gamteb.loopinfo
-Zlp と -xloopinfo の最大の違いは、ループに関するコンパイラヒントを出力することに加えて、-Zlp によって実行時にタイミングに関する統計情報を記録するようになるという点です。このため、ループツールとループレポートは、-Zlp を指定してコンパイルされたプログラムだけを対象として解析を行います。
-Zlp を指定してコンパイルした後、実行可能ファイルを実行してください。この結果、ループタイミングファイル program.looptimes が作成されます。ループツールとループレポートは、計測機構付きの実行可能ファイルとループタイミングファイルの 2 つのファイルを処理します。
読み込むプログラム (実行可能ファイル) の名前を指定することにより、ループツールを起動できます。
% looptool program &
ファイル指定なしにループツールを起動した場合、「ファイルを開く」ダイアログボックスが開き、検証するファイルの選択を要求されます。
% looptool &
ループツールは、プログラムに関連するタイミングファイルを読み込みます。タイミングファイルには、ループに関する情報が記載されています。通常、タイミングファイルには program.looptimes という形式の名前が付けられ、プログラムと同じディレクトリに入れられます。
デフォルトでは、ループツールは実行可能ファイルのあるディレクトリを検索して、タイミングファイルを探します。したがって、タイミングファイルが所定のディレクトリにあれば (正常な状態) 、探す場所を指定する必要はありません。
% looptool program &
コマンド行でタイミングファイルの名前を指定すると、ループツールおよびループレポートはそのファイルを使用します。
% looptool program program.looptimes &
コマンド行オプション -p を指定すると、ループツールおよびループレポートは -p で示されたディレクトリを検索して、タイミングファイルを探します。
% looptool -p timing_file_directory program &
環境変数 LVPATH が設定されている場合、ツールは指定されたディレクトリを検索して、タイミングファイルを探します。
% setenv LVPATH timing_file_directory % looptool program &
メインウィンドウに、ソースファイルがコンパイラに渡された順序に従って、プログラムにある各ループの各実行時間が棒グラフとして表示されます。
図 3-1には、ループツールウィンドウの構成要素を示します。

実行可能ファイルとタイミングファイルを開くには、メインウィンドウの「ファイル」メニューの「開く」コマンドを選択します。
以下の 2 つの方法で、開くファイルを指定できます。
開くファイルの名前を入力する。
ファイルチューザを起動する。
実行可能ファイルのパスを入力した後は、タイミングファイルを指定する必要はありません。ただし、タイミングファイルが別のディレクトリにある場合やデフォルト以外の名前を使用している場合は除きます。
ファイルを開く方法の詳細については、Sun WorkShop オンラインヘルプの「プログラム中のループの分析」を参照してください。
プログラム中にある全ループに関する情報を含むウィンドウ (図 3-2 参照) を開くには、メインウィンドウの「ファイル」メニューから「レポートを作成」を選択します。ここで作成されるレポートは、ループレポートで作成されるものと同じです。
レポートウィンドウの「ヘルプ」 ボタンは、コンパイラに関するヒントを含む Sun WorkShop オンラインヘルプの関連項目にリンクされています。

ループツールのグラフを印刷するには、メインウィンドウの「ファイル」メニューから 「グラフを印刷」を選択し、選択しているプリンタ名を入力します。グラフをファイルに保存するには、プリンタ名の代わりにファイル名を入力します。
印刷についての詳細は、Sun WorkShop オンラインヘルプを参照してください。
エディタを選択するには、メインウィンドウの「ファイル」メニューから「オプション」コマンドを選択し、「テキストエディタのオプション」ダイアログボックスでソースコードの編集に使用するエディタを選択します。vi、mule または xemacs のエディタが使用可能です。
vi と xemacs は、ループツールとともにディレクトリ (通常は /opt/SUNWspro/bin) にインストールされます (エディタがまだシステムにインストールされていない場合に限ります) 。mule の場合は、ユーザー自身がインストールする必要があります。いずれの場合も、使用するエディタは、ループツールが検出できるように検索パス上のディレクトリに存在しなければなりません。たとえば、mule をシステム上の /usr/local に置く場合、PATH 環境変数にはこのパスが指定されていなければなりません。
エディタの選択については、Sun WorkShop オンラインヘルプを参照してください。
メインウィンドウ上 (図 3-1) でループを選択すると、以下の 2 つの動作が行われます。
ソースコードを編集するためのウィンドウが表示され (図 3-3 参照)、vi、xemacs、mule の各 エディタが使用可能となります。
vi の詳細については、vi(1) のマニュアルページを参照してください。また xemacs および mule については、オンラインヘルプが用意されています (「ヘルプ」 ボタンをクリックしてください) 。
Sun WorkShop の vi エディタには、「バージョン」という特殊なメニューがあります。このメニューでは、SCCS (ソースコード管理システム) ユーティリティを使用して、ファイルを共有化できます。詳細については、ループツールおよび sccs(1) に関するオンラインヘルプを参照してください。
選択したループに関する 1 つあるいは複数のヒントを含む「ヒント」ウィンドウが表示されます。このウィンドウの「ヘルプ」ボタンを押すと、WorkShop オンラインヘルプのコンパイラヒントに関連する内容が表示されます。ヒントの詳細については、「コンパイラヒント」を参照してください。
図 3-3 には、ループが選択された状態の xemacs エディタウィンドウと、コンパイラヒントの説明が表示されたヒントウィンドウを示します。

ソースコードを編集すると、ループツールで表示される行番号がソースの行番号と異なる場合があります。編集済みのソースをいったん保存し、これを再コンパイルしてから最新の実行可能ファイルとともにループツールを起動すると、新しいループ情報が表示され、行番号が一致するようになります。
ループレポートの起動時に、プログラム名を入力します。loopreport の後ろに検証したいプログラム名 (実行可能ファイル) を入力してください。
% loopreport program
プログラム名を指定せずにループレポートを起動することも可能です。ただし、プログラム名を指定せずにループレポートを起動する場合、ループレポートは現在の作業ディレクトリを検索して、a.out という名前のファイルを探します。
% loopreport > a.out.loopreport
出力をファイルにリダイレクトしたり、パイプによってほかのプログラムへと受け渡すことも可能です。
% loopreport program > program.loopreport % loopreport program | more
ループレポートはプログラムに関連したタイミングファイルを一緒に読み込みます。タイミングファイルは、-zlp オプションを使用する場合に作成され、ループに関する情報を含みます。通常、タイミングファイルには program.looptimes という形式の名前が付けられ、プログラムと同じディレクトリに収められます。
しかし、 タイミングファイルの位置を指定する方法も 4 種類ほど用意されています。ループレポートは、以下に示すルールに従ってタイミングファイルを選択します。
コマンド行オプション -p が使用された場合、ループレポートは -p によって指定されるディレクトリからタイミングファイルを探します。
% loopreport program -p /home/timingfiles > program.loopreport
環境変数 LVPATH が設定されている場合、ループレポートはそのディレクトリからタイミングファイルを探します。
% setenv LVPATH /home/timingfiles % loopreport program > program.loopreport
ループレポートはループ統計情報のテーブルを標準出力である stdout に書き込みます。この出力はファイルにリダイレクトしたり、パイプによってほかのコマンドに受け渡すことも可能です。
% loopreport program > program.loopreport % loopreport program | more

以下の説明は、ループツールの「レポートの作成」出力およびループレポートの出力にもそのままあてはまります。
ループレポートには、以下の情報が含まれています。
ループ ID
コンパイル時にコンパイラによって割り当てられる任意の番号。これはループとの対話には便利な内部的なループ ID で、プログラムとの実質的な関連はありません。
行番号
ソースファイルにおけるループの先頭ステートメントの行番号。
並列
Yes ならば、ループが並列化の対象としてマークされていることを示し、No ならば、そうでないことを示します。
ヒント
「コンパイラヒントの説明」リストのヒントテキストに対応した番号。
回数
その上位からループへと入り込む回数。エントリは、ループが実行される総数であるループ反復回数とは異なります。たとえば、以下のコードは Fortran で書かれた 2 つのループです。
do 10 i=1,17
do 10 j=1,50
...some code...
10 continue
最初のループには 1 度だけ入り、ループは 17 回反復されます。2 番目のループには 17 回入り込み、ループは 17*50 = 850 回反復されます。
入れ子
ループの入れ子のレベル。ループがトップレベルのループならば、入れ子のレベルは 0 です。ループがほかのループの子ループならば、レベルは 1 となります。
たとえば、以下に示す C のコードでは、i ループはレベル 0、j ループはレベル 1、k ループはレベル 2 となります。
for (i=0; i<17; i++)
for (j=0; j<42; j++)
for (k=0; k<1000; k++)
do something;
時計時間
プログラム全体に対して、このループを実行するために費やされる経過時計時間の総計。外側のループの経過時計時間には、内側のループの経過時計時間が含まれます。以下に例を示します。
for (i=1; i<10; i++)
for (j=1; j<10; j++)
do something;
たとえば、上記の例で、外側のループ ( iループ) に割り当てられる時間が 10 秒だとしたら、内側のループ (j ループ) に割り当てられる時間は 9.9 秒となります。
% (割合)
ループの実行に費やされる時計時間として計測されたプログラム実行時間全体におけるパーセンテージ。時計時間においては、外側のループにはそこに含まれるループに費やされる時間も含まれます。
変数
ループにおいてデータ依存を生じる変数の名前。このフィールドは、コンパイラヒントに、ループがデータ依存の弊害を受けていると示される場合のみ表示されます。あるループ内の繰り返しの中で計算される値がほかのループでも使用されるなどの理由で、ループの並列化が安全に行われない場合などに、データ依存の問題は発生します。データ依存の例を以下に示します。
do i = 1, N
a(i) = b(i) + c(i)
b(i) = 2 * a(i + 1)
end do
上記のループ例が並列処理で実行される場合、a(2) の値を基に b(1) の値を再計算する反復 1 は、a(2) の再計算を終えたばかりの反復 2 の後に実行される可能性が出てきます。つまり、ループが並列処理されなかった場合とは異なり、b(1) の値は、オリジナルの値ではなく、新しい a(2) の値によって決まってしまいます。
ループツールとループレポートは、特定のループに対して適用された最適化についてのヒント、そして、なぜ並列化が行われなかったかについてのヒントを提示します。最適化の間にコンパイラが生成するヒントは、わかりにくい場合もあります。こうしたヒントは全体の文脈の中で理解されるべきものであり、特定のループに対して生成されるコードについての絶対的な事実ではありません。ただし、コンパイラがループの並列化を含めたより一層の最適化を実行できるように、コードを変換する方法についての重要な指針となることも少なくありません。
並列化の上で参考になる説明とヒントについては、『 Fortran ユーザーズガイド』を参照してください。
表 3-3 には、ループに適用される最適化についてのヒントを示します。
表 3-3 ループ最適化のヒント|
ヒント番号 |
ヒントの定義 |
|---|---|
|
0 |
ヒントはありません。 |
|
1 |
ループ中に手続き呼び出しがあります。 |
|
2 |
コンパイラはこのループに対して 2 つのバージョンを生成しました。 |
|
3 |
ループ中に、変数 "%s" に対するデータの依存関係があります。 |
|
4 |
最適化の間に、ループが大幅に変形されています。 |
|
5 |
ループは、並列化動作によって効果が得られるかどうか不明です。 |
|
6 |
ループにユーザーが挿入した DOALL プラグマが付いています。 |
|
7 |
ループ中に複数の出口があります。 |
|
8 |
ループ中にマルチスレッドで安全でない I/O またはほかの関数呼び出しがあります。 |
|
9 |
ループ中に制御が逆方向に進む箇所があります。 |
|
10 |
ループは分散して実行されている可能性があります。 |
|
11 |
複数のループが融合されている可能性があります。 |
|
12 |
複数のループが交換されている可能性があります。 |
このループに適用されるヒントはありません。これは、その他のヒントがまったく適用されないという意味ではなく、単にコンパイラから導かれるヒントがないということです。
ループ中にマルチスレッドで動作すると安全でない手続き呼び出しが含まれているので、並列化できません。このようなループを並列化すると、ループの複数のコピーによって関数呼び出しが同時にインスタンス化され、この関数に固有の変数や戻り値に悪影響を与えたり、関数の目的を無効にする可能性があります。このループ中の手続き呼び出しがマルチスレッドで使用しても安全なことがわかっている場合は、ループ本体の前に DOALLプラグマを追加することにより、このループを無条件に並列化するよう指示できます。たとえば、foo がマルチスレッドで使用しても安全な関数呼び出しである場合、c$par DOALL を挿入することによって、強制的な並列化を指定できます。
c$par DOALL
do 19 i = 1, n*n
do 29 k = i, n*n
a(i) = a(i) + x*b(k)*c(i-k)
call foo()
29 continue
19 continue
コンパイラは -parallel または -explicitpar を指定してコンパイルした場合に限り、DOALL プラグマを解釈します。-autopar を指定してコンパイルすると、DOALLプラグマは無視されます。
コンパイル時には、ループの中に並列化する意味のある要素が含まれているかどうか、コンパイラは判断できません。コンパイラは、逐次形式と並列形式の 2 つのバージョンのループを生成します。実行時の検査によって、どちらのバージョンを使用するかを決定します。この実行時検査は、ループの繰り返し値を検査して、ループの処理量を判断します。
ループ中の変数が、前回の繰り返しに使用された変数の値による影響を受けます。たとえば、次のとおりです。
do 99 i=1,n
do 99 j = 1,m
a[i, j+1] = a[i,j] + a[i,j-1]
99 continue
これは、あくまで説明の便宜上、意図的に考えた例にすぎません。このような単純なループの場合は、単にオプティマイザによって、内側と外側のループを入れ替えて、内側のループを並列化できますが、この例は、「ループ伝播データ依存性」と呼ばれるデータの依存関係の概念をわかりやすく示したものでもあります。
コンパイラは、ループ伝播データ依存性の原因となっている変数の名前を特定できる場合があります。プログラムを変更して、この種の依存関係を取り除く (または最小限に抑える) ことにより、コンパイラはさらに積極的な最適化を実行できるようになります。
コンパイラがこのループを最適化した結果、生成されたコードとソースコードの対応が取れなくなる場合があります。このため、行番号の整合性がなくなっている可能性があります。ループを大幅に変更する可能性のある最適化の例として、ループの分散化、融合、交換 (「10. ループは分散して実行されている可能性があります。」、 「11. 複数のループが融合されている可能性があります。」、 および 「12. 複数のループが交換されている可能性があります。」 を参照) などがあります。
コンパイラは、ループが並列化動作によって効果が得られるかどうかを、コンパイル時には判断できません。このヒントが表示されたループは、場合によって、「並列」のラベルが付けられることがあります。これは、コンパイラが 2 つのバージョンのループを生成 (「2. コンパイラはこのループに対して 2 つのバージョンを生成しました。」 を参照) し、並列バージョンと逐次バージョンのいずれを使用するかは実行時に決定するという意味です。
コンパイラヒントは、ループの並列化の有無を示すフラグも含め、すべてコンパイル時に生成されるので、「並列」とラベル付けされたループが実際に並列的に実行されるかどうかについて確証はありません。
このループは、コンパイラに対する DOALL プラグマの指示によって並列化されています。このヒントは、明示的に並列化を指定したループを簡単に識別する場合に利用できます。
DOALL プラグマは、-parallel または -explicitpar を指定してコンパイルする場合に限り、コンパイラにより解釈されます。-autopar を指定してコンパイルすると、コンパイラは DOALL プラグマをすべて無視します。
ループの中に、通常のループの出口以外に、GOTO やループを終了するその他の分岐が使用されています。したがって、コンパイラではループの実行時の動作が予測できないため、ループの並列化は危険と判断されます。
これは、ヒント 1 に似ています。ただし、ヒント 8 がマルチスレッドで動作すると安全でない「入出力」に適用され、ヒント 1 はマルチスレッドで動作すると安全でない「関数の呼び出し」に適用される点が異なります。
ループの中に、GOTO や、逆方向に流れたりループ本体から抜けたりするような制御フローが使用されています。すなわち、コンパイラはループの内部にある文が、すでに実行済みのコードに戻るフローになっていると解釈します。複数の出口が存在するループの場合と同様に、このループを並列化することは安全ではありません。
逆方向の制御フローを取り除く (または最小限に抑える) ことにより、コンパイラはより積極的な最適化を実現できるようになります。
ループの内容が、何度か繰り返すうちに分散している可能性があります。すなわち、ループを並列化できるようにコンパイラによってループ本体が書き換えられている可能性があります。ただし、この書き換えはオプティマイザの内部表現で使用する言語によって行われるので、もとのソースコードと書き換えたコードを対応させるのはきわめて困難になります。このため、分散されたループについてのヒントでは、実際のソースコードにある行番号には一致しない行番号が示される可能性があります。
並列化の効果を高めるよう大きなループにするため連続した 2 つのループが 1 つにまとめられています。また、この場合も、ソースの行番号が誤っている可能性があります。
内部ループと外部ループのインデックスが入れ替えられています。これは、データの依存関係を内部ループから可能な限り遠い位置に置き、この入れ子ループを並列化するための処置として行うものです。深いレベルで入れ子にしたループの場合は、3 つ以上のループで交換が行われている可能性があります。
コンパイラヒントの説明からもわかるとおり、最適化コードとソースコードを対応させるのは難しい場合があります。コンパイラが出力する情報が、可能な限りソースコードに近い形式で表示されるほうが望ましいのは明らかです。残念ながら、コンパイラオプティマイザは、プログラムを内部表現によって「読み取る」ため、もとのソースコードに対応させようとしても、それほど効果はありません。
次に、混乱の原因となりやすい最適化の手法を紹介します。
インライン化とは、 最適化レベル -O4 に限り、1 つのファイルに含まれる関数だけに 適用される最適化の 1 つです。すなわち、あるファイルに 17 個の Fortran 関数が含まれている場合、16 個の関数を残りの 1 つの関数に展開でき、-O4 レベルでコンパイルされていれば、その 16 個の関数のソースコードを 1 つの関数の本体にコピーします。これ以上の最適化を適用すると、ソースコードのどの行番号がどの最適化の対象となっているかが特定できなくなります。
コンパイラヒントの説明が不明確と思われる場合は、-O3 -parallel -Zlp を付けてコンパイルすると、コンパイラが関数をインライン化する前の段階で、ループに関するコンパイラからの指摘をより詳しく確認できます。
特に実態のないループ (すなわちコンパイラが、実在することを前提とするループで、実際のソースコード中には存在しないもの) は、インライン化の対象となっている可能性があります。
コンパイラは、ループ本体を大きく書き換えるようなループの最適化を多数実行します。 具体的には、ループの最適化、 展開、詰め込み、 分割、 入れ替えなどの操作があります。
ループツールとループレポートは、可能な限り、意味のあるヒントを提供しようとしますが、最適化済みコードとソースコードを対応させる上での根本的な問題があるため、ヒントの内容が誤解を招くこともあります。
逐次ループの中に並列ループが入れ子にされている場合、各ループを繰り返すごとに時計時間を使用するという条件があるため、ループツールおよびループレポートが報告する実行時間の情報が誤解を招く場合があります。内部ループが並列化されていると、ループの一部を並列的に繰り返す場合であっても、反復ごとの時計時間が確保されます。
これに対して、外部ループには、入れ子にした並列ループ全体の実行時間しか割り当てられません。すなわち、内部ループを並列的にインスタンス化した場合の最も長い時間に相当します。このように計測方法が二重になる結果、内部ループに比べ、外部ループの消費時間が短くなるという、「外部ループの変則性」という問題を生じることになります。