DelphiでMMX・x86インラインアセンブラ


 インラインアセンブラとは?


高級言語(C言語・PASCAL等)内に、低級言語であるアセンブラを織り交ぜて書くことです。
高級言語では、細かい部分まで計算をすることが難しいので、CPUに特化した最適を行いたい場合は、アセンブラを使わなければなりません。
しかし、高級言語で開発を行うほうがずっと作りやすいので、高級言語内にアセンブラを取り込んでしまえば良いという発想で作られています。

高級言語では、たとえば、MMXのような特殊な命令は使えません。
また、同時に二つの計算を行うなどのようなSIMD計算を行うのが非常に面倒です。
そこで、インラインアセンブラを使って計算を行わせて速度の向上を図るわけです。

ここでは、実数計算は一切扱いません。整数計算のみの解説を行います。

 Delphiでx86インラインアセンブラを使うには?

プログラムのコード内に以下の記述をするとアセンブラを書くことが出来ます。
asm
 //アセンブラコード
end;

CPU内部には、レジスタというメモリーに相当するデータを保管する記憶部分が存在します。アセンブラでは、この記憶部分をダイレクトに操作して、計算を行います。

 データタイプ


変数には、Integer型やByte型なのがあります。同じ変数なのになんでこんなにたくさんあるのか?という疑問があるはずです。
その答えは、「データタイプ」です。

データサイズ 呼称 参考例
(符号アリ=負)
参考例
(符号なし=自然数)
符号あり範囲 符号なし範囲
8bit バイト
(Byte)
ShortInt Byte -127〜127 0〜255
16bit ワード
(Word)
SmallInt Word -32767〜
32767
0〜65535
32bit ダブルワード
(DoubleWord)
Integer,LongInt LongWord,DWORD,
Cardinal
-2^31〜2^31 0〜2^32
64bit クアドワード
(QuadWORD)
Comp,Int64 Extended -2^63〜2^63 0〜2^64
128bit ダブルクアドワード
(DoubleQuadWORD
なし なし -2^127〜
2^127
0〜2^128

レジスタとこのデータタイプは切っても切れない関係なのです。

 レジスタ

レジスタ一覧
eax〜edx
構造
32bit
16bit(下位)
× 8bit(上位) 8bit(下位)
EAX eax
AX ax
AH/AL ah al

EAXという32bitのレジスタの中には、下位16bitをax,さらにその16bit内に含まれる8ビットのレジスタをah,alと呼びます。

EAXのような構造をもつものは、EBX,ECX,EDXと合計4つあります。
これに加えて、32ビット専用レジスタがESI,EDIとあります。

レジスタ(32bit) 内部に持つレジスタ(16bit) さらに内部に持つレジスタ(8bit)
EAX ax ah,al
EBX bx bh,bl
ECX cx ch,cl
EDX dx dh,dl
ESI SI
EDI DI
EBP※ BP
ESP※ SP
※(通常はESP,EBPレジスタは使いません)


インラインアセンブラでは、合計6個の変数を同時に使用することが出来ます。
逆にいうと、6個しか同時に使えません。資源は少ないので大切に使いましょう。


さて、簡単に言うと、EAXという構造体を考えて、
type TEAX=record
 dammy:WORD;
 ax:TAX;
end;

type TAX=record
 ah:Byte;
 al:Byte;
end;
というような形です。あくまで例です。

EAX=Dammy(16bit)+AX(16Bit)
AX=AH(8bit)+AL(8Bit)
という形となります。

つまり、EAXを変数で使っているときは、AXは下位16ビットのそのEAXの値となってしまいます。
この辺が普通の変数と違う部分ですので気をつけてください。

 レジスタの構造と特徴の例

ちなみに、ALを8ビット分掛ける(左シフト8、2の8乗を掛ける事)と、ALはAHとなります。
そして、AHに128,ALに64と置いて、AXを2で割ると、AHは64,ALはALは32となります。
インラインアセンブラの例では、
var iah,ial:byte;

iah:=128;
ial:=64;

asm
 mov al,ial;
 mov ah,iah;
 shr ax,1;//右シフト1は、割り算の2に相当。速いのでこちらを使用。
 mov ial,al;
 mov iah,ah;

end;

実はコレが、MMXやSSEの高速化手法と同じになります。2個同時に計算させてしまって、2倍の速度を得るわけです。この例では、計算データは8ビットですが、SSEでは浮動小数点のデータを2つを、MMXでは32ビットのデータを二個同時に計算できます。SSE2だと、64ビットデータを2つ(32ビットデータを4つ)計算できるってことになりますかね。単純にそうは行かないとは思いますが。

Win32のアセンブラには、以下のようなコマンドがあります。


 符号

符号とは、マイナス、プラスのことである。
符号なしとは、マイナスを考慮しない(自然数)
符号アリとは、マイナスを考慮する(整数)
ということになります。

符号なしの方が、速いです。そりゃあマイナスを考えていたら考える分だけ遅くなります。

 x86アセンブラ命令表(インラインアセンブラ用途に向かないものは排除済み)

コピー系
mov 転送先,転送元 レジスタ同士、レジスタ変数同士の値をコピーします。型サイズが同じじゃないとコピーできないので注意
(byte=8bit=ah,WORD=16bit=ax,Integer=32bit=eax)
movzx 転送先,転送元 転送先が転送元よりサイズが小さいとき、ゼロで埋めてコピー。
xchg 転送先,転送元 スワップ(交換)。レジスタ同士の値を交換します。
算術系
add 処理先,値 符号無し 足し算
sub 処理先,値 符号無し 引き算。
div 処理先,値 符号無し 割り算。クソ遅いです。
mul 処理先,値 符号無し 掛け算。
adc 処理先,値 符号あり 足し算
sbb 処理先,値 符号あり 引き算
idiv 処理先,値 符号あり 割り算。クソ遅いです。
imul 処理先,値 符号あり 掛け算
inc 処理先 1加える。インクリメント。
dec 処理先 1減らす。デクリメント。
neg 処理先 符号反転。C=-A。A=29のとき、C=-29
シフト系(シフト=ずらす、つまりビットをずらすこと。)
shr 処理先,値 符号なし 右シフト、2のn乗分割る。超はやい。
shl 処理先,値 符号なし 左シフト、2のn乗分掛ける。超はやい。
sar 処理先,値 符号あり 右シフト、2のn乗分割る。超はやい。
sal 処理先,値 符号あり 左シフト、2のn乗分掛ける。超はやい。
ローテーション
ror 処理先,値 符号なし 右シフト
rol 処理先,値 符号なし 左シフト
rcr 処理先,値 符号あり 右シフト
rcl 処理先,値 符号あり 左シフト
論理演算(論理とは、TrueとFalseから考えること。)
and 処理先,値 論理積。0011 and 1100=0000。
or 処理先,値 論理和。0011 or 1100=1111。
xor 処理先,値 排他的論理和。0011 xor 1100=1111
not 処理先 否定とか反転。 not 0011=1100
サブルーチン
call サブルーチンラベル名 サブルーチンを呼び出す。
ret サブルーチンを終える。
比較命令
cmp A,B レジスタA,B同士の比較。分岐命令の前に使用。
test AND積。
擬似命令
nop 何もしない。
ジャンプ・分岐命令(引数は全てジャンプ先だけ)
jmp 無条件でジャンプ
loop ecx!=0のときジャンプ先にジャンプ。
loope ecx==0のときジャンプ先にジャンプ。
loopne ecx!=0のときジャンプ先にジャンプ。
jne
jnz
cmp結果が<>のときジャンプ先にジャンプ。
jz
je
cmp結果が==のときジャンプ先にジャンプ。
jb
jnae
jc
符号なし cmp結果が<のときジャンプ先にジャンプ。
ja
jnbe
符号なし cmp結果が>のときジャンプ先にジャンプ。
jbe
jna
符号なし cmp結果が<=のときジャンプ先にジャンプ。
jae
jnb
jnc
符号なし cmp結果が>=のときジャンプ先にジャンプ。
jl
jnge
符号あり cmp結果が<のときジャンプ先にジャンプ。
jg
jnle
符号あり cmp結果が>のときジャンプ先にジャンプ。
jle
jng
符号あり cmp結果が<=のときジャンプ先にジャンプ。
jge
jnl
符号あり cmp結果が>=のときジャンプ先にジャンプ。
jcxz cxレジスタが==のときジャンプ
jecxz ecxレジスタが==のときジャンプ
レジスタ退避・スタック
push レジスタ 退避させたりスタックしたりします。使うつもりのレジスタは、インラインアセンブラを使う前にこの命令を使って退避させてください。
pop レジスタ 退避させたレジスタを復活させます。この命令は、退避格納した逆順に使用してください。
レジスタ退避の例。
asm
 //eax,ebx,esiを計算で使うから退避させておく。
 push eax;
 push ebx;
 push esi;
 
 //色々な計算〜。
 
 //退避させたレジスタを復活
 pop esi;
 pop ebx;
 pop eax;
end;

 なぜ、退避させるのか?

それは、実行中のソフトウェアが既にレジスタを使用している恐れがあるからです。Pascalで書かれた部分は、Pascalコンパイラがアセンブラコードを生成しますが、インラインアセンブラ部分は一切コンパイラは手を出しません。よって、レジスタの管理や退避を自前でやらなければいけないのです。

 スタックとは?

レジスタ用のデータ保管庫で片方通行止めのものです。

まず、Aというデータをpush(入れる)します。
次に、C、Dと入れていきます。
ここで、Aを取りたいときは、一旦、DとCをpop(取り出す)しなければなりません。右の黒い部分は行き止まりです。

このような特性を持つデータ保管庫が、スタックです。
順番が決まってるので、面倒なのが欠点ですがこれがコンピュータにとっては分かりやすいんでしょう。たぶん。

関数の呼び出し(callとか)で、pushを使って引数指定をすることが可能です。スタックの何個目まで呼び出し、等です。push何回やろうが、順番さえ守れば他のスタックに保管しているデータには影響はありません。pushしたらpopしてやればいいのです。
数少ないレジスタで使うデータの置き場所に困ったら、スタックにぶち込んでやればオッケーです。そして、必要なときにまた取り出せば良いのです。 

 論理の考え

大学の情報処理Iで勉強する、論理回路の中の基本、論理式について完璧に覚えておきましょう。

A and B=C AもBも正しいとき、Cは正しい。
AかBどちらかでも正しくないとき、Cは正しくない。
A or B=C AかBが正しいとき、Cは正しい。
AもBも正しくないとき、Cは正しくない。
not A=C Aが正しくないとき、Cは正しい。
Aが正しいとき、Cは正しくない。
A xor B=C Aが正しく、Bが正しくないとき、Cは正しい。
Aが正しくなく、Bが正しいとき、Cは正しい。
AもBも正しい場合、Cは正しくない。
AもBも正しくない場合、Cは正しくない。

以上を、表にすると

■論理積
A and B A=0(False) A=1(True)
B=0(False) 0(False) 0(False)
B=1(True) 0(False) 1(True)

■論理和
A or B A=0(False) A=1(True)
B=0(False) 0(False) 0(True)
B=1(True) 1(True) 1(True)

■否定・反転
not A A=0(False) A=1(True)
C 1(True) 0(False)

■排他的論理和
A xor B A=0(False) A=1(True)
B=0(False) 0(False) 1(True)
B=1(True) 1(True) 0(False)

 論理のインラインアセンブラでの応用


色データは、1ピクセル当たり24ビットデータで構成されます。そのデータをAとすると、十六進数で、
A=0xBBGGRR
となります。(ファイルでの並びはRRGGBBとなりますが、数値的に24ビットであらわすと逆になります)
各RR,GG,BBは0x00-0xFF(0-255)までの8ビットの値です。それが3個で、24ビットとなります。

ここで、Aから、GGの値だけを取り出して、32ビットの変数Cにコピーしたい場合、どうすればいいでしょうか?

まず、GGを右にずらさなければなりません。ずらす個数は、RRの大きさの8ビットです。
例:shr A,8;
となります。割り算でも可能で、
例:div A,256;
となります。割り算する意味は無いので、右シフト命令を使います。

ここで、一時保管32ビット変数Bにシフト結果を代入します。
B=A shr 8;
Bの内部は、
B=0x0000BBGG
となります。

ここで論理演算を使用します。論理積のandを使いましょう。
これは、二つの論理対象が共に1でないと成り立ちません。つまり、1以外の値を与えてやれば消えるわけです。
ですので、GGの部分をマスクする0x000000FFというものをandで与えてやればいいのです。
C=B and 0x000000FF;

これで、Cに目的の値が代入されました。

インラインアセンブラで書きますと、
asm
 push eax;//退避
 mov eax,A;//eax=A
 shr eax,8;//eax=eax shr 8
 and eax,$000000FF;// eax=eax and $000000FF
 mov C,eax;//C=eax

 pop eax;//復活
end;
となります。

 ジャンプとラベル


インラインアセンブラ内には、ラベルを書くことが出来ます。
このラベルは、ジャンプ先として使用します。
@ラベル名:

1〜10まで足していくΣkのアセンブラプログラムを書いて見ましょう。

まず、ループ用カウンタとしてeaxを使います。ecxがループ用として使われる場合が多いのですが、とりあえず何でも良いです。
eaxには、ループ回数の10を代入しておきます。合計はebx,現在の値はedxとします。

一応、PASCALとアセンブラの両方でこれを書いてみましょう。
パスカル
edx:=1;
ebx:=0;
for eax:=0 to 9 do begin
  ebx:=ebx+edx;
  edx:=edx+1;
end;
インラインアセンブラ
var i,j,k:Integer;

i:=9;ループ回数
j:=0;//合計
k:=1;//現在の値初期値

asm
 push eax;push ebx;push ecx;push edx;//退避
 mov edx,k;//edx=k
 mov eax,i;//eax=i
 mov ebx j;//ebx=j

 @loop1:
  add ebx,edx;//ebx=ebx+edx
  inc edx;//edx=edx+1
  dec eax;//eax=eax-1
  cmp eax,0;// eax?0?
  je loop1;//if eax==0 then goto loop1

 
 pop edx;pop ecx;pop ebx;pop eax;//復活
end;

インラインアセンブラで、わざとi,jkに置いているのは、オペランドのサイズ不一致エラーを防ぐためです。数字だけでは、アセンブラはその数字がどのくらいのサイズの変数か判別できません。そこで、32ビット変数に一旦代入させてmov命令でコピーしています。ちなみに、ecxをカウンタとして扱うと、cmpとjeをloopという命令一つにまとめることが出来ます。


 ラベルとサブルーチン

@ラベル名:
〜〜
ret
で囲んだ区間を、サブルーチンとして扱うことが可能です。呼び出すには、call命令を使用します。

var i,j,k:Integer;

i:=9;ループ回数
j:=0;//合計
k:=1;//現在の値初期値

asm
 push eax;push ebx;push ecx;push edx;//退避
 mov edx,k;//edx=k
 mov eax,i;//eax=i
 mov ebx j;//ebx=j

 @loop1:
  call subroutine1;
  cmp eax,0;// eax?0?
  je loop1;//if eax==0 then goto loop1
 jmp asmend;//goto asmend
 
 @subroutine1:
  add ebx,edx;//ebx=ebx+edx
  inc edx;//edx=edx+1
  dec eax;//eax=eax-1
  ret;


 @asmend:
 pop edx;pop ecx;pop ebx;pop eax;//復活
end;


 インラインアセンブラで画像処理

procedure MonoYUV(BMP:TBitmap);
var
 x,y,i,a,w :Integer; //ビットマップのX軸、Y軸
 PBit :PByteArray;
begin

//画像BMPは24ビット形式であるとします。

w:=(BMP.Height-1)*3;

 for y := 0 to BMP.Height-1 do begin
  pBit:=BMP.ScanLine[y];

  asm
   push eax;push ebx;push ecx;push edx;
   
   //値の初期化
   mov eax,pBit;//eax:=pBit
   mov ecx,w;//ecx:=w

   //計算処理ループ
   @loop1:
     //読込部分
     xor edx,edx;//edx:=0
    movzx bl,byte[eax];//ebx=pBit[w-ecx]
    add edx,ebx;
    movzx bl,byte[eax+1];//ebx=pBit[w-ecx+1]
    add edx,ebx;

    movzx bl,byte[eax+2];//ebx=pBit[w-ecx+2]
    add edx,ebx;


    div edx,3;//平均値を出す edx/=3

    //代入部分
    
mov eax,dl;//pbit[w-ecx]:=byte(edx);
    inc eax;//ポインタを進める
    mov eax,dl;//pbit[w-ecx+1]:=byte(edx);
    inc eax;//ポインタを進める
    mov eax,dl;//pbit[w-ecx+2]:=byte(edx);
    inc eax;//ポインタを進める

    //分岐
    
loop @loop1;//if ecx<>0 then goto @loop1:
   pop edx;pop ecx;pop ebx;pop eax;
  end;
  //次のY行へ。以下高さ分繰り返す。
 end;
end;

TBItmap画像をインラインアセンブラで画像を白黒に変換しています。
最適化も何もしていないので遅いですので、各自高速化を勉強して早くしてみましょう。

32ビット一気に読み出しや、シフト、代入の1手順化等色々テクニックがあります。
一番遅い原因は、divの部分なんで、これをシフトとかで代用する技を考えると速度が数倍上がります。
たとえば、3で割る方法ではなく、Y,U,Vの比率を8ビットシフト計算可能なように変形するとか・・・。

 MMXやSSEとは?

CPUの拡張命令で、マルチメディア用途に良く使われます。
これらは、データを一度に処理できるような命令と大きなサイズのレジスタが用意されており、処理速度を高速化することが可能なように作られています。

得意とするのは、四則・論理演算を何個も一度処理して何度も繰り返す事です。比較構文などの処理の用途には向きません。MPEG処理や、グラフィックス処理、行列的な処理に向きます。

 MMXのレジスタの種類と特徴


MMXには、データをためておくためのレジスタが8つ用意されています。
レジスタ名:mm0〜mm7
全て64ビットまで値を保持可能です。整数のみ対応。

MMXレジスタに値をコピーする命令は、movd(32ビットコピー)とmovq(64ビットコピー)があります。
movqは、ポインタから呼び出すのが主です。(64ビットの変数はあまり使うことはないでしょう)


 算術命令の特徴


普通に計算するのですが、MMXはSIMDです。レジスタ一つに、最高でも4個のデータがビットを挟んで置かれていることになります。
ですので、ビットの位置や大きさに注意しながら計算をしなければなりません。
(境界を16bit,32bitの違いによって、計算可能な最大の大きさが異なります)

レジスタ 64bit
16bit 16bit 16bit 16bit
mm0 A B G R
+(加算)
mm1 a b g r
=
代入先 A+a B+b G+g R+r

ビットの持てる大きさを越えた時、その状態を飽和と呼びます。飽和、不飽和とはその状態をどうするかという処理を指定するものです。

 変数の仕組み


変数は通常、円のようになっています。
たとえば、符号付Byteの場合、-127〜127のようになります。ここで、128になった瞬間、この型の変数は自動的に-127になってしまいます。これが通常の変数です。
形無しの場合は、0〜255となり、255を越えた瞬間、変数は0を示すことになります。通常はオーバーフロー(桁あふれ)エラーが発生します。この値が変わってしまうことをラップアラウンドと呼びます。

しかし、この越えた瞬間(飽和)を考慮した場合、処理速度が低下していしまいます。そこで、MMXではこのときになった場合のための演算命令が用意されています。

 飽和演算

種類 最大値を越えたとき 最小値を下回った場合
不飽和(ラップアラウンド) 最小値になります。 最大値になります。
飽和符号アリ 自動で最大値を代入します。 自動で最小値を代入します。
飽和符号なし 自動で最大値を代入します。 自動で最小値を代入します。

以上のような処理が行われます。目的に応じて使い分けてください。

 パック演算・アンパック演算


アンパックとは、位置を大きさを考慮して変えて交互に混ぜる事です。

mm0 0 0 0 0 A B G R
mm1 0 0 0 0 a b g r

これを、
punpcklbw mm0,mm1
としますと、
mm0 a A b B g G r R

となります。mm0がWORDの下部に展開されてmm1がWORDの上部に展開されます。
例:αブレンディング等では、mm1をゼロにして、mm0のワード展開のみを行います。

パックは、この逆で、
mm0 0 A 0 B 0 C 0 D
mm1 0 a 0 b 0 c 0 d
が、
mm0 a b c d A B C D
となります。

0の部分は、ナニを置いても無視されます。


以上から、SIMD計算を行うには、パック演算は欠かせない機能となります。

 MMXの命令リスト

コピー命令
movd 処理先処理元 32ビット同士のコピー
movq 処理先処理元 64ビット同士のコピー
算術命令(x=b(8bit),w(16bit),d(32bit)
paddx 処理先,値 不飽和 加算命令。
psubx 処理先,値 不飽和 加算命令。
paddsx 処理先,値 符号あり飽和 加算命令。
psubsx 処理先,値 符号あり飽和 加算命令。
paddusx 処理先,値 符号なし飽和 加算命令。
psubusx 処理先,値 符号なし飽和 加算命令。
pmulhx 処理先,値 上位 乗算命令。
pmullx 処理先,値 下位 乗算命令。
pmaddwd 乗加算命令。
論理演算
pand 論理積
pandn 論理積の後反転
por 論理和
pxor 排他的論理和
シフト(x=w(16bit),d(32bit),q(64bit)
psllx 符号なし 論理左シフト
psrlx 符号なし 論理右シフト
psrax 符号あり 算術右シフト
パック演算(オペランドサイズ変換命令)
packssxx 処理先,処理元 符号あり パック
xx=wb,dw
packusxx 処理先,処理元 符号なし
punpckhxx 処理先,処理元 上位 アンパック
xx=bw,wd,dq
punpcklxx 処理先,処理元 下位
比較(x=b(8bit),w(16bit),d(32bit)
cmpeqx
cmpgtx
終了
emms MMXアセンブラを終了



 参考文献・サイト


・TurboAssembler入門
・Intel IA-32アーキテクチャマニュアル(英語)
プログラマの隠れ里




Copyright(C)'2003- buin2gou
無断でこのページを転載・複写・掲載することを禁じます。