目次


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

メインフレーム・コンピューターの C/C++ アプリケーションにアセンブラー命令を埋め込むための基本

Comments

はじめに

2015年にリリースされた IBM XL C/C++ for Linux on z Systems コンパイラーのバージョン 1 では、ユーザーが記述したアセンブラー・コードを直接 C/C++ プログラムに組み込む機能 (インライン・アセンブリー) をサポートしています。この新しいコンパイラーにより、高度に最適化されたコードが生成され、ハードウェアを最大限に利用できるほか、上級ユーザーには、チップ・レベルの命令を使用できるという高い柔軟性がもたらされます。インライン・アセンブリーを利用すると、ソフトウェア技術者は C/C++ プログラムの中で最もパフォーマンスに影響する部分をアセンブラー・コードでハンドコーディングすることができます。そのため、プログラマーの才能を最大限に活かして、アプリケーションの実行にかかる時間をさらに縮めることになります。

この連載は何回かの記事で構成されます。それぞれの記事では、Linux on z Systems のアセンブラー命令、汎用レジスターを使用したインライン・アセンブリー・ステートメント、パフォーマンスの向上、他のレジスター (浮動小数点レジスターやベクトル・レジスターなど) を使用した処理について説明します。今回の記事は連載第 1 回目として、Linux を実行する z Systems 上のインライン・アセンブリーで使用されるアセンブラー命令を取り上げます。各セクションでは、メインフレーム・コンピューターの汎用レジスター、条件コード、一般的な命令について、その概要を紹介します。

レジスター

汎用レジスターは 16 個あり、0 から 15 までの番号で指定されます。各レジスターには 64 個のビット・ポジションがあります。汎用レジスター 0 は例外的に、ベース・アドレスやインデックスを格納するように指定することはできませんが、汎用レジスター 1 から 15 はアドレスの算術演算をする際のベース・アドレス・レジスターやインデックス・レジスターとして使用することができます。一般的な算術演算や論理演算では、汎用レジスターをアキュミュレーターとして使用することができます。Linux を実行する z Systems における、一部の汎用レジスターに指定された使い方を表 1 に記載します。

表 1: Linux on z Systems の汎用レジスターに指定された使い方
レジスター名特別な使い方
r2, r3 パラメーター、戻り値
r4, r5, r6 パラメーター
r13 リテラル・プールのベース・レジスター
r14 戻りアドレス
r15 スタック・ポインター

汎用レジスター 0 から 15 は r0 から r15 と表現されます。MR (乗算) や DR (除算) などの命令では、2 つの隣り合うレジスターを結合して 1 つのオペランドの中で使用する必要があります。これらの演算では、偶数番号の汎用レジスターと奇数番号の汎用レジスターのペアをプログラムで指定しなければなりません。レジスター・ペアのオペランドは、偶数番号のレジスターで指定します。図 1 では、汎用レジスターを個別に表す場合と、ペアで表す場合にどのようになるかを示しています。

図 1. 16 個の汎用レジスターを個別に指定する場合とペアで指定する場合

インライン・アセンブリーでは、汎用レジスターの他に浮動小数点レジスターとベクトル・レジスターが使用されます。浮動小数点レジスターやベクトル・レジスターを使用したインライン・アセンブリーについては、この連載の今後の記事で詳しく説明します。

条件コード

条件コード (cc) はプロセッサー内にあるプログラム・ステータス・ワード (PSW) の 18 ビット目と 19 ビット目で構成されます。cc は 2 ビットの値であることから、ある命令を実行して取得された結果に応じて cc には 0、1、2、または 3 が設定されます。いったん設定されると、別の命令によって変更されるまで、cc は同じ状態を維持します。ほとんどの算術演算や論理演算、さらにはその他の処理の一部でも、cc に値が設定されます。実行分岐命令では、分岐判断の基準として cc の値を選択するよう指定することができます。

リスト 1 のスニペットは、cc の使い方の概要を説明したものです。インライン・アセンブリーのフォーマットについては、インライン・アセンブリーの一般的な命令を取り上げる次回の記事で詳しく説明します。

リスト 1: BRC と条件コード
1  #include <stdio.h>
2 
3  void myAdd(int a, int b) {
4    int noOverflow = 1;
5    asm("AR  %0, %2\n"                 // If an overflow occurs, AR will set cc to 3
6        "BRC 0xE, OK\n"                // BRC inspects cc: if it is not 3 (no overflow), branch to line 8, that is, skip line 7
7        "XR  %1, %1\n"                 // XR unsets noOverflow variable
8        "OK:\n"
9        : "+r"(a), "+r"(noOverflow)
10       : "r"(b)
11       : "cc"                         // cc in the clobber list to inform the compiler that condition
12      );                              // code might be changed by the assembler instructions
13   if (noOverflow){
14      printf ("Sum is %d\n", a);
15   } else {
16      printf ("Overflow\n");
17   }
18 }
19 int main() {
20   int a, b;
21   a = 11, b = -2;                    // With a=11, b= -2, no overflow will occur,
22   myAdd(a, b);                       // "Sum is 9" will be printed out
23   a = 0x7fffffff, b = 0x7fffffff;    // a and b are set with large values to cause overflow
24   myAdd(a, b);                       // Overflow will occur. "Overflow" will be printed out
25   return 0;
26 }

%0、%1、%2 は、ライン 9 とライン 10 にある第 1 オペランド (変数 a)、第 2 オペランド (変数 noOverflow)、第 3 オペランド (変数 b) をそれぞれ指しています。

ライン 5 の AR (ADD) 命令により、条件コードは以下のように設定されます。

0 加算の結果はゼロ。オーバーフローなし
1 結果はゼロより小。オーバーフローなし
2 結果はゼロより大。オーバーフローなし
3 オーバーフロー発生

ライン 6 の BRC (条件分岐) 命令によって cc の値が調べられて、この後の実行パスが決定されます。cc が 0、1、2 (つまりオーバーフローなし) の場合、実行される処理はライン 8 のラベル "OK" へと分岐され、ライン 7 の XR (排他的論理和) 命令を実質的にスキップします。cc が 3 (つまりオーバーフロー発生) の場合、次のライン 7 にある XR 命令によって noOverflow 変数の値はゼロに設定されます。

リスト 1 のコードはオーバーフローなしの場合は和を出力し、オーバーフローが発生した場合は 「Overflow」を表示します。

リスト 2: リスト 1 のプログラムをコンパイルして実行する
$ xlc -o ex1 ./example01.c
$ ./ex1
Sum is 9                            // Result of 11 + (– 2)
Overflow                            // Overflow caused by adding a = 0x7fffffff to b = 0x7fffffff

すべての命令に関して、設定される可能性がある条件コードの値についての情報は、z Systems のドキュメントを参照して下さい。特に、資料「z/Architecture Principles of Operation」(IBM Publication No. SA22-7832-10) の「Appendix C. Condition-Code Settings」を参照して下さい。

アセンブラー命令

一般的な命令では、汎用レジスターやストレージに格納されているデータを使用して処理を行います。それ以外にも、PSW 内のデータ、TOD (Time-Of-Day) クロックのデータ、一連の命令を実行した結果として得られたデータを扱う一般的な命令があります。命令によっては、命令の名前の中にオペランドの長さを示す文字または文字列 (例えば 16 ビットを示す H (ハーフワード)、32 ビットを示す F (ワード)、64 ビットを示す G (ダブルワード) など) が含まれているものもあります。ある命令のオペランドがすべて 32 ビット長の場合には、命令の名前に「F」という文字を含む場合と含まない場合があります。これと同じ命令でも、オペランドがすべて 64 ビットの場合には、その命令の名前に「G」という文字が含まれ、第 1 オペランドが 64 ビットで第 2 オペランドが 32 ビットの場合には、その命令の名前に「GF」という文字列が含まれることになります。各命令についての完全な説明は、z Systems のドキュメントを参照して下さい。

説明の内容をわかりやすくするため、この記事ではアセンブラー命令を大まかに定義したグループに分類しています。ここでは、ソフトウェア技術者が使用する可能性が最も高いと思われる一般的な命令のみにフォーカスしています。アセンブラー命令を網羅したリストは、「参考文献」セクションに記載されているリンク先で見つけることができます。

代数による算術命令グループ

これらの命令はオペランドに対して代数による演算を行います。算術演算のオペランドは 1 つのレジスターの場合もあれば、メモリー・アドレス、または即値の場合もあります。オペランドが 2 つある場合、両方のオペランドを使用して演算が行われますが、演算の結果は第 1 オペランドに格納されます。つまり、その命令は第 1 オペランドに対して、READ と WRITE の両方の処理を実行します。

代数による算術命令の多くでは、以下のように条件コードが設定されます。

0 演算の結果はゼロ。オーバーフローなし
1 結果はゼロより小。オーバーフローなし
2 結果はゼロより大。オーバーフローなし
3 オーバーフロー発生

以下のセクションでは、より使用頻度の高いフォーマットを取り上げます。

OP R1, R2: [レジスター] - [レジスター] フォーマット
第 1 オペランド R1 で指定されるレジスターに現在格納されている値と、第 2 オペランド R2 で指定されるレジスターに格納されている値を使用して、加算、減算、乗算、除算などの演算を実行します。演算の結果は第 1 オペランド R1 に格納されます。
R1 ← R1 OP R2

OP R1, I2: [レジスター] - [即値] フォーマット
第 1 オペランド R1 で指定されるレジスターに現在格納されている値に対し、第 2 オペランド I2 で指定される即値を使用して、要求される演算を実行します。演算の結果は第 1 オペランド R1 に格納されます。
R1 ← R1 OP I2

OP R1, D2 (X2,B2): [レジスター] - [ストレージ] フォーマット
第 1 オペランド R1 で指定されるレジスターに現在格納されている値と、第 2 オペランドで指定される実効アドレスに格納されている値を使用して、要求される演算を実行します。演算の結果は第 1 オペランド R1 に格納されます。
R1 ← R1 OP [value-stored-in-effective-address D2 (X2,B2)]

第 2 オペランドの実効アドレスは以下のようにして計算されます。

   D2: the displacement from the base address
+  B2: the base address
+  X2: the index to the base address

代数による算術命令の例
アセンブラー命令をプログラムに埋め込むには、(1) アセンブラー・コードに直接追加する、(2) C/C++ コードでインライン・アセンブリーを使用する、という 2 つの方法のいずれかを使用します。このセクションでは、AH (add halfword: ハーフワードを追加) という代数による算術命令を C プログラムに挿入する方法を説明します。AH 命令は、第 1 オペランドのレジスターの内容に、第 2 オペランドで指定されるストレージの中にある 2 バイト・フィールド (ハーフワード) の内容を代数演算で追加します。演算の結果は第 1 オペランドのレジスターに格納されます。リスト 3 のスニペットは asm 命令を挿入する前の C プログラムです。

リスト3: example02.c (asm 命令を挿入する前の C プログラム)
1   #include <stdio.h>
2   int main() {
3       int a = 0;
4       int ar[] = {0x00112233, 0x44556677};
5       printf ("a = 0x%08x, ar[0] = 0x%08x, ar[1] = 0x%08x\n", a, ar[0], ar[1]);
6       return 0;
7   }

このプログラムを実行すると、example02.c はリスト 4 に示すように aar[0]ar[1] という値を出力します。

リスト 4: example02.c をコンパイルして実行する
$ xlc -o ex2 ./example02.c
$ ./ex2
a = 0x00000000, ar[0] = 0x00112233, ar[1] = 0x44556677

この例の目的は、AH 命令をコードに挿入することにより、アドレス ar に格納されている 2 バイトの値を値 a に加算することです。ar に格納されている 2 バイトの値は 0x0011 であり、a の現在の値は 0x0 なので、演算を行った後に想定される a の値は 0x11 になります。

(1) アセンブラー・コードに手作業で AH 命令を注入する
-S オプションを指定して example02.c をコンパイルすると、example02.s というアセンブラー・ファイルが生成されます。
$ xlc example02.c -c -S

アセンブラー・コード example02.s の中で、最も重要な部分をリスト 5 に示します。

リスト 5: アセンブラー命令を挿入する前のアセンブラー・ファイル example02.s の内容
  .L_main:
1         STMG    %r13,%r15,104(%r15)
2         LARL    %r13,$CONSTANT_AREA
3         LAY     %r15,-184(,%r15)     # Reserve 184 bytes on the stack; r15 to hold the top of the stack
4         MVHI    176(%r15),0          # Move 0 (value of a) to bytes 176 → 179 from r15
5         MVHHI   168(%r15),17         # Bytes 168 →169 from r15 hold 0x00 and 0x11 (which is 1710)
6         MVHHI   170(%r15),8755       # Bytes 170 →171 from r15 hold 0x22 and 0x33 (which is 875510)
7         MVHHI   172(%r15),17493      # Bytes 172 →173 from r15 hold 0x44 and 0x55 (which is 1749310)
8         MVHHI   174(%r15),26231      # Bytes 174 →175 from r15 hold 0x66 and 0x77 (which is 2623110)
          LA      %r2,16(,%r13)        # Load parameters for printf
          LGF     %r3,176(,%r15)
          LGF     %r4,168(,%r15)
          LGF     %r5,172(,%r15)
          BRASL   %r14,printf          # Call printf

ライン 4 からライン 8 で、スタックには a の値 (0)、ar[0] の値 (0x00112233)、ar[1] の値 (0x44556677) が退避されています。

図 2 はライン 8 を実行した後のスタックを視覚的に表したものです。ライン 9 以降で、プログラムは printf に渡すパラメーターの準備をします。そして BRASL 命令が printf 関数を呼び出します。

図 2: ライン 8 を実行した後のスタックの値

期待通りの結果を実現するには、example02.s プログラムが printf のパラメーターの準備を開始する直前の位置に AH 命令 ([レジスター] - [ストレージ] フォーマット) を追加する必要があります。具体的には、リスト 6 のように AH 命令を挿入します。

リスト 6: アセンブラー・ファイル example02.s にアセンブラー命令を挿入する
   .L_main:
1          STMG    %r13,%r15,104(%r15)
2          LARL    %r13,$CONSTANT_AREA
3          LAY     %r15,-184(,%r15)      # Reserve 184 bytes on the stack, r15 to hold the top of the stack
4          MVHI    176(%r15),0           # Move 0 (value of a) to bytes 176 → 179 from r15
5          MVHHI   168(%r15),17          # Bytes 168 →169 from r15 hold 0x00 and 0x11 (which is 1710)
6          MVHHI   170(%r15),8755        # Bytes 170 →171 from r15 hold 0x22 and 0x33 (which is 875510)
7          MVHHI   172(%r15),17493       # Bytes 172 →173 from r15 hold 0x44 and 0x55 (which is 1749310)
8          MVHHI   174(%r15),26231       # Bytes 174 →175 from r15 hold 0x66 and 0x77 (which is 2623110)

9          LA      %r1,168(,%r15)        # Load address of array ar on the stack to r1
10         L       %r0,176(,%r15)        # Load value of a (bytes 176 → 179 from r15) to r0
11         AH      %r0, 0(%r1)           # Add 2-byte stored in displacement 0 from address of ar (r1) to r0 
12         ST      %r0,176(,%r15)        # Results of AH in r0 is stored back to a (bytes 176 → 179 from r15)

           LA      %r2,16(,%r13)         # Load parameters for printf
           LGF     %r3,176(,%r15)
           LGF     %r4,168(,%r15)
           LGF     %r5,172(,%r15)
           BRASL   %r14,printf           # Call printf

ar が格納されている場所から 2 バイトを読み込めるようにするために、AHar のアドレスを知る必要があります。また、加算を実行するには a の現在の値も知る必要があります。AH のための情報を準備できるようにライン 9 からライン 10 が追加されています。

  • ライン 9: LAar のアドレスを r1 にロードします。
  • ライン 10: La の現在の値を r0 にロードします。
  • ライン 11: AH が r0 と r1 を使用します。

ライン 11 では、AH が以下の処理を実行します。

  1. ar のアドレスからディスプレイスメント 0 のところに格納されている 2 バイトの値を取得します。
    ar ={0x00112233, 0x44556677} なので、このディスプレイスメント 0 のところにある 2 バイトは 0x0011 になります。
  2. この 2 バイトの値を r0 の内容に加算します。
    この時点での r0 には a の値 (つまり、0) が保持されているため、加算した合計は 0x11 となります。
  3. 結果を r0 に戻すので、r0 は値 0x11 を保持することになります。

ライン 11 の AH が実行されると、r0 には新しい値が保持されます。この結果はライン 12 の ST 命令によって a に格納されます。

ライン 9 からライン 12 に AH とその他のアセンブラー命令を挿入した結果を検証するために、編集されたアセンブラー・コードをコンパイルして新しい実行ファイルを実行します。

リスト 7: 編集されたアセンブラー・コードをコンパイルして実行する
$ xlc -o ex2_new ./example02.s
$ ./ex2_new
a = 0x00000011, ar[0] = 0x00112233, ar[1] = 0x44556677           → a is 0x11, as expected

(2) インライン・アセンブリーを使用して C コードに AH 命令を埋め込む
インライン・アセンブリーは、上記と同等の結果をはるかに少ない作業で実現する手段となります。

リスト 8: C ファイル example02.c にインライン asm ステートメントを挿入する
1 #include <stdio.h>
2 int main() {
3     int a = 0;
4     int ar[] = {0x00112233, 0x44556677};
5     asm( "AH %0, %1" : "+r"(a) : "m"(ar[0]) );                                 // AH is inserted here  
6     printf ("a = 0x%08x, ar[0] = 0x%08x, ar[1] = 0x%08x\n", a, ar[0], ar[1]);
7     return 0;
8 }

インライン・アセンブリーでユーザーに要求されることは、主となる命令 (この場合は AH) を追加することだけです。インライン・アセンブリーでは、ライン 9、10、12 の LA (アドレスのロード)、L (ロード)、ST (ストア) などの補助的な処理を追加するという手間のかかる作業は、コンパイラーに任されます。このようにして C コードの中でインライン・アセンブリーを使用すると、実行時には同じ出力が生成されるはずです。

リスト 9: インライン・アセンブリーを使用した新しい C ファイルをコンパイルして実行する
$ xlc -o ex2_asm ./example02_asm.c
$ ./ex2_asm
a = 0x00000011, ar[0] = 0x00112233, ar[1] = 0x44556677          → a is 0x11, as expected

この例のインライン・アセンブリー命令は、メモリー制約を使用しています。メモリー制約については、今後の記事で Linux on z Systems のインライン・アセンブリーを取り上げる際に説明します。

比較命令グループ

このグループの命令では、オペランドが 2 つある場合には、第 1 オペランドを第 2 オペランドと比較します。比較の結果は以下のように条件コードに記録されます。

0 2 つのオペランドが等しい
1 第 1 オペランドは第 2 オペランドより小
2 第 1 オペランドは第 2 オペランドより大
3 不使用

COMP R1, R2: [レジスター] - [レジスター] フォーマット
第 1 オペランド R1 で指定されるレジスターに現在格納されている値と、第 2 オペランド R2 で指定されるレジスターに格納されている値を比較します。

COMP R1, I2: [レジスター] - [即値] フォーマット
第 1 オペランド R1 で指定されるレジスターに現在格納されている値と、第 2 オペランド I2 で指定される即値を比較します。

COMP R1, D2 (X2,B2): [レジスター] - [ストレージ] フォーマット
第 1 オペランド R1 で指定されるレジスターに現在格納されている値と、第 2 オペランドで指定される実効アドレスに格納されている値を比較します。

COMP D1(B1), I2: [ストレージ] - [即値] フォーマット
第 1 オペランドで指定される実効アドレスに格納されている値と、第 2 オペランド I2 で指定される即値を比較します。

比較命令の例
CH 5, 0x30 (0, 9)
CH (ハーフワードの比較) 命令は、ストレージに格納されている 16 ビット符号付き整数 (ハーフワード) をレジスターの内容と比較します。CH は第 1 オペランドを 32 ビット符号付き整数として扱い、ディスプレイスメントは 12 ビット符号なし整数として扱われます。

この例では、処理を実行する前の状態を以下のように想定しています。

表 2: CH を呼び出す前のレジスターとメモリー位置の値

オペコードのシンボル
OP R1, D2(X2,B2)
アセンブラー・フォーマット
CH 5, 0x30 (0, 9)
意味 内容
OP
R1
D2
X2
B2
CH
5
0x30
0
9
ハーフワード比較
レジスター 5
ディスプレイスメント
インデックス 0
レジスター 9

0xFFFF 9000 = – 2867210
0x30
0x0
0x00012050
ストレージ位置 12080 – 12081 0x9000 = – 2867210

実効アドレスは、B2 + X2 + D2 = 0x00012050 + 0x0 + 0x30 = 0x00012080 です。
アドレス位置12080 に格納されている 2 バイトの値は 0x9000、つまり - 2867210 です。
レジスター 5 の内容は 0xFFFF 9000、つまり - 2867210 です。

2 つの数値が同じなので、条件コード 0 が設定されます。

この場合も、インライン・アセンブリーは上記のように実効アドレスを直接計算するのではなく、それに代わる手段としてメモリー制約を提供することで、上記のプロセスを大幅に単純化できるようになっています。

条件分岐命令グループ

条件分岐命令では、現在の PSW の条件コードを検査します。マスクで指定される値の 1 つが条件コードに含まれている場合、PSW の中にある「次の命令アドレス」は、分岐命令の中で指定される分岐アドレスで置き換えられます。それ以外の場合は、通常の命令シーケンスが実行されます。

条件分岐命令によって条件コードが変更されることはありません。

BC M1, R2: [レジスター] - [レジスター] フォーマット
マスク M1 で指定される値の 1 つが条件コードに含まれている場合、命令の実行は第 2 オペランド R2 で指定される汎用レジスターに格納されているアドレスへと分岐します。

BC M1, D2 (X2,B2): [レジスター] - [ストレージ] フォーマット
マスク M1 で指定される値の 1 つが条件コードに含まれている場合、命令の実行は D2X2B2 に基づいて計算されるアドレスへと分岐します。

マスク
M オペランドは 4 ビット・マスクです。4 つの条件コード (0、1、2、3) は以下のようにマスクの 4 ビットに対応します。

表 3: 条件コードとマスクの関係
2 ビットの条件コード4 ビット・マスクマスクの値
002 または 01010000x8
012 または 11001000x4
102 or 21000100x2
112 または 31000010x1

想定される条件コードが、対応するマスク・ビットを選択する際の基準となります。条件コードを基準に選択されたマスク・ビットが 1 の場合、その分岐は成功します。それ以外の場合には、通常の命令シーケンスが実行されます。リスト 1 で説明したように、条件コードが 3 に等しくない場合にのみ分岐するには (つまり、条件コードが {0, 1, 2} のいずれかの場合にのみ分岐するには)、そのマスクは11102 (つまり、16 進表現で 0xE) でなければなりません。

4 つのマスク・ビットすべてがゼロの場合 (つまり R2 オペランドがゼロを保持している場合)、その分岐命令は NOP と同じことになります。同様に、マスクが 11112 の場合には、R2 がゼロでない限り無条件で分岐することになります。BCR 15, 0 を実行すると、パフォーマンスが大幅に低下する結果となる可能性があります。

BRC 命令の例

下記 absoluteValue 関数は整数を引数に取って、整数の絶対値を返すものであるため、当然ながら、以下のように簡単なものにすることができます。

int absoluteValue(int a) { return a < 0 ? -a : a; }

ただしこの例では、比較命令と BRC 命令を使用して関数を作成し、これらの命令を条件コードとマスクと組み合わせて使用する方法を示しています。

リスト 10: 比較命令と分岐命令を使用したインライン・アセンブリー・ステートメント
1   int absoluteValue(int a) {
2        asm (" CFI %0, 0\n"            // Compare value of a with 0
3             " BRC 0xA, DONE\n"        // If a >= 0, go to DONE i.e. skip line 4
4             " LCR %0, %0\n"           // Coming here means a <0, load-complement a to negate it
5             " DONE:\n"
6             :"+r"(a)
7             );
8       return a;
9   }

%0 は、インライン・アセンブリー・ステートメントの第 1 オペランドを指しており、この場合は変数 a です。

ライン 2 で、a はゼロと比較されています。「比較命令グループ」セクションで説明したように、CFI 命令は PSW の条件コードを以下のように設定します。

表 4: cc とマスクとの関係
a を 0 と比較条件コードマスク・ビット
a = 00 = 0021000
a < 01 = 0120100
a > 02 = 1020010

関数 absoluteValue(int a) のロジックは、a が 0 以上の場合、この関数は a を返すことを定めています。これは条件コードが 0 または 2 に設定されているのと同じことです。これらのケースに対応するマスク・パターンは 1010 (つまり 16 進で 0xA) です。

ライン 3 の BRC は、PSW の現在の条件コードがマスク 0xA と一致するかをチェックしています。一致する場合はライン 5 のラベル DONE へと分岐し、実質的にこの関数が a を変更しないままで返す準備をします。このパスに従うと、absoluteValue(int a) は a >= 0 の場合に a を返します。

ライン 3 の BRC で一致しなかった場合、ライン 4 で補数をロードする LCR 命令が実行されます。LCR 命令は、第 1 オペランドの位置 (変数 a そのもの) に第 2 オペランド (変数 a) の 2 の補数をロードし、実質的に a の値の符号を反転させています。このパスに従うと、この関数は a が負の場合に -a を返します。

プログラムのパフォーマンスを最大限に高めるために、ソフトウェア技術者は z Systems の極めて豊富なアセンブラー命令の中から最適な命令を選択することができます。例えば、上記の関数を (下記リスト 12 のような) 異なるコードで記述すると、より高いパフォーマンスを実現することができます。

ロード命令グループ

オペランドが 2 つある場合、ロード命令は第 2 オペランドをそのまま変更せずに、あるいは符号拡張して第 1 オペランドの位置に格納します。
処理の方向は Operand1Operand2 です。

L R1, R2: [レジスター] - [レジスター] フォーマット
第 2 オペランド R2 で指定されるレジスターに格納されている値をそのまま変更せずに、あるいは符号拡張して第 1 オペランド R1 で指定されるレジスターに格納します。

L R1, I2: [レジスター] - [即値] フォーマット
第 2 オペランド I2 で指定される即値をそのまま変更せずに、あるいは符号拡張して第 1 オペランド R1 で指定されるレジスターに格納します。

L R1, D2 (X2,B2): [レジスター] - [ストレージ] フォーマット
第 2 オペランドで指定される実効アドレスに格納されている値をそのまま変更せずに、あるいは符号拡張して第 1 オペランド R1 で指定されるレジスターに格納します。

ロード命令の例
ロード命令の使い方を実際の例で説明するために、「BRC 命令の例」で取り上げた関数 absoluteValue を少し変更します。

リスト 11: ロード命令の例
1    int absoluteValue(int a) {
2        asm (" LTR %0, %0\n"           // Load and test value of a 
3             " BRC 0xA, DONE\n"        // If a >= 0, go to DONE i.e. skip line 4
4             " LCR %0, %0\n"           // Coming here means a < 0, load-complement a to negate it
5             " DONE:\n"
6             :"+r"(a)
7            );
8       return a;
9   }

ライン 2 の LTR (ロードしてテスト) 命令は以下の 2 つの処理を実行します。

  1. 第 2 オペランドをそのまま変更せずに第 1 オペランドの位置に格納する
  2. ロードされた値に応じて条件コードを更新する
0 ロードされた値はゼロ
1 ロードされた値はゼロより小
2 ロードされた値はゼロより大
3 不使用

2 つのオペランドは、両方とも変数 a を保持する同じレジスターであるため、ライン 2 の LTR は、a がゼロより小か、ゼロか、ゼロより大かを、データを移動せずにテストするのと同じことになります。

ライン 3 の BRC は現在の条件コードがマスク 0xA と一致するかを検査しています。一致する場合 (つまり a >= 0 の場合)、ライン 5 のラベル DONE へと分岐するため、この関数は a をそのまま変更せずに返します。それ以外の場合 (つまり a < 0 の場合) には、ライン 4 の LCR によって a の 2 の補数をロードすることで、その値の符号を反転させます。従って、この関数は -a を返します。

この関数とまったく同じ関数を別のアセンブラー命令で作成し、より高いパフォーマンスを実現することができます。次の例では、1 つのロード命令によって、上記の 2 つのロード命令、1 つの分岐命令、そしてラベルを置き換えています。

リスト 12: 別の asm 命令を使用する
1   int absoluteValue(int a) {
2       asm (" LPGFR %0, %0\n" :"+r"(a) );          // Load-Positive a
3       return a;
    }

LPGFR 命令は、第 1 オペランドに第 2 オペランドの絶対値をロードします。どちらのオペランドも変数 a なので、この命令は実質的に代入演算 a = | a | を実行します。この関数は LPGFR を実行した後、要求されている値を返します。

ストア命令グループ

オペランドが 2 つある場合、ST (ストア) 命令は第 1 オペランドを第 2 オペランドの位置に格納します。

処理の方向は Operand1 Operand2 です。

ST R1, D2 (X2,B2): [レジスター] - [ストレージ] フォーマット
第 1 オペランド R1 に格納されている値をそのまま変更せずに、第 2 オペランドで指定される実効アドレスに格納します。

注: z Systems には他にも数多くの命令があります。この記事では代表的な命令のみを取り上げています。

ストア命令の例
以下の例での ST 命令は、現在変数 b を保持しているアドレスに変数 a の値を格納します。処理を実行する前は a = 1 かつ b = 0 なので、処理を実行すると b が 1 になります。b = 1 であることから、関数は 0 を返します。

この例では、メモリー制約 "m" を使用しています。メモリー制約を使用すると、アドレスの計算を大幅に単純化することができます。

リスト 13: ST 命令の例
1   int main() {
2       int a = 1, b = 0;               // b = 0
3       asm("ST %1,%0\n"                // Store value of a to the address which currently holds b 
4          :"=m"(b)
5          :"r"(a)
6          );
7       return a==b ? 0 : 1;           // Based on line 2, b is expected to become 1. Accordingly, function must return 0
8   }

まとめ

一般に、ある命令が 2 つのオペランドを使用して処理を行う場合、処理の方向は命令の種類によって異なります。比較命令やテスト命令は、演算の結果と共に条件コードを記録します。分岐命令は、条件コードを検査することによって次の実行パスを決定します。ロード命令で 2 つのオペランドがある場合には、第 2 オペランドを第 1 オペランドにロードします。つまり Operand1Operand2 です。一方、ストア命令は、第 1 オペランドを第 2 オペランドに格納します。つまり Operand1Operand2 です。算術命令は、両方のオペランドを使用して処理を行い、その結果を第 1 オペランドに格納します。つまり Operand1Operand1 OP Operand2 です。

Linux on z Systems は IBM z/OS、z/TPF、z/VSE、z/VM と同じアーキテクチャーで動作しますが、Linux on z Systems のインライン・アセンブリーはアセンブラー命令のみを受け付けます。HLASM (高水準アセンブラー) の命令や DS、DD、GETMAIN などのマクロは受け付けられません。

上級ユーザーは、コンパラーが生成するアセンブラー・コードを微調整することによって、ランタイム・パフォーマンスを向上させることができます。ただし、そうした微調整はプログラムの中で最もパフォーマンスに影響する部分のみに限定する必要があります。インライン asm ステートメントやアセンブラー命令をプログラムに挿入すると、実行速度は改善されるかもしれませんが、コンパイラーによる高度な最適化を阻害し、大幅なパフォーマンスの低下という結果になるかもしれません。例えば、インライン・アセンブリーでラベルを使用すると、インライン化などによる最適化に悪影響を与えるかもしれません。アセンブラー・コードを扱う場合には、入念な計画と十分なテストが不可欠です。

謝辞

IBM Canada の Toronto Software Lab で z/OS コンパイラー最適化のリーダーを務める Visda Vokhshoori 女史に感謝いたします。この記事を作成するにあたって、彼女の技術的な助言が重要な役割を果たしました。

参考文献

  • 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=1017364
ArticleTitle=IBM z Systems の IBM XL C/C++ コンパイラーとインライン・アセンブリーを使用してパフォーマンスを向上させる
publish-date=10222015