※システム名、CPU名は一般に開発会社の登録商標です。
以下の情報はあまり過度に信用しないで下さい。より正確な情報は、asやgccのinfoから得て下さい。
個々のプロセッサ命令の解説はここでは述べません。そのような技術資料は、インテルやAMDのウェブ
サイトのdeveloper向けのページからpdf形式で入手できます。
以下の文及びプログラム例の運用結果に関して、筆者は一切責任を負いません。
参考文献
[0] D. Stancevic, K. Scheibler, J. Leto, Linux
Assembly, http://linuxassembly.org/
[1] 蒲地輝尚 著 『はじめて読むMASM』, アスキー (1988)
[2] 村瀬康治 監修 蒲地輝尚 著 『はじめて読む8086』, アスキー
(1987)
[3] Linux magazine 2001年11月号 藤沢敏喜 執筆 プログラミング工房
第23回 ステップアップC言語『asm ()文』, アスキー
[4] トランジスタ技術の過去の多くの記事, CQ出版
[5] インターフェースの過去の多くの記事, CQ出版
[6] K6 Perfect Book, ソフトバンク, Oct. 1998
[7] Intel Architecture Software Developer's Manual,
Intel Corporation, http://developer.intel.com/design/mmx/manuals/
[8] AMD-K6プロセッサ データシート, 日本AMD, Advanced Micro Devices,
Inc.
[9] AMD社製プロセッサ識別方法, 日本AMD, http://www.amd.com/japan/
[10] Using the GNU Compiler Collection (GCC), Free Software
Foundation, Inc. http://gcc.gnu.org/onlinedocs/gcc-3.2.1/gcc/index.html
x86インラインアセンブリ
インラインアセンブリ構文 レ
ジスタ 一括りの書き方 数値 関数 メモ 型と命令末尾 グローバル変数 ローカル変数 関数の引数 関数の例(不定長ビットリバーサル)
x86拡張インラインアセンブリ
拡張インラインアセンブリ構文 ConstraintとModifierの
表 引数や変数との対応付け コー
ド内でのレジスタ表記 関数例(整数の積1) 関
数例(整数の積2) 関数例(実数の積) 関
数例(2進数で出力) ※Linuxのwriteシステムコール
SIMDに関する追記
SIMDのチェック MMXを使った例(ベクト
ル引き算) 3D Now!を使った例(除算推定値)
asm {命令 dest source};
であったが、GASがAT&T形式なので、GCCでは
asm ("命令 source,dest");としてdestとsourceを逆に書くこと。
なお、 asm
が他の語と競合する可能性があれば、__asm__(" ");と書いた方が良い。
また、GCCの最適化によってもasm文の位置が変わらないようにするには、
asm volatile (" "); や asm
__volatile__ (" ");
あるいは __asm__ __volatile__ (" ");
というようにvolatileを併用する。
これは主に、繰り返し文の中に入れ込みたいときに使う。最適化オプション(-O2 等)
を指定してコンパイルするときはvolatileを付けると無難である。
アセンブリコードの中では、コメント文は下のように'#'に続けて 記述する。
asm (" movl %eax,%edi #commentアセンブリコードの#を使ったコメント文では日本語は使用を避け るべきである。
movl %edi,%edx #Move Data.
xor %edi,%edi #this is XOR.
");
■2:レジスタ■
レジスタは、%マークを付けて、
%eax や %ebp と書くこと。
また、レジスタの指しているアドレスのメモリは、
intel形式では[eax]であったが、
AT&T形式では、(%eax)である。
さらに、オフセットが val であるとき、
intel形式では[eax+val]であったが、
AT&T形式では、val(%eax)である。
AT&T形式で、val(a,b,c)と書くと、
intel形式の[a + b * c + val]と等価である。
例えば、int型の配列 array
の%eax番目の数は、intが4bytesなので、
array(,%eax,4)
である。
●メモリの参照の書き方の違い
oインテル形式 section:[base + index*scale +
displacement]
o AT&T形式
section:displacement(base, index, scale)
■3:一括り■
引用符で括った複数の命令を一つの括弧内に書く場合は、
最後の命令を除いて、文毎に"\n\t"が必要。
例えば、
asm ("foo: "); /*hogehoge*/は、一括りにして
asm (" movl $12,%eax ");
asm (" imul $3,%eax ");
asm ("foo: \n\t" /*hogehoge*/と書いても良い。
" movl $12,%eax \n\t"
" imul $3,%eax ");
asm ("命令が多いときは最後の書き方が美しいであろう。
foo: #hogehoge
movl $12,%eax
imul $3,%eax
");
asm ("foo:; movl $12,%eax; imul $3,%eax");
■4:数値■
数値は先頭に$マークを付けること。$4や$0xa03fと書く。
例えば、
asm ("foo: \n\t"と書くとfooでは、0x1f2fを%eaxに代入し、手続きか ら復帰する。
" movl $0x1f2f,%eax \n\t"
" ret ");
extern int foo (void);
■5:関数■
上の例のように、非拡張asm文を使ってC言語の関数を作るときは、asm文中に
関数名にしたいラベルを書き、リターンしたい値を%eax(浮動小数点数は%st)
に置いた後、retをする。その後改めて関数をexternで宣言する。勿論、関数
名はラベルと同じにする。
なお、asm文中のretは手続きからの復帰命令であるが、C言語
のreturn
命令であると思って差し支えない。
●整数もしくはポインタを返す場合は、その値を%eaxに入れてからretする。
●浮動小数点数を返すときは、FPUのある普通のx86プロセッサでは値を浮動小
数点レジスタスタックの先頭(%st(0)。単に%stと記述しても良い。)に積んで
からretする。(古いプロセッサでFPUがないときは%eaxを使う。)
ここで浮動小数点数を返す例を示す。
/*非拡張asm文での関数例 asm_ffunc_test.c*/実行結果
#include <stdio.h>
double a = 1.234;
/*実数を%stにロードして返すだけ*/
asm ("foo:; fldl a; ret");
extern double foo(void);int main ()
{
printf("%g\n", foo());
return 0;
}
拡張asm文で書くのが通常であり、非拡張asm文で関数を作るこ
とはまずない。
引数については後述する。
■6:メモ■
コンパイルしたときに最適化などでasm文の位置が入れ替えられることが多いので、
一組の引用符の中に命令を全て書いたり、上の例のように括弧の中に"\n\t"を用いて
つらつら記述する方が良い。
命令毎にasm文を書いた場合、意図したように動かないことが多い。
しかし、いずれにしても、%ebxや%ecx、%ebpなどは、イ
ンラインアセンブリの最初に
退避させ、最後に元に戻しておくべきである。これを怠るとまともに動かない。
ただし、スタックに積もうとしてもうまくいかないことがある。このときは、後述の
グローバル変数に退避させてごまかす方法もあるが、最良の方法は拡張インラインア
センブリの構文で書いてコンパイラ任せにすることであろう。
■7:命令の
末尾■
movやpush、pop、lea など命令はいずれも、
byte、word、longwordのデータ型でレジスタ等を扱う場合に、それぞれ
movb, movw, movl や pushb, pushw, pushl と命令末尾を使い分けること。
■8:グローバル変数■
グローバル変数は、asm文の中からもそのまま簡単に使用できる。
int foo_global;などなど。asm ("movl $12,foo_global");
asm ("movl foo_global,%eax");
■9:ローカル変数■
ローカル変数は、x86系では、%ebpを用いてアクセスする。
例えばint型は4bytesなので、以下の例では、変数は書いた順に、
-4(%ebp)、-8(%ebp)、-12(%ebp) ...となる。
ただ通常、変数を使う際の労力を減らす為に拡張インラインアセンブリを使う。
/*ローカル変数テストプログラム asm_local_test.c*/実行結果
#include <stdio.h>void foo (void)
{
int a,b,c;
/*aは-4(%ebp)、bは-8(%ebp)、cは-12(%ebp)*/asm ("movl $1,-4(%ebp)");
asm ("movl $2,-8(%ebp)");
asm ("movl $3,-12(%ebp)");printf ("%d %d %d\n", a,b,c);
return;
}int main ()
{
foo();
return 0;
}
■10:関数の引数■
x86系では関数の引数は、スタックポインタ(%esp)やフレームポインタ(%ebp)を用いて
アクセスできる。しかし、プログラムの中でスタックの状態を意識しつつコーディングせ
ねばならない。骨が折れるので、普通は拡張インラインアセンブリを使う。
詳しくは、参考文献やインテルの配布しているpdfマニュアルを参照されたし。
ここで敢えて非拡張asm文を用いた整数の掛け算の例を示す。
/*引数テストプログラム asm_argument_test.c*/実行結果
#include <stdio.h>
void foo (int a, int b)
{
int c;
/*ここでは、aは8(%ebp)、bは12(%ebp)、cは-4(%ebp)*/
asm volatile("movl 8(%ebp),%edi \n\t"
"subl 12(%ebp),%edi \n\t"
"movl %edi,-4(%ebp) ");
/* c=a-b を表示*/
printf ("%d\n", c);
}
int main ()
{
foo (3, 5);
return 0;
}
★ %espのメモ ★
(a) なお、%espを使って、
asm ("foo: \n\t"と書くと、4(%esp)つまり引数 a を%eaxに代入し、その%eaxの内容でreturnする関数 foo が
" movl 4(%esp),%eax \n\t"
" ret ");
extern int foo (int a);
具体的に、前項の(a)の方法で意味のある関数を作成してみる。
この関数は 10110001 を 10001101 と、反対から読んで返す、ビットリバースであ
る。
ただし、続きに示した通常のCで書いたコードの方が分かり易い。
注意:これは不定長のビットリバーサルであり、FFTで使われる固定長のものとは異なる。
/*
インラインアセンブリを用いたビットリバーサル。
10011 --> 11001 とビット列を逆さに配置する。
不定長であり、見つかった最初の1以降でのビットリバーサルである。
pushl/popl 命令後には、バイトサイズ4だけ%espの値が減/増する点に注意。
3回 pushl/poplしているので、バイトサイズ12だけ%espの値が変化している。
*/
__asm__ (
"btrv: \n\t"
" pushl %ebx \n\t"
" pushl %ecx \n\t"
" pushl %edx \n\t"
/****srcを%ecxにロード。16(%esp)になっているのは pushl 3回の後だから*/
" movl 16(%esp),%ecx \n\t"
/****%ecxをbit scan reverseして1のビットを見つけ番号を%edxに入れる***/
" bsr %ecx,%edx \n\t"
/****%eaxを0にする***************************************************/
" xor %eax,%eax \n\t"
/****%ecxが0ならば0を返す********************************************/
" jecxz retv \n\t"
"btrb: \n\t"
/****%ecxをbit scan forwardして1のビットを見つけ番号を%ebxに入れる***/
" bsf %ecx,%ebx \n\t"
/****%ecxの中の上の操作で見つかったビットを0にする*******************/
" btr %ebx,%ecx \n\t"
/****最後尾の1であるビット番号から今最前列の1であるビット番号を引く**/
" neg %ebx \n\t"
" add %edx,%ebx \n\t"
/****その値の位置のビットを1にする***********************************/
" bts %ebx,%eax \n\t"
/****もし%ecxが0であれば%eaxを返す***********************************/
" jecxz retv \n\t"
/****もし%ecxに1のビットが残っていれば繰り返し***********************/
" jmp btrb \n\t"
"retv: \n\t"
" popl %edx \n\t"
" popl %ecx \n\t"
" popl %ebx \n\t"
/****ret means return %eax*******************************************/
" ret \n\t"
);
/*bit reverse 10011 --> 11001*/
extern int btrv (int src);
C言語で書くと次のようになる。
もう一度書くが、これは不定長のビットリバーサルであり、FFTに使う固定長のものではない。
int btrv (int src)ページの目次へ戻る
{
int dst = 0;
while (1)
{
dst += src & 1;
src >>= 1;
if (!src) break;
dst <<= 1;
}
return dst;
}
asm (アセンブリコード : 出力オペランド : 入力オペランド : 破壊レジスタ);より細かくみると、次のようになっている。
asm ("アセンブリコード" : "指定文字列" (出力先変数) : "指定文字列" (入力用の式や変数) : "内容が破損するので退避したいレジスタ");"指定文字列"は、続く括弧でくくった式に、どのようなレジスタやメモリを割 り当てるかを決める。これは
また、"アセンブリコード"によって内容が破損してしまうレジスタを3番目の
: の後に書いておくと、その
レジスタの内容が事前に退避、事後に復帰されるようにコンパイラが自動でコード生成してくれる。これによっ
て、"アセンブリコード"はレジスタの退避、復帰を省いて書くことができる。
なお、出力と入力、コードによって破損するレジスタは何れも省略可能である。省
略する場合、その部分は空
白にしておく。曖昧でなければ : も省略可能である。
■13:Constraint
and Modifier■
出力先と入力元に使われるレジスタ指定には、1文字の指定文字もしくはその頭に修飾文字を加えた文字列が
使われる。
ただし、出力先には、"=r"というように修飾文字 '=' を付ける決まりである。
★ 386系のレジスタや定
数、メモリに対応した指定文字の抜粋
(公式には Constraint、つまり制限子とい
う。詳しくは、gccのinfo[10]のConstraintsの項目を参照。)
|
|
★ 修飾文字の抜粋 (Modifier Characters)
|
|
■14:引数や変数と
の対応付け■
実際に関数の引数やブロック変数と指定するレジスタやメモリを対応づけるには、"指定文字列"の後ろの括弧内にそれを書くだけで良い。
asm文が実行される順を追って見てみる。
1.
入力オペランド部分で括弧で囲んだ変数などの式が、その値をもって、指定したレジスタやメモリに割り当てられる。
また、出力オペランド部分で括弧で囲んだ変数も指定したレジスタやメモリに割り当てられるが、出力専用なので変数の
値は保証されない。
要するに、入力用の変数の値は指定したレジスタ,メモリの初期値になるが、出力用
の変数の値は指定したレジスタ,メモリの初期値になるとは限らない。
2. アセンブリコードが実行される。
3.
出力オペランド部分で指定したレジスタやメモリのアセンブリコード実行後の値が、対応づけた出力用の変数に保持される。
入力用の変数の値は、アセンブリコード実行後の対応するレジスタ,メモリの値を反映するとは限らない。(通常反映す
るが、asm文以前の元の値となることも多い。)
例えば x,y を入力、 z を出力に対応させるには
int foo(int x, int y)或いは、#define文を使って x,y を入力用に使うには、
{
int z;
asm ("アセンブリコード" : "=r" (z) : "r" (x), "r" (y));
return z;
}
#define foo(x,y) \というように記述する。こうするとインライン展開されて高速である。(具体例 は17)
({ int z;\
asm volatile ("アセンブリコード" : "=r" (z) : "r" (x), "r" (y));
z; })
自動的に退避してほしいレジスタを指定するのであれば、
int foo(int x, int y)などとする。また、通常はないことだが、参考文献[10]には、− 予測不可能なメモリ破壊を含むアセンブリコードを
{
int z;
asm ("%eaxと%edxの内容を壊すアセンブリコード" : "=r" (z) : "r" (x), "r" (y) : "%eax", "%edx");
return z;
}
なお、関数例などで"q"や"r"、"g"や"m"を指定している意味は特にな
い。アセンブリコードに書く命令のオペランド、
つまりdestやsourceとして正しければ良いのである。ただ、レジスタに"q"や"r"、メモリに"m"を使っていればあまり
考えずにコードが書ける。
■15:指定したレジスタの
コード内表記■
入出力用に指定した上記のレジスタやメモリは、左から指定した順に、アセンブリコードの
中では %0, %1, %2, %3,... で表記される。
上の例では、"=r" (z) で割り当てられたものが %0 で、同様に"r"
(x) が %1、"r" (y) が
%2 である。
また、 %0, %1, %2,...
だけではなく、eaxやebxといったレジスタ名も使いたい場合は、
拡張asm中では、%%eax や %%ebxというように、%%を付けるようにする。
というのは、asm文の中で %0, %1,... のような入出力オペランドが使われているときは、
その文中のレジスタ名には % が二つ付く決まりだからである。
■16:整数の積の例■
拡張インラインアセンブリを使った整数の掛け算の関数
(注意: アセンブリ文が短い場合、関数の呼び出しは非効率的なので#defineを使って
17
のように書くことを好む人が多いようである。)
int mul (int x, int y)(解説)
{
asm ("imul %2,%0"
: "=r" (x) : "0" (x), "r" (y) );
return x;
}
演算としては単純にimulで、x *= yを実行している。
実際に上の関数を使って整数の掛け算をするプログラムを示そう。
/*実行結果
asm_mul_test.c
*/
#include <stdio.h>
#include <stdlib.h>int mul (int x, int y)
{
asm ("imul %2,%0" : "=r" (x) : "0" (x), "r" (y));
return x;
}int main (int argc, char *argv[])
{
int a,b;
if (argc == 3)
{
a = atoi (argv[1]);
b = atoi (argv[2]);
printf ("%d\n", mul (a, b));
}
return 0;
}
(注意点)コンパイルオプションとして -O 等を使っても良い。
■17:整数の積の例
2■
上記の整数の掛け算を行う関数を#defineを使ってスマートに作ると、以下のようになる。
同じレジスタを入出力に使うので、出力用に"=r"を指定したものを、入力用で"0"を指定することで利用している。
"=r" (_x) と "0" (_x) は %0 、"r" (_y) が %2 である。
#define mul(x,y) \まず _x, _y というブロック変数に x , y を代入し、_x *= _y を実行している。
({ int _x=(x), _y=(y);\
asm volatile ( "imul %2,%0": "=r" (_x): "0" (_x), "r" (_y));\
_x; })
/*実行結果
asm_mul_test2.c
*/
#include <stdio.h>
#include <stdlib.h>#define mul(x,y) \
({ int _x=(x), _y=(y);\
asm volatile ( "imul %2,%0": "=r" (_x): "0" (_x), "r" (_y));\
_x; })int main (int argc, char *argv[])
{
int a,b;
if (argc == 3)
{
a = atoi (argv[1]);
b = atoi (argv[2]);
printf ("%d\n", mul (a, b));
}
return 0;
}
■18:実数の積■
浮動小数点の演算も拡張インラインアセンブリによって、出力に"=&t" 、入力に"f"を指定するだけで簡単に書ける。
例えば、倍精度の掛け算を行う関数は#define文を用いるならば
#define mul(x,y) \と書け、このブロックで_x = x * y という値が得られる。%0がxの値の入った数値浮動小数点レジスタ、
({ double _x=(x), _y=(y);\
asm volatile ( "fmul %2,%0": "=&t" (_x): "0" (_x), "f" (_y));\
_x; })
/*実行結果
浮動小数点レジスタのスタック%stが使えるが、
拡張インラインアセンブリで"f"や"t"を使う方が簡単に書ける。
*/
/*
asm_fmul_test.c
*/
#include <stdio.h>
#include <stdlib.h>#define mul(x,y) \
({ double _x=(x), _y=(y);\
asm volatile ( "fmul %2,%0": "=&t" (_x): "0" (_x), "f" (_y));\
_x; })int main (int argc, char *argv[])
{
double a,b;
if (argc == 3)
{
a = atof (argv[1]);
b = atof (argv[2]);
printf ("%f\n", mul (a,b));
}
return 0;
}
倍精度の掛け算を行うCの関数として書くならば、次のようになる。
double mul(double x, double y)(解説)
{
asm ("fmul %2,%0" : "=&t" (x) : "0" (x), "f" (y));
return x;
}
■19:関数例(2進
数で出力)■
最後にやや長めの例を示す。ここでも"q"や"r"などの Constraint は深く考えずに指定している。
以下は、整数値 n をコマンドラインから読み込んで2進数で出力するプログラムである。本当はC
のみで書く方が簡単なので、その例も続きに示す。
/*実行結果
asm_print_as_bin.c
*/
#include <stdio.h>
#include <stdlib.h>/* 整数 n のビット数をビットスキャンで得る関数 */
/*
n は入出力を兼ねるため、出力"=r"を指定し、
入力オペランドとして同じものを"0"を用いて指定する。
bsr命令はdestがレジスタである必要があるので"=g"は避ける。
また、%eaxを自動的に退避してもらう。
*/
int n_uppermost (int n)
{
asm __volatile__ (
" movl %0,%%eax \n\t"
" bsr %%eax,%0 "/*Bit Scan Reverse*/
: "=r" (n) : "0" (n) : "%eax");
return n;
}/* nのm番目のビット値を得る関数
●実際はマクロで以下のように書いた方が楽である。
#define judge_m_th_bit(n,m) \
((n) & (1<<(m)) ? 1 : 0)
*/
/*
n は入出力を兼ねるため、出力"=q"を指定し、
入力オペランドとして同じものを"0"を用いて指定する。
これも指定した回数として数えるため、 "q" (m) で割
り当てたレジスタはアセンブリコードの中では %2 で表
される。
*/
int judge_m_th_bit (int n, int m)
{
asm __volatile__ ("
bt %2,%0 #bit test ==> CF
jc one_ #goto one_ when CF is 1
movl $0,%0 #output 0 if the bit was 0
jnc out_
one_:
movl $1,%0 #output 1 if the bit was 1
out_:
" : "=q" (n) : "0" (n), "q" (m));
return n;
}/* nを2進数で表示する関数 */
void print_as_bin (int n)
{
int m;
m = n_uppermost(n);
for ( ; m > -1; m--)
printf ("%d", judge_m_th_bit(n, m));
}int main (int argc, char *argv[])
{
int a;
if (argc == 2)
{
a = abs (atoi (argv[1]));
print_as_bin (a);
putchar ('\n');
}
return 0;
}
比較の為、標準のC言語で書くと以下のようになる。
実行結果/*
print_as_bin.c
*/
#include <stdio.h>
#include <stdlib.h>
/* nを2進数で表示する関数 */
void print_as_bin (int n)
{
int m = 1, M, is_visible = 0;
M = 1 << (sizeof(int) * 8 - 1);
if (!n)
putchar ('0');
else
while(m)
{
if ((!is_visible) && (n & M))
is_visible = 1;
if (is_visible)
printf ("%d", (n & M ? 1 : 0));
n <<= 1;
m <<= 1;
}
}
int main (int argc, char *argv[])
{
int a;
if (argc == 2)
{
a = abs (atoi (argv[1]));
print_as_bin (a);
putchar ('\n');
}
return 0;
}
/*実行結果
asm_lin_sys_write.c
*/
/*
※これはx86 Linux専用のコード
※プロセッサに依存する上にOSにも依存するのである。
システムコール write で標準出力に書くテストプログラム
*/
/* size_t を得るためのインクルード */
#include <linux/types.h>
/*
文字列 s を n 文字だけstdoutに書く関数。
writeはシステムコール番号4、stdoutは fd が1。
%eaxに4、%ebxに1、%ecxに文字列のアドレス、%edxに出力
したい文字の個数を入れ、割り込みするだけ。
"=a" (result)によりアセンブリコード実行後の%eaxレジス
タの内容をresultとして出力する。
"=a", "c", "d"を使っているので%eax,%ecx,%edxは塞がっ
ており、これらをさらに退避しようとするとコンパイルで
きない。
*/
int wr_n_sys_call (const char* s, size_t n)
{
int result = 0;
__asm__ __volatile__ ("
movl $4,%%eax #system call number is 4 for write
movl $1,%%ebx #output to stdout
int $0x80 #call software interrupt
" : "=a" (result) : "c" (s), "d" (n) : "%ebx");
return result;
}
/* 文字数を得るための関数(最大1万文字) */
#define MAX_CHARS 10000
size_t get_str_length (const char* s)
{
size_t i;
for (i = 0; *s != '\0' && i < MAX_CHARS; s++)
i += sizeof(char);
return i;
}
#undef MAX_CHARS
/* puts関数の代用に使える関数 */
int yaputs (const char *s)
{
int result;
if ((result = wr_n_sys_call (s, get_str_length (s))))
wr_n_sys_call ("\n", sizeof(char));
return result;
}
int main (int argc, char *argv[])
{
if (argc == 2)
yaputs (argv[1]);
return 0;
}
ページの目次へ戻る
□□SIMDに関する追記□□
アセンブリでは、SIMD(Single Instruction Multiple Data,
1命令で複数データを処理する命令)が使える。
昨今のgas、gccでは、MMXや3D Now!のレジスタ、命令はごく普通にasm()文で記述して使用できる。Pentium
III
以降で搭載されたSSE(Streaming SIMD Extension)や、Athlonの拡張3D
Now!も利用できるが、ここで説明する
のはMMXと3D Now! だけにしておく。
■21:SIMDのサポートチェック■
まず、手持ちのCPUでどのようなSIMD命令セットがサポートされているかチェックしてみよう。
以下のプログラムは、CPUID命令を使った標準的なチェック方法である。
asm文では、入出力兼用にブロック変数 i 、入力用に引数 id_t
を用い、%eax,%ebx,%ecx,%edxを自動退避している。
/*AMDのDuronプロセッサで実行した結果
asm_simd_test.c
*/
#include <stdio.h>
/*
CPUID命令の後で %edx と比較すべき値
*/
#define ID_TEST_MMX 0x800000
#define ID_TEST_SSE 0x2000000
#define ID_TEST_3DNOW 0x80000000
/*
CPUIDでMMXやSSEを検知する関数 (インテルやAMDのマニュアル通り) [7,8]
注意: CPUIDでは、%eax, %ebx, %ecx, %edx を退避する必要がある。
*/
int cpuid_simd_detect (int id_t)
{
int i = 1; /* CPUID命令の標準関数は %eax が 1 */
if (id_t == ID_TEST_3DNOW)
i = 0x80000001; /* CPUID命令の拡張関数の利用 */__asm__ __volatile__ ("
movl %1,%%eax
cpuid
testl %2,%%edx
jnz yes_
movl $0,%0
jz no_
yes_:
movl $1,%0
no_:
" : "=m" (i) : "0" (i), "m" (id_t)
: "%eax", "%ebx", "%ecx", "%edx");
return i;
}
/*
CPUID命令の有無を返す。
以下のコードはAMDのマニュアル通り。[9]
EFLAGSレジスタの21番ビットが書き込み可能か否か
で判別する。
*/
int cpuid_supported (void)
{
int v;
__asm__ __volatile__ ("
pushf
popl %%eax #get EFLAGS
movl %%eax,%%ebx
xor $0x00200000,%%eax #toggle bit 21
pushl %%eax #write bit 21 of EFLAGS...
popf # .
pushf # .
popl %%eax # .
cmpl %%ebx,%%eax #Is bit 21 changed?
jz no_cpuid_
movl $1,%0
jmp out_
no_cpuid_:
movl $0,%0
out_:
" : "=r" (v) :: "%eax", "%ebx");return v;
}
/*
CPUID拡張関数の最大入力値が0x80000000より大きいか
否かを調べる。詳しくは参考文献[7,8,9]を参照。
*/
int cpuid_extended (void)
{
unsigned int v;
__asm__ __volatile__ ("
movl $0x80000000,%%eax
cpuid
movl %%eax,%0
" : "=r" (v) :: "%eax", "%ebx", "%ecx", "%edx");return (v > 0x80000000 ? 1 : 0);
}int main ()
{
puts ("SIMD命令セットのサポートチェック");
if (!cpuid_supported ())
{
puts ("No CPUID instruction.");
return 0;
}
printf (" MMX : %d\n", cpuid_simd_detect (ID_TEST_MMX));
printf (" SSE : %d\n", cpuid_simd_detect (ID_TEST_SSE));
printf ("3DNOW : %d\n", cpuid_extended () ?
cpuid_simd_detect (ID_TEST_3DNOW): 0);
return 0;
}
[akira@localhost mypage]$ gcc
asm_simd_test.c
[akira@localhost mypage]$ ./a.out
SIMD命令セットのサポートチェック
MMX : 1
SSE : 0
3DNOW : 1
[akira@localhost mypage]$
妥当な結果である。
■22:MMXを使った例(ベ
クトル引き算)■
MMXレジスタとして、%mm0 〜 %mm7 の8つの64-bitレジスタが利用できる。(拡張asmでは %%mm0
のように%%付きで書く。)
MMXレジスタは、64-bitを分割して以下のようなデータ型を扱える。
★ MMXデータ型の表 (詳しくはインテルの開発者用マニュアル[7]を参照)
データ型の名称 | パックの仕方 |
Packed bytes | 8-bitのデータ × 8 (char array[8]) |
Packed words | 16-bitのデータ × 4 (short int array[4]) |
Packed doublewords | 32-bitのデータ × 2 (int array[2]) |
Quadword | 64-bitのデータ × 1 (long long int variable) |
o 32-bitのデータの移動には movd
、64-bitのデータの移動には movq を使う。
o MMXレジスタは浮動小数点レジスタの流用なので、MMX命令利用後には、emms
命令によってプロセッサを浮動小数点状態に戻す必要がある。
o 表中の各々のデータ型向けに、例えば足し算であれば順に paddb、paddw、paddd といった命令が用意されている。
以下は、Packed words同士の引き算の例である。
入力部分でアドレスを使っているので、asm文中ではメモリを (%1) などとして使用している。
/*実行結果
asm_mmx_psubsw_test.c
*/
#include <stdio.h>int main ()
{
short int a[4] = {1111,1112,1113,1114};
short int b[4] = {1111,2112,3113,4114};
short int c[4];/*Word 4要素のベクトル同士の符号付き引き算 a - b*/
/*
入力値には配列 a,b のアドレス &a,&b を使い、そのアドレスにある
メモリ内容を movq で %mm0,%mm1に移動する。
引き算の結果は出力用の配列 c に割り当てたレジスタ %0 に movq で
移動する。
*/
asm volatile ("
movq (%1),%%mm0
movq (%2),%%mm1
psubsw %%mm1,%%mm0
movq %%mm0,%0
emms
" : "=g" (c) : "r" (&a), "r" (&b));printf ("%d,%d,%d,%d\n", c[0], c[1], c[2], c[3]);
return 0;
}
■23:3D
Now!を使った例(除算推定値)■
3D Now!では、MMXレジスタを利用して単精度浮動小数点(float)のデータを二つ同時に演算可能である。
o emms よりも高速な femms 命令によってプロセッサを浮動小数点状態に戻せる。
以下は、3d Now!を使った高速で低精度の割り算の例である。
/*実行結果
asm_3dnow_pfrcp_test.c
*/
#include <stdio.h>int main ()
{
float a[2] = {1.0, 2.0};
float b = 4.0;
float c[2];
/* a/b の低精度推定値を計算する。(正確な結果はc={0.25,0.5})*//*
一つ目の入力値には配列 a のアドレス &a を使い、そのアドレスにある
メモリ内容を movq で %mm0 に移動する。二つ目の入力値は変数 b を使
い、movd で %mm1 に移動する。
まず %mm1 の逆数をとり、それを %mm0 に掛ける。
結果は出力用の配列 c に割り当てたレジスタ %0 に movq で移動する。
*/
asm volatile ("
movq (%1),%%mm0
movd %2,%%mm1
pfrcp %%mm1,%%mm1
pfmul %%mm1,%%mm0
movq %%mm0,%0
femms
" : "=g" (c) : "r" (&a), "r" (b));printf ("c={%g,%g}\n", c[0], c[1]);
return 0;
}
低精度であることは否めないが、ともかく高速である。精度も画像処理には十分で ある。
homeへ戻る
-
gccでx86インラインアセンブリ(インラインアセンブラ)を使う際の留意点 - s-akira (1999-2006) -