Linux on z Systems 向けインライン・アセンブリーの高度な 機能 IBM z Systems の IBM XL C/C++ コンパイラーとインライン・アセンブリー を使用してパフォーマンスを向上させる Anh Tuyen Tran Software Developer IBM 2015年 10月 22日 この記事では、Linux on z Systems 向けの IBM XL コンパイラーでサポートされているインライ ン・アセンブリーの高度な機能について説明します。対象とする読者は、IBM XL コンパイラー が提供する最適化の範囲を超えて、アプリケーションの中で最もパフォーマンスに影響する コード部分を微調整したいと思っているソフトウェア技術者です。 はじめに 2015年にリリースされた IBM XL C/C++ for Linux on z Systems, V1.1 コンパイラーでは、ユーザーが 記述したアセンブラー命令を直接 C/C++ プログラムに組み込む機能 (インライン・アセンブリー) をサポートしています。これにより、上級ユーザーには、チップ・レベルの命令を使用できると いう高い柔軟性がもたらされます。インライン・アセンブリーを利用すると、ソフトウェア技術 者は C/C++ プログラムの中で最もパフォーマンスに影響する部分をアセンブラー・コードでハン ドコーディングすることができます。そのため、プログラマーの才能を最大限に活かして、アプ リケーションの実行にかかる時間をさらに縮めることができます。 この記事では、Linux on z Systems 向けの IBM XL コンパイラーでサポートされているインライン・ アセンブリーの高度な機能を紹介する目的で、アセンブリー・ラベル、基本分岐、相対分岐、入 出力オペランドのシンボル名、マッチング制約、そしてクロバー・リストに記載されたレジス ターについて詳しく説明します。この記事で取り上げる範囲は、汎用レジスターを扱うアセンブ ラー命令に限定し、ベクトル・レジスターや浮動小数点レジスターについては、別の記事に譲る ことにします。対象とする読者は、Linux on z Systems のコンパイラーが提供する最適化の範囲を 超えて、高速なアプリケーションの中で最もパフォーマンスに影響するコード部分を微調整した いと思っている上級ソフトウェア技術者です。 アセンブリー・ラベル コンパイラーは、コンパイル・プロセスにおいて、ユーザー・プログラムの中で宣言された変 数や関数のそれぞれに対する内部名をオブジェクト・ファイルの中に作成します。この内部名 © Copyright IBM Corporation 2015 Linux on z Systems 向けインライン・アセンブリーの高度な機能 商標 ページ 1 / 11 developerWorks® ibm.com/developerWorks/jp/ はアセンブリー・コードの中で対応する変数や関数を参照する場合にも使用されます。アセン ブリー・ラベル機能を使用すると、ユーザーはオブジェクト・ファイルの中にある特定の変数 や関数の内部名を制御することができます。アセンブリー・コードが生成されると、アセンブ リー・ラベルで指定された名前は、そのラベルに対応する変数または関数の名前となります。 従って、int func( ) asm ("my_function") と宣言すると、オブジェクト・ファイル内の関数 func の名前は慣習的な _func ではなく、my_function となります。 この機能の 1 つの使用例として、通常は関数や変数の名前の前にアンダーバーが付加されるシ ステムであっても、アンダーバーで始まらない名前をリンカー用に定義する使い方が考えられ ます。ただし、アセンブリー・ラベルの指定を適用できる対象は、グローバル変数の宣言と、グ ローバル関数のプロトタイプ宣言のみであることに注意して下さい。 C プログラム label_b.c (リスト 1) と label_a.c (リスト 2) は、関数プロトタイプにアセンブリー・ ラベルを使用する方法を示すコード・スニペットです。 リスト 1. 関数 func_asm を定義する label_b.c int func_asm() { return 55; } //func_asm is defined here ファイル label_b.c の中で、関数 func_asm( ) が定義されています。 リスト 2. は関数名をアセンブリー・ラベルに関連付ける label_a.c int func() asm("func_asm"); int main() { return func(); } // func is associated with “func_asm” // func is called ファイル label_a.c では、1 行目のアセンブリー・ステートメントによって、関数 func が func_asm という名前に関連付けられています。3 行目では関数 func( ) が呼び出されています が、この関数の定義はありません。このコードによって期待される動作は、func が func_asm に バインドされ、関数 func( ) の呼び出しが func_asm( ) の呼び出しになるというものです。 label_a.c と label_b.c をコンパイル、リンクして、生成された実行ファイルを実行すると、成功す るはずです。実行によって 55 が返されますが、これはシンボル func が func_asm に関連付けられ ているためです。図 1 はプログラム label_a.c に対して生成されたアセンブリー・コードを示して います。これを見ると、func: [ BRASL %r14, func_asm ] の代わりに func_asm という名前が使 用されていることがわかります。 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 2 / 11 ibm.com/developerWorks/jp/ developerWorks® 図 1. func ではなく func_asm を呼び出す label_a.c ラベルへの分岐 ラベルへの分岐には、基本分岐と相対分岐という 2 通りの方法があります。基本分岐では、分岐 命令は特定の条件に応じて、ある 1 つのラベルへと分岐します。ラベルはプログラムの中で一 意に定義されていなければなりません。相対分岐では、ターゲット・ラベルは分岐命令の位置に 対して相対的なものになります。ターゲット・ラベルが分岐命令よりも前にある場合には、b と いう文字を分岐アドレスに追加します (b は backward (戻る) を意味します)。同様に、ターゲッ ト・ラベルが分岐命令よりも後にある場合には、f という文字を分岐アドレスに追加します (f は forward (進む) を意味します)。 基本分岐 リスト 3 は基本分岐の使い方の例です。 リスト 3. 基本分岐の例 int absoluteValue(int a) { asm (" CFI %0, 0\n" " BRC 0xA, DONE\n" " LCR %0, %0\n" " DONE:\n" :"+r"(a) ); return a; } 表 1 はリスト 3 の 2 行目で使用されている CFI (即値と比較する) 命令の条件コードとマスクの関 係を示しています。 表 1. CFI 命令の条件コードとマスクの関係 0 との比較 条件コード マスク・ビット a=0 0 = 002 1000 a<0 1 = 012 0100 a>0 2 = 102 0010 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 3 / 11 developerWorks® ibm.com/developerWorks/jp/ リスト 3 の 2 行目では、CFI 命令によって変数 a (%0) をゼロと比較しています。a == 0 または a > 0 の場合には、条件コードは 0 または 2 に設定されます (表 1 の2 行目と 4 行目)。条件コード 0 と 2 を組み合わせた場合のマスク・ビットは 10102 であり、10102 は 16 進で表現すると 0xA です。 従って 3 行目の分岐命令では、この 0xA に基づいて分岐が行われ、a >= 0 の場合には 5 行目のラ ベル DONE へと分岐することになり、この関数は 4 行目の LCR 命令を実行せずに a の値を返しま す。一方 a < 0 の場合には分岐が起こらず、4 行目の LCR 命令によって a の補数が a にロードさ れた後、a が返されます。つまり、この関数は実質的に a の絶対値を返します。この例では、4 行 目の LCR の実行をスキップするために、ラベル DONE への基本分岐が使用されています。 相対分岐 リスト 4 の例では、相対分岐を使用してループバックしています。 リスト 4. 相対分岐を使用した擬似コード asm ( "1: \n" "DoSomeWork\n" "BRCT %0, 1b \n" :"+r"(limit) ); BRCT (カウントを基準にした相対分岐) 命令は、第 1 オペランド limit の値 (%0) から 1 を減算し、 その結果を第 1 オペランドに戻して保存します。結果がゼロでない場合、BRCT 命令は、第 2 オ ペランドである 1b (つまり、ラベル 1-backward) で指定されるアドレスへと分岐します。ラベル 1 は、分岐命令に対して「戻った (backward) 1 行目になります。この例では、limit がゼロでない限 り、BRCT 命令によって limit がデクリメントされて、ラベル 1 へとループバックします。limit がゼロになると、ループが終了します。 相対分岐では、ラベル名に含められるのは数字のみであることに注意して下さい。基本分岐のラ ベルには、この前提条件は適用されません。また、ラベルは同じアセンブリー・ステートメント 内になければなりません。別のアセンブリー・ステートメント内のラベルへのジャンプはサポー トされていません。 シンボル名 入出力オペランドをシンボル名で定義することもできます。アセンブリー・コードの中では、シ ンボル名を参照することができます。シンボル名は角括弧で囲んで指定し、その後に制約ストリ ングを続けます。アセンブリー・コードの中でシンボル名を参照するには、パーセント記号の後 にオペランドの数字を続けるのではなく、%[name] を使用します。シンボル名は、C の有効な変数 名にすることができ、その名前が前後の C コードの中で定義されていても構いません。ただし、 シンボル名は各インライン・アセンブリー・ステートメントの中で一意でなければなりません。 リスト 5 のスニペットでは、シンボル名として [results]、[first]、[second] を使用して、それぞれ が第 0 オペランド、第 1 オペランド、第 2 オペランドを表しています。このステートメントでは %0、%1、%2 を参照せずに %[result]、%[first]、%[second] を参照しています。 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 4 / 11 ibm.com/developerWorks/jp/ developerWorks® リスト 5. シンボル名の使い方の例 int main(){ int sum = 0, one=1, two = 2; asm ("AR %[result], %[first]\n" "AR %[result], %[second]\n" :[result] "+r"(sum) :[first] "r"(one), [second] "r"(two) ); return sum == 3 ? 0 : 1; } マッチング制約 0, 1, … 9 はマッチング制約です。マッチング制約は、入力オペランドと、番号で指定された出力 オペランドの両方に同じレジスターを割り当てるよう、コンパイラーに指示するために使用さ れます。マッチング制約はこのようなものとして、入力オペランドにのみ使用することができま す。このマッチング制約は、複数ある演算の 1 つが前の演算の結果を入力として使用する場合に は不可欠です。マッチング制約がない場合、コンパイラーは入力オペランドと出力オペランドの 両方に同じレジスターを使用しなければならないことを認識できません。 リスト 6 の C プログラム example07a.c では、マッチング制約を使用せずにプログラムを実行した 場合に誤った結果が生成される例です。 リスト 6. マッチング制約を使用しないため、誤った結果が生成される example07a.c #include <stdio.h> int main () { int a = 10, b = 200, c = 3000; printf ("INITIAL: a = %d, b = %d, c = %d\n", a, b, c ); asm ("LR %0, %2\n" "LR %1, %3\n" :"=r"(a),"=r"(b) :"r"(c), "r"(a)); printf ("RESULT : a = %d, b = %d, c = %d\n", a, b, c ); return 0; } 5 行目にある 1 つ目の LR (レジスターのロード) 命令では、a に c がロードされます。c は 3000 で あるため、a は 3000 になります。次に、6 行目にある 2 つ目の LR 命令で b に a がロードされま す。もしプログラマーの意図が、1 つ目の LR によって更新された a の値 (つまり、3000) を b に ロードすることであるなら、example07a.c の結果はそのようになるとは限りません。LR の 2 回の 呼び出しで、同じ変数 a に対して、コンパイラーが同じレジスターを使用する保証はないからで す。同じレジスターが使用されなければ、更新前の a の値である 10 が b にロードされます。リス ト 7 には、example07a.c をコンパイルして実行すると、誤った結果が生成されて b が 3000 ではな く (ほとんどの場合、3000 になりますが)、10 になる様子を示しています。 リスト 7. example07a.c をコンパイルして実行する xlc -o example07a example07a.c; ./example07a INITIAL: a = 10, b = 200, c = 3000 RESULT : a = 3000, b = 10, c = 3000 <- b is loaded with a, but b is 10 while a is 3000 ユーザーの意図は、更新された a の値を b にロードすることなので、マッチング制約を使用し て、5 行目の LR 命令の出力を 6 行目の LR 命令の入力として使用するよう、コンパイラーに指示 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 5 / 11 developerWorks® ibm.com/developerWorks/jp/ しなければなりません。マッチング制約を使用すると、コンパイラーはどちらの LR 命令を実行す る場合も変数 a に対して同じレジスターを選択します。リスト 8 は、マッチング制約を使用する ように修正したバージョンの C プログラム example07b.c を示しています。 リスト 8. マッチング制約を使用した example07b.c #include <stdio.h> int main () { int a = 10, b = 200, c = 3000; printf ("INITIAL: a = %d, b = %d, c = %d\n", a, b, c ); asm ("LR %0, %2\n" "LR %1, %3\n" :"=r"(a),"=r"(b) :"r"(c), "0"(a)); printf ("RESULT : a = %d, b = %d, c = %d\n", a, b, c ); return 0; } 修正されたプログラム example07b.c では、8 行目でマッチング制約 "0"(a) を使用して、入力オペ ランド a (%3) には 0 番目の出力オペランド a と同じレジスターを使用するよう、コンパイラーに 指示しています。5 行目にある 1 つ目の LR 命令は、3000 が設定されている c を a にロードし、6 行目にある 2 つ目の LR 命令は入力オペランド a に対して同じレジスターを使用するため、意図し たとおり 3000 という値が b にロードされます。 図 4 は、example07a.c 用に生成されたアセンブリー・ファイル (図の中の左側) と example07b.c 用 に生成されたアセンブリー・ファイル (図の中の右側) の違いを示しています。マッチング制約が ない場合 (example07a.c)、コンパイラーが出力オペランド a と入力オペランド a のそれぞれに対 し、2 つの異なるレジスター r1 と r5 を使用しているのが明らかです。マッチング制約を使用して いる example07b.c では、どちらの LR 演算にも同じ r1 レジスターを使用しています。 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 6 / 11 ibm.com/developerWorks/jp/ developerWorks® 図 2. マッチング制約の有無によって異なる、生成されるコード 表 2 はマッチング制約を使用していない example07a.c について説明したものです。r1 で行われ た更新は r5 の入力値とは無関係であるため、更新された値は 2 つ目の LR 命令では使用されませ ん。 表 2. マッチング制約を使用しない場合のコード (example07a.s) アセンブリー・コード 説明 BRASL %r14,printf printf INITIAL …を呼び出します L %r3,168(,%r15) c の値を r15+168 から r3 レジスターにロードし、r3 は 3000 を保持しま す L %r5,176(,%r15) a の値を r15+176 から r5 レジスターにロードし、r5 は 10 を保持します #GS00000 ユーザーのインライン・アセンブラー命令を開始します LR %r1, %r3 r3 (c の値、つまり 3000) を r1 (a) にロードします LR %r0, %r5 r5 (更新前の a の値、つまり 10) を r0 (b) にロードします #GE00000 ユーザーのインライン・アセンブラー命令を終了します 一方、マッチング制約を使用する example07b.c のアセンブリー・コードを見ると、変数 a に同じ r1 レジスターが使用されていることがわかります。1 つ目の LR 命令が実行されると r1 は更新さ れ、更新された値が 2 つ目の LR 命令の入力値になります。そのため、b には更新された a の値が 適切にロードされます。 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 7 / 11 developerWorks® ibm.com/developerWorks/jp/ 表 3. マッチング制約を使用した場合に生成されるコード (example07b.s) アセンブリー・コード 説明 BRASL %r14,printf printf INITIAL … を呼び出します L %r3,168(,%r15) c の値を r15+168 から r3 レジスターにロードし、r3 は 3000 を保持しま す L %r1,176(,%r15) a の値を r15+176 から r1 レジスターにロードし、r1 は 10 を保持します #GS00000 ユーザーのインライン・アセンブラー命令を開始します LR %r1, %r3 r3 (c の値、つまり 3000) を r1 (a) にロードします LR %r0, %r1 r1 (更新された a の値、つまり 3000) を r0 (b) にロードします #GE00000 ユーザーのインライン・アセンブラー命令を終了します クロバー・リスト上のレジスター名 アセンブラー命令の中で、入出力オペランドのリストに記載されていないレジスターを使用また は更新する場合、ユーザーは影響を受けるすべてのレジスターをクロバー・リストに記載しなけ ればなりません。この情報を基に、コンパイラーはインライン・アセンブリー・ステートメント の演算を実行します。 リスト 9 はアセンブラー命令のオペランドとして汎用レジスター r7 が明示的に指定されている例 を示しています。 リスト 9. 入出力オペランド・リストに記載されていないレジスターを使用する example09.c #include <stdio.h> int main () { int a = 15, b = 20; printf ("INITIAL: a = %d, b = %d\n", a, b ); asm ("LR 7, %1\n" "MSR %0, 7\n" :"+r"(a) :"r"(b) :"r7" ); printf ("RESULT : a = %d, b = %d\n", a, b ); return 0; } 5 行目の LR 命令は出力オペランドとして r7 レジスターを指定しています。6 行目の MSR 命令も 入力オペランドとして r7 を使用しています。r7 レジスターはアセンブラー命令のオペランドとし て使用されていますが、入出力オペランドのリストに r7 は記載されていません。そのため、r7 を クロバー・リストに追加して、r7 が使用されることをコンパイラーに指示する必要があります。 一般に、プログラムが正しいことを確実にするには、アセンブラー命令によって影響を受けるす べてのレジスターをオペランド・リストまたはクロバー・リストのいずれかに記載する必要があ ります。コンパイラーはその情報に基づいてレジスターの割り当てを調整します。 使用するレジスターを変えた場合のコードの違いを比較すると、特定のレジスターをクロバー・ リストに含めることによるパフォーマンスへの影響がわかります。リスト 10 に示すプログラム example09a.c では、r7 レジスターではなく r1 レジスターを使用しています。 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 8 / 11 ibm.com/developerWorks/jp/ developerWorks® リスト 10. 異なるレジスターをクロバー・リストに含める example09a.c #include <stdio.h> int main () { int a = 15, b = 20; printf ("INITIAL: a = %d, b = %d\n", a, b ); asm ("LR 1, %1\n" "MSR %0, 1\n" :"+r"(a) :"r"(b) :"r1" ); printf ("RESULT : a = %d, b = %d\n", a, b ); return 0; } 図 3 はコンパイラーによって生成された 2 つのアセンブリー・ファイルを比較しています。左側 のファイルは r7 を使用し、右側のファイルは r1 を使用しています。 図 3. 異なるレジスターをクロバー・リストに含める場合のコードを比較する 図 3 の右側では、r1 レジスターをクロバー・リストに含めた場合に、コンパイラーが変数 b に 対して r3 レジスターを選択することがわかります [ L %r3,168(,%r15) ]。もっと重要な点とし て、r1 が選択された場合、コンパイラーはクロバー・リストに含められたレジスターの内容を保 存しません。r7 が選択されると、r7 レジスターの内容は R15+56 で示される場所に保存されます [ STG %r7,56(,%r15) ]。これはつまり、r7 ではなく r1 をクロバー・リストに含めると、STORE 命令を 1 つ減らせるということです。図 6 は、適切なレジスターを選択してクロバー・リストに 含めることで、パフォーマンスが向上する可能性があることを示しています。 明示的にレジスターを指定するのが望ましくない場合には、ユーザーは、コンパイラーが適切な レジスターを選択するように、コードに変更を加えることができます。以下の例では、一時的 なレジスター・オペランドを追加し、またマッチング制約を使用することにより、そのオペラン ドが入出力オペランドの両方に使用されるようにしています。この具体的なコードが含まれた example09b.c をリスト 11 に示します。 Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 9 / 11 developerWorks® ibm.com/developerWorks/jp/ リスト 11. コンパイラーがレジスターを選択するように変更したコード #include <stdio.h> int main () { int a = 15, b = 20, tmp = 1; printf ("INITIAL: a = %d, b = %d\n", a, b ); asm ("LR %1, %2\n" "MSR %0, %3\n" :"+r"(a), "=r"(tmp) :"r"(b) , "1"(tmp) ); printf ("RESULT : a = %d, b = %d\n", a, b ); return 0; } まとめ インライン・アセンブリーを使用すると、C/C++ プログラムにアセンブラー命令を直接組み込む ことができます。この機能により、上級ユーザーは特定のコード部分にアセンブラー命令をハン ドコーディングすることで、アプリケーションのパフォーマンスをさらに向上させることができ ます。IBM XL コンパイラーでは、最適化の各レベルで生成されたコードをさらに最適化するため に、非常に高度な処理を行います。そのため、インライン ASM でパフォーマンスを向上させるに は、ユーザーはターゲット・コードの実行に関して詳細に理解している必要があります。埋め込 まれたアセンブラー命令によってパフォーマンスがどう影響を受けるかを注意深く分析し、綿密 なプランニングと徹底的なテストを行うことが、パフォーマンスの向上を実現するには不可欠で す。 謝辞 この記事の作成にあたって助言してくださった Visda Vokhshoori 女史と Nha-Vy Tran 女史に感謝 いたします。 参考文献 • IBM XL C/C++ for Linux on z Systems 製品ページにアクセスして、詳細な情報を入手してくだ さい。 • Rational C/C++ Cafe コミュニティーに参加して、他のメンバーとつながってください。 参考資料 • 「z/Architecture Principles of Operation」(IBM Publications、No. SA22-7832-10) • 「z/Architecture Reference Summary」(IBM Publications、No. SA22-7871-08) • 「Inline assembly statements」(XL C/C++ for Linux on z Systems, V1.1、2015年6月1日更新) Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 10 / 11 ibm.com/developerWorks/jp/ developerWorks® 著者について Anh Tuyen Tran Anh Tuyen Tran は、カナダのトロント大学のコンピューター・サイエンス学部ソフ トウェア・エンジニアリング・グループで学士号を取得しており、日本の山口大学 の金融関連の分野で修士号を取得しています。2007年からは IBM でソフトウェア 開発者として働いており、当初はデバッガー開発チームに、そして現在はコンパイ ラー・チームに所属しています。2009年から 2013年まではコンパイラー分野で Test Servers Administration チームのチーム・リーダーを務めていました。Anh は、Linux on z Systems 向けインライン・アセンブリーのドキュメントや、IBM POWER8 アーキ テクチャーの組み込み機能である、ハードウェア・トランザクション・メモリー機 能および暗号 (AES および SHA) 機能のドキュメントの作成における、主要なコント リビューターの一人あるとともに、IBM コンパイラーに関する何本もの記事の執筆も 行っています。 © Copyright IBM Corporation 2015 (www.ibm.com/legal/copytrade.shtml) 商標 (www.ibm.com/developerworks/jp/ibm/trademarks/) Linux on z Systems 向けインライン・アセンブリーの高度な機能 ページ 11 / 11