目次


Linux on z Systems 向けインライン・アセンブリーの高度な機能

IBM z Systems の IBM XL C/C++ コンパイラーとインライン・アセンブリーを使用してパフォーマンスを向上させる

Comments

はじめに

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 のコンパイラーが提供する最適化の範囲を超えて、高速なアプリケーションの中で最もパフォーマンスに影響するコード部分を微調整したいと思っている上級ソフトウェア技術者です。

アセンブリー・ラベル

コンパイラーは、コンパイル・プロセスにおいて、ユーザー・プログラムの中で宣言された変数や関数のそれぞれに対する内部名をオブジェクト・ファイルの中に作成します。この内部名はアセンブリー・コードの中で対応する変数や関数を参照する場合にも使用されます。アセンブリー・ラベル機能を使用すると、ユーザーはオブジェクト・ファイルの中にある特定の変数や関数の内部名を制御することができます。アセンブリー・コードが生成されると、アセンブリー・ラベルで指定された名前は、そのラベルに対応する変数または関数の名前となります。従って、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() {        //func_asm is defined here
    return 55;
}

ファイル label_b.c の中で、関数 func_asm( ) が定義されています。

リスト 2. は関数名をアセンブリー・ラベルに関連付ける label_a.c
int func() asm("func_asm");        // func is associated with “func_asm”
int main() {
   return func();                 // func is called
}

ファイル label_a.c では、1 行目のアセンブリー・ステートメントによって、関数 funcfunc_asm という名前に関連付けられています。3 行目では関数 func( ) が呼び出されていますが、この関数の定義はありません。このコードによって期待される動作は、funcfunc_asm にバインドされ、関数 func( ) の呼び出しが func_asm( ) の呼び出しになるというものです。

label_a.clabel_b.c をコンパイル、リンクして、生成された実行ファイルを実行すると、成功するはずです。実行によって 55 が返されますが、これはシンボル funcfunc_asm に関連付けられているためです。図 1 はプログラム label_a.c に対して生成されたアセンブリー・コードを示しています。これを見ると、func: [ BRASL %r14, func_asm ] の代わりに func_asm という名前が使用されていることがわかります。

図 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

リスト 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] を参照しています。

リスト 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 (レジスターのロード) 命令では、ac がロードされます。c は 3000 であるため、a は 3000 になります。次に、6 行目にある 2 つ目の LR 命令で ba がロードされます。もしプログラマーの意図が、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 命令の入力として使用するよう、コンパイラーに指示しなければなりません。マッチング制約を使用すると、コンパイラーはどちらの 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 が設定されている ca にロードし、6 行目にある 2 つ目の LR 命令は入力オペランド a に対して同じレジスターを使用するため、意図したとおり 3000 という値が b にロードされます。

図 4 は、example07a.c 用に生成されたアセンブリー・ファイル (図の中の左側) と example07b.c 用に生成されたアセンブリー・ファイル (図の中の右側) の違いを示しています。マッチング制約がない場合 (example07a.c)、コンパイラーが出力オペランド a と入力オペランド a のそれぞれに対し、2 つの異なるレジスター r1 と r5 を使用しているのが明らかです。マッチング制約を使用している example07b.c では、どちらの LR 演算にも同じ r1 レジスターを使用しています。

図 2. マッチング制約の有無によって異なる、生成されるコード

表 2 はマッチング制約を使用していない example07a.c について説明したものです。r1 で行われた更新は r5 の入力値とは無関係であるため、更新された値は 2 つ目の LR 命令では使用されません。

表 2. マッチング制約を使用しない場合のコード (example07a.s)
アセンブリー・コード説明
BRASL %r14,printfprintf INITIAL …を呼び出します
L %r3,168(,%r15)c の値を r15+168 から r3 レジスターにロードし、r3 は 3000 を保持します
L %r5,176(,%r15)a の値を r15+176 から r5 レジスターにロードし、r5 は 10 を保持します
#GS00000ユーザーのインライン・アセンブラー命令を開始します
LR %r1, %r3r3 (c の値、つまり 3000) を r1 (a) にロードします
LR %r0, %r5r5 (更新前の a の値、つまり 10) を r0 (b) にロードします
#GE00000ユーザーのインライン・アセンブラー命令を終了します

一方、マッチング制約を使用する example07b.c のアセンブリー・コードを見ると、変数 a に同じ r1 レジスターが使用されていることがわかります。1 つ目の LR 命令が実行されると r1 は更新され、更新された値が 2 つ目の LR 命令の入力値になります。そのため、b には更新された a の値が適切にロードされます。

表 3. マッチング制約を使用した場合に生成されるコード (example07b.s)
アセンブリー・コード説明
BRASL %r14,printfprintf INITIAL … を呼び出します
L %r3,168(,%r15)c の値を r15+168 から r3 レジスターにロードし、r3 は 3000 を保持します
L %r1,176(,%r15)a の値を r15+176 から r1 レジスターにロードし、r1 は 10 を保持します
#GS00000ユーザーのインライン・アセンブラー命令を開始します
LR %r1, %r3r3 (c の値、つまり 3000) を r1 (a) にロードします
LR %r0, %r1r1 (更新された 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 レジスターを使用しています。

リスト 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 に示します。

リスト 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 コミュニティーに参加して、他のメンバーとつながってください。

参考資料


ダウンロード可能なリソース


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=1017452
ArticleTitle=Linux on z Systems 向けインライン・アセンブリーの高度な機能
publish-date=10222015