インライン・アセンブラ

最近ではDSPといえどもC/C++のコンパイラはかなりいいコードをはいてくれるようになりました。しかし、FIRフィルタでも説明しているようにある種の機能はC/C++言語から効率的に利用することができません。そこで、性能上のボトルネックになる部分だけをアセンブリ言語で記述するのが最近のDSPプログラミングです。ここでは、C言語とアセンブリ言語を混用する方法の一つであるインライン・アセンブラについて説明します。

インライン・アセンブラとは高級言語プログラムの中に直接アセンブリ言語でプログラムを書くための機能です。VisualDSP++のC/C++コンパイラはこの機能を呼び出すためにasmキーワードを使います。

テンプレート

インライン・アセンブラはC/C++言語の中に直接書き込むことができます。その構文は次のとおりです。asm文は省略できる引数が多いため、かなり文法構造は複雑です。しかし、コツさえ覚えてしまえばそれほど恐れる必要はありません。

<asm文> ::= asm [ volatile ]( <template> [ : <output_param_list> 
                            [ : <input_param_list>   
                            [ : <clobbered_reg_list> ] ] ] );

<template> ::= <文字列>
<output_param_list>  ::= [ <output_param> [ , <output_param_list>  ] ]
<input_param_list>   ::= [ <input_param>  [ , <input_param_list>   ] ]
<clobbered_reg_list> ::= [ <clobbered_reg>[ , <clobbered_reg_list> ] ]

<output_param> ::=              <output_operand_constraint> ( <variable> )
<output_operand_constraint> ::= <文字列>
<variable> ::=                  <C/C++言語の変数>

<input_param> ::=              <input_operand_constraint> ( <expression> )
<input_operand_constraint> ::= <文字列>
<expression> ::=               <C/C++言語の式>

<clobbered_reg> ::= <文字列>

中心となるのは、テンプレートと呼ばれる文字列です。この文字列はCコンパイラが生成するコードの中に埋め込むアセンブリ・プログラムです。asm文は他の引数無しにテンプレートだけを引数とすることがあり、この場合テンプレート文字列の中身をそのままコンパイラの出力に埋め込みます。例えば次の例を見てください。

asm( "nop;" );

このプログラムはNOP命令をコンパイラが生成したコードの中に埋め込みます。埋め込む位置はasm文がC/C++プログラムの中に現れる位置です。つまり、asm文自身がnopに取って代わられると考えてもいいでしょう。

この使い方はとても簡単でわかりやすいのですが、アセンブリ言語で書いた部分とC/C++言語の間でデータのやり取りができません。例えば、掛け算をインライン・アセンブラで処理しようとしても、このままではC/C++言語から値を渡したり、結果をC/C++言語で処理することができません。

入力引数

テンプレートに記述されているアセンブリ・プログラムにC/C++言語から値を渡したり、逆に受け取るために入出力引数が用意されています。

まずは入力引数から見てみます。次のインライン・アセンブリプログラムはIO空間転送命令を使ってFLAGSレジスタに値を書き込んでいます。

asm( "io( 3 ) = %0;" : : "e"(0x0a) );

この文にはいくつか見所があります。まずテンプレート文字列の中に'%0'という見るからに怪しい文字列が書いてあります。つぎに、テンプレート文字列に続いて':'が二度続き、最後に"d"(0x0a) が現れます。

まずテンプレート文字列の中の%0という記号ですが、これは0番目の引数がここに埋め込まれることを表します。引数はテンプレート文字列に続いて記述され、左側から順に0番、1番となります。

次にコロン記号です。これは引数リストを分割するために使います。先の文法記述にあるように、asm文は三つの引数リストを持つことができます。それぞれの引数リストは省略可能であり、省略された場合は区切り子であるコロンだけを書きます。引数リストは次の三種類であり、左から順に<output_param_list> <input_param_list> <clobbered_reg_list>の順に記述します。

<output_param_list>
これは出力引数リストです。引数リストの中のおのおのの引数はコンマで区切られます。
<input_param_list>
これは入力引数リストです。引数リストの中のおのおのの引数はコンマで区切られます。
<clobbered_reg_list>
これは破壊されるレジスタのリストです。引数リストの中のおのおのの引数はコンマで区切られます。

文法をもう一度見てみると、入力引数リストは次のようになっています。

<input_param_list>   ::= [ <input_param>  [ , <input_param_list>   ] ]

<input_param> ::=              <input_operand_constraint> ( <expression> )
<input_operand_constraint> ::= <文字列>
<expression> ::=               <C/C++言語の式>

これを読むと入力引数リスト<input_param_list>とは入力引数<input_param>だけか、あるいは入力引数にコンマと入力引数リストを続けたものだということがわかります。入力引数リストが再帰的に用いられていることに注意してください。このことから、結果的に入力引数リストとは入力引数を任意の数だけコンマで区切って並べたものだとわかります。

入力引数<input_param>はどうなっているでしょう。文法を読んでみると入力オペランド制約<input_operand_constraint>に"( )"で包んだ式が続く形式だとわかります。式<expression>はC言語の式です。

たかが引数1つ渡すのに大変な騒ぎですが、これはひとえにC/C++コンパイラがテンプレートの中身を理解できないことに起因します。コンパイラは独自の知識と規則に従って与えられたC/C++プログラムをアセンブリ言語に落とします。当然、どのようなレジスタがあり、どのような命令でそれらのレジスタを使えるのか、使えないかを知っています。一方でテンプレート文字列はコンパイル結果であるアセンブリ・プログラムの中に機械的に挿し込まれるに過ぎません。この結果テンプレートが挿し込まれる時点では、コンパイラはプログラマがテンプレートの中でどのような命令を使っており、それがどういったレジスタをオペランドに取ることができるのか把握することはできません。

そこでasm文はテンプレートをコンパイル済みアセンブリ・プログラムに挿し込む前にコンパイラがテンプレートに細工を施すことができるような仕掛けを提供します。それがオペランド制約です。もういちど先ほどの例題を見てみましょう。

asm( "io( 3 ) = %0;" : : "e"(0x0a) );

asm文のテンプレートに続いてコロンが二つ現れます。これは出力引数列が空であることを意味します。続く入力引数列には入力オペランド制約<input_operand_constratint>として"e"が宣言されています。VisualDSP++3.0の"ADSP-219x C/C++ Compiler and Library Manual"のInline Assembly Language Support Keywordを参照すると"e"とはDREGであることがわかります。つまりこの制約は次のように宣言していることになります。

「この引数の値を評価してDREGに属するレジスタに代入しなさい。テンプレートの引数引用位置にはそのレジスタ名を埋め込みなさい」

これはもちろんコンパイラに対する宣言です。コンパイラは"e"に対応するDREGを知っていますし、コード生成の過程でDREGのどのレジスタがあいているかも知っています。また、その文字列表現も知っています。これによってコンパイラは自分自身が生成したコードと矛盾することなく、またテンプレートの意味を知ることもなく安全に0x0aという引数をIO命令に渡すことができます。なお、文法規則により括弧"( )"の中の式はC/C++言語の式でなければなりません。今回は0x0aという定数ですがこれがa+bといった動的な式ならば、実行プログラムの中で値を評価してその評価値が入るレジスタ名をテンプレートに埋め込みます。

ちなみに上の例は下のコードにコンパイルされました。

si=0x000a;
io(0x0003)=si;

出力引数

入力同様出力もオペランド制約をつかってテンプレートからC/C++言語の変数に値を返します。例を見てみましょう。

int a;

...
asm( "%0=%1+%2;" : "=c"(a) : "e"(2), "e"(3) );

ぐちゃぐちゃしてきましたが、入力の二つの引数の和をC言語の変数aに返すプログラムです。出力引数の制約は入力引数の頭に"="をつけたものです。上の"=c"はマニュアルに拠ればARレジスタを使うことを宣言しています。ここで間違って"=e"などとすると、DREGの中から適当なものが選ばれてテンプレートに埋め込まれます。その結果"ax2=ax1+si;"などという命令が生成されて、アセンブラがエラーメッセージを吐きます。

上の例は実験してみると下のようなコードに変換されました。

si=0x0002;
ax1=0x0003;
ar=si+ax1;
dm(a)=ar;

値が破壊されるレジスタ

次の例を見てください。

asm( "mr=%1*%2(ss); %0=sr0+%3;": "=c"(a) : "g"(2), "g"(3), "g"(c) );

これは2*3+4の結果を変数aに代入するインライン・アセンブリ・プログラムです。しかし、このプログラムを実行すると変数aには結果として6が格納されます。これは間違った結果です。なぜこのようなことがおきるのかは、コンパイル結果を見てみるとわかります。下はそのコンパイル結果の抜粋です。

ax1=0x0002;
ax0=0x0003;
sr1=0x0004;        // sr1 を%3として使う
sr=ax1*ax0(ss);    // sr1 が上書きされている!
ar=sr0+sr1;        // そりゃ結果を間違うよね
dm(a)=ar;

コンパイラは%3引数をsr1に割り当てています。ところがテンプレート中でこのレジスタを上書きしているために結果が狂ってしまっているのです。先に説明したようにコンパイラはテンプレートの中で何が起きているか知るすべを持ちません。そこでコンパイラに「このレジスタはテンプレートの中で壊すからね」と教えてやる必要が出てきます。それを教えるのが破壊されるレジスタのリスト<clobbered_reg_list>です。

破壊されるレジスタのリストは、レジスタ名を文字列として渡します。以下の例を見れば自明だと思います。

asm( "mr=%1*%2(ss); %0=sr0+%3;": "=c"(a) : "g"(2), "g"(3), "g"(c) : "sr0", "sr1", "sr2" );

この例ではコンパイラに対してSR0, SR1, SR2がテンプレート内部で破壊されることを宣言しています。この結果、コンパイラはsr1を使わずに他のレジスタを%3に使用します。私の手元で実験したところ、mr1が使用され、実行後のaの値は正しく10になりました。

べし、べからず集

インライン・アセンブラは非常に便利なのですが相手がコンパイラなだけに、最適化の影響をもろに食らいます。そのため、使用にあたっては細心の注意が必要です。以下にgcc関係の資料などから集めた注意事項を列挙します。

入力引数に書いてはならない
入力引数レジスタに書き込んだ場合、最適化フェーズでどのような処理をされるかわかりません。
出力引数から読んではいけない
入力の場合と同じです。Read-Modify-Writeを行うときには入力引数の制約を、同じ変数を示す引数番号にします。
出力引数に書き込んだ後に入力から読んではいけない
コンパイラはレジスタを節約するために入力引数と出力引数を同じレジスタに割り当てることがあります。これは非常に重要な注意事項です。どうしても出力のあとに入力引数を読まなければならない場合には出力制約を"=&c"のように&つきで宣言します。
メモリーへの書き込みに注意
メモリーに書き込みを行う場合には、破壊されるレジスタとして"memory"を宣言します。
最適化を避ける場合にはvolatileを使う
asm volatile (... )とすれば、テンプレート内部に最適化が及ぶことはありません。ただし、やや非効率になります。

2番目の項目には例題を示したほうがいいでしょう。次のプログラムを見てください。

a=3;
asm( "%0=%2+1; %1=%2;" : "=c"(a), "=c"(b) : "0"(a) );  // b= ++a;

このプログラムは変数aに対してRead-Modify-Writeを行います。この場合入力引数は出力引数と同じであることを明示的に宣言するため、入力引数の番号である0を制約として宣言しています。これによって、コンパイラに「%2は%0と同じレジスタを使うよ」と宣言するのです。なぜこのような宣言が必要かというと、テンプレート内部で入力引数の値を変えたくても先のべからず集の制限で変えらません。一方、出力引数を読むことも許されません。そこでこのようなRead-Modify-Writeが必要な場合は入力と出力のレジスタを共用するのです。その結果、テンプレートの中の2番目の引数にはきちんと最初の命令の結果が反映されます。上のプログラムをコンパイルすると、次のようになりました。

// %0と%2に同じレジスタが割り当てられている
ar=3;
dm(a)=3;
ar=dm(a);
ar=ar+1;  // テンプレートの最初の命令
si=ar;    // テンプレートの2番目の命令
dm(a)=ar;
dm(b)=si;

実行するとaとbの結果は4になります。これは目論見どおりです。ちなみに入力引数の制約を下のように設定するとどうなるでしょうか。

// 間違った例
a=3;
asm( "%0=%2+1; %1=%2;" : "=c"(a), "=c"(b) : "e"(a) );  // b= ++a;

生成されたコードは下のとおりです。実行するとaは4になりますがbは3です。つまり、aの変化がテンプレート内できちんと伝わらなかったのです。

// 間違った例のコンパイル結果
ar=3;
dm(a)=ar;
si=ar;
ar=si+1;  // テンプレートの最初の命令
ax1=si;   // テンプレートの2番目の命令
dm(a)=ar;
dm(b)=ax1;

まとめ

インライン・アセンブラは非常に強力な武器です。とくに後段の最適化によって入出力が最適化されることがあり、効率がいっそう高まります。反面、最適化を意識して細心の注意を払わなければ足をすくわれることになります。使用する場合にはくれぐれも注意してください。

最後に、ADSP-219xコンパイラが受け取ることのできる制約の一覧をマニュアルから引用します。

制約 説明 レジスタ
b MACのx入力 MX1, MX0, SR1, SR0, MR1, MR0, AR
B MACのy入力 MY1, MY0
c ALUの結果 AR
C MACの結果 MR0
cc 破壊されるレジスタのリストに記述して、コンパイラにASTATが破壊されることを知らせる ASTAT
d シフターのx入力 SI, SR1, SR0, MR1, MR0, AX0, AY0, AX1, AY1, MX0, MX1, MY0, MY1, AR
D シフターの結果 SR1
e データ・レジスタ SI, AX1, AX0, MX1, MX0, MY0, MY1, AY1, AY0, MR1, MR0, SR1, SR0, AR
f シフト量 SE
g ALUのx入力 AX1, AX0, AR, SR1, SR0, MR1, MR0
G ALUのy入力 AY1, AY0
memory 破壊されるレジスタのリストに記述して、コンパイラにメモリーへの書き込みが起きることを知らせる
r すべてのレジスタ SR1, SR0, SI, MY1, MX1, AY1, AX1, MY0, MX0, AY0, AX0, MR1, MR0, AR, I0-I7, M0-M7, L0-L7
u DAG1 L レジスタ L0-L3
v DAG2 L レジスタ L4-L7
w DAG1 I レジスタ I0-I3
x DAG2 I レジスタ M0-M3
y DAG1 M レジスタ I4-I7
z DAG2 M レジスタ M4-M7
=&制約 出力引数の制約。入力引数とレジスタを共用しない。
=制約 出力引数の制約
2191空挺団 | プログラム | EZ-KIT | こぼれ話 | アーキテクチャー | 命令 | レジスタ | DSP掲示板 | FAQ |