[asm編]アセンブラ入門  2005. 5.10 (01版)
本節では、アセンブラを使ってCPU命令レベルでプログラミングする場合について解説していきたいと思います。
MMX命令を中心に解説する予定ですが、C++の章とは異なり全くCPU命令を知らない人を読者対象とし、
C++でプログラミング出来る人ならば、同様にアセンブラプログラミングが出来るようになることを目標としています。

進め方としては、とりあえずC++言語との連携から、MMX命令へと解説していき、
その後(気力が続けば)基礎編へと解説していきたいと思っています。

アセンブラの参考文献としては、Intelの日本語技術資料は入手しておいた方がよいでしょう。

目次へジャンプ。
[asm編] MMX, SSE, SSE2, 3D Now!のサポートCPUの判定  2005. 5.10 (01版)
現在のCPUの普及状況では、MMX命令はもはや普通に使用してもよいとは思いますが、CPUがサポートしているかどうか チェックするにはどうしたら良いかを説明します。
自分でチェックしても良いのですが、このための関数がavisynthでは用意されているのでこれを利用するとよいでしょう。
	if(!(env->GetCPUFlags() & CPUF_INTEGER_SSE)) {
		//--- SSE: pentium III/4
		
	} else if(!(env->GetCPUFlags() & CPUF_MMX)) {
		//--- MMX: pentium II
		
	} else {
		//--- no MMX
	};


----------- avisynth.h (抜粋) ----------------------------------------------

// For GetCPUFlags.  These are backwards-compatible with those in VirtualDub.
enum {                    
                    /* slowest CPU to support extension */
  CPUF_FORCE			  = 0x01,   // N/A
  CPUF_FPU			    = 0x02,   // 386/486DX
  CPUF_MMX			    = 0x04,   // P55C, K6, PII
  CPUF_INTEGER_SSE	= 0x08,		// PIII, Athlon
  CPUF_SSE			    = 0x10,		// PIII, Athlon XP/MP
  CPUF_SSE2			    = 0x20,		// PIV, Hammer
  CPUF_3DNOW			  = 0x40,   // K6-2
  CPUF_3DNOW_EXT		= 0x80,		// Athlon
  CPUF_X86_64       = 0xA0,   // Hammer (note: equiv. to 3DNow + SSE2, which only Hammer
                              //         will have anyway)
};

class IScriptEnvironment {
public:

  virtual /*static*/ long __stdcall GetCPUFlags() = 0;

------------------------------------------------------------------------------
目次へジャンプ。
[asm編]インラインアセンブラを使ってみる(1) C++データへのアクセス  2005. 5.10 (01版)
高速化を目指しているとどうしてもアセンブラを避けて通れなくなってくることがあります。
複雑なプログラムではアセンブラなんか使うよりもロジックを見直した方がよっぽど効果がありますが
画像処理では、簡単なロジックを単に(順序性の低い)膨大なデータに適用するだけということが多く、こういう場合
SIMD演算が有効なのでしようがありません。
でも最近のコンパイラの最適化は目を見張るものがあり、無駄な努力となる場合もあるので、
処理のどの部分に時間が掛かっていてアセンブラ化して本当に効果がありそうかどうかをまず考えてみて、
C++と連携させて特定部分のみをアセンブラ化するのが良いでしょう。
(まあ無駄なことをやってみることもけっこう勉強にはなるわけなんですが...)

ところでインラインアセンブラとは、C++のコンパイラが含有するマクロ命令で、C++のコードのなかで
アセンブリ命令を直接記述できるものでCPU命令レベルで細かく指示できます。

実際にどのように記述するのかと言うと、
__asm  mov  eax, 100
__asm  mov  ebx, 200
__asm  mov  ecx, 300

または、
__asm {
       mov  eax, 100
       mov  ebx, 200
       mov  ecx, 300
}
と書きます。一行で
__asm mov eax,100 __asm mov ebx,200 __asm mov ecx,300
と記述してもかまいません。なお__asmブロック中は大文字小文字の区別はありません。

では実際にやってみましょう。
データアクセスの基礎(1) [YUY2編]の【処理5】をアセンブラ化 してみましょう。
【処理5】は以下のようになっていましたね。
PVideoFrame __stdcall Sample::GetFrame(int n, IScriptEnvironment *env) {
    //*** in-place filtering ***
    PVideoFrame src = child->GetFrame(n, env);
    env->MakeWritable(&src);
    const int       pitch   = src->GetPitch();
    const int       rowsize = (src->GetRowSize() + 4) & -8;
    const int       height  = src->GetHeight();
    const int       modulo  = pitch - src->GetRowSize();
    const __int64  *srcp    = (__int64*)src->GetReadPtr();
    __int64        *dstp    = (__int64*)src->GetWritePtr();
    
    for (int j=0; j<height; j++) {
        for (int i=0; i<rowsize>>3; i++) {
            *dstp++ = (*dstp & 0x00ff00ff00ff00ffi64) | 0x8000800080008000i64;
        }
        dstp += (modulo>>3);
    }
    return src;
}
ここで、二重ループの中のメイン処理のみをまずMMX化してみましょう。
ただし、この処理はmmx化することで逆に遅くなりますが。
PVideoFrame __stdcall Sample::GetFrame(int n, IScriptEnvironment* env) {
//*** in-place filtering ***
    PVideoFrame src = child->GetFrame(n, env);
    env->MakeWritable(&src);
    const int       pitch   = src->GetPitch();
    const int       rowsize = (src->GetRowSize() + 4) & -8;
    const int       height  = src->GetHeight();
    const int       modulo  = pitch - src->GetRowSize();
    const __int64  *srcp    = (__int64*)src->GetReadPtr();
    __int64        *dstp    = (__int64*)src->GetWritePtr();
    static const __int64  MASK_UV    = 0x00ff00ff00ff00ffi64;
    static const __int64  SET_UV_128 = 0x8000800080008000i64;
    
    for (int j=0; j<height; j++) {
        for (int i=0; i<rowsize>>3; i++) {
            __asm {
                    mov    eax, dstp          ;このようにC++の変数(dstp)を参照することができます。
                    movq   mm0, [eax]         ;
                    pand   mm0, MASK_UV       ;データ定義は_asmブロックの外でしましょう。
                    por    mm0, SET_UV_128
                    movq   [eax], mm0
            }
            dstp++;
        }
        dstp += (modulo>>3);
    }
    __asm { emms }                            //MMXレジスタはFPUレジスタと共有しているため、
                                              //  MMX使用後は必ずFPUモードに戻す必要があります。
                                              //このemms命令はコストが高いので出来るだけ少ない方が望ましい
    return src;
}

この例のようにC++で定義している変数にアクセスできます。

mov eax, dstpは、mov eax, [dstp] 又は mov eax, dword ptr [dstp] とも書けます。
この点が最初はちょっと混乱する部分です。
dstp と [dstp] じゃ違うっぽい気がしますよね。C言語風に言えば、dstp と *dstp に相当するんじゃないの?って。
でも両者とも同じで、dstpで指されたメモリ領域の内容を意味します。

配列の場合、mov array[6 * type int], 0 TYPE演算子でスケールを指定してください。

また(静的な)変数のアドレスを求める場合はOFFSET演算子を使用します。
(動的な)変数のアドレスの場合はLEA命令を使ってください。
static int var_a = 0;
int var_b = 1;
int array_b[] = { 1, 2, 3, 4 };
struct {
   int  member_a;
   int  member_b;
} struct_C = { 10, 20 };

__asm {
    mov  eax, var_a                     ; eax = 0
    mov  eax, [var_a]                   ; 同上
    mov  eax, OFFSET var_a              ; eax = var_a のアドレス。C言語で言うと &var_a のことです
    lea  eax, var_b                     ; eax = var_b のアドレス(動的な変数の場合)lea eax, _var_b$[ebp]
    mov  eax, array_b[3 * TYPE int]     ; eax = array_b[3] = 4
    mov  eax, [array_b + 3 * TYPE int]  ; 同上
    mov  eax, struct_C.member_b         ; eax = 20
}
目次へジャンプ。
[asm編]インラインアセンブラを使ってみる(2) レジスタの使用と保護   2005. 5.10 (01版)
次にループ処理も一緒にアセンブラ化してみましょう。
【処理6】
PVideoFrame __stdcall Sample::GetFrame(int n, IScriptEnvironment* env) {
//*** in-place filtering ***
    PVideoFrame src = child->GetFrame(n, env);
    env->MakeWritable(&src);
    const int       pitch   = src->GetPitch();
    const int       rowsize = (src->GetRowSize() + 4) & -8;
    const int       height  = src->GetHeight();
    const int       modulo  = pitch - src->GetRowSize();
    const __int64  *srcp    = (__int64*)src->GetReadPtr();
    __int64        *dstp    = (__int64*)src->GetWritePtr();
    static const __int64  MASK_UV     = 0x00ff00ff00ff00ffi64;
    static const __int64  SET_UV_128  = 0x8000800080008000i64;
    const  int     rowsize_by_16 = rowsize>>4;
    
    __asm {
                push    esi
                push    ebx
                push    ecx
                movq    mm6, MASK_UV
                movq    mm7, SET_UV_128
                mov     esi, dstp
                mov     ebx, height
                mov     ecx, rowsize_by_16
              align 16
              LOOP_Y:                           ; ジャンプ等のために、ラベルも使用できます。
              LOOP_X:                           ;
                movq    mm0, [esi]              ; mm0とmm1に8バイトずつ計16バイトのデータを読み込む
                movq    mm1, [esi+8]
                add     esi, 16                 ; 次データの読み込みのためにソースポインタを+16する。
                pand    mm0, mm6                ; データmm0とmm1に交互に
                pand    mm1, mm6                ;   mmx & 0x00ff00ff00ff00ff | 0x8000800080008000
                por     mm0, mm7                ; を行う
                por     mm1, mm7
                movntq  [esi-16],mm0            ; 結果を格納する
                movntq  [esi- 8],mm1
                loopnz  LOOP_X                  ; LOOP_X
                add     esi, modulo             ; ライン末尾のパディングデータをスキップする
                dec     ebx
                mov     ecx, rowsize_by_16      ; 次のループのためにXループカウンタを再セット
                jnz     LOOP_Y                  ; 高さ(ライン数)数分繰り返す
                sfence
                emms
                pop     ecx
                pop     ebx
                pop     esi
    }
    return src;
}

さて、ここで注目して欲しいことは汎用レジスタ(EBX, ECX, ESI)は使用前後でスタックに退避・復元している点です。
でもMMXレジスタや最初の例のEAXレジスタは破壊したままにしていますね。
これはC++コンパイラが使用するレジスタとの関係で破壊してはいけないレジスタもあるからです。
Visual C++では、EAX,ECX,EDXレジスタであれば破壊しても構わないことになっています。※
私は、間違いの無いように使用した汎用レジスタはEAXを除いて全て保護するようにしています。

※ もっと正確には、EAX、EBX、ECX、EDX、ESI、EDIレジスタを保存する必要はないけれども、
これらを使用するとC++コンパイラのレジスタアロケータがASMブロックの前後でレジスタに値を
格納するのにそれらを使用できなくなるのでクオリティに影響します。
またEBX、ESI、EDIを使用することによって、コンパイラが関数のプロローグとエピローグに
それらのレジスタを保存して、回復するコードを追加します。
またif文の場合、注意が必要でSTDやCLDでフラグを元に戻さないといけない場合もあります。
なお、関数の呼び出し規約で、__cdecl, __stdcallではなく__fastcallを使用した場合には、
ECXも破壊してはいけません。

目次へジャンプ。
[asm編]インラインアセンブラを使ってみる(3) ジャンプラベル   2005. 5.10 (01版)
前章の例中で、__asm中でループのためにLOOP_X, LOOP_Y というジャンプラベルを使用していますが、
飛び先は別に__asmブロック中でなくてC++文中でも飛ぶことができます。
(ただしスタックレベルやレジスタの復元の注意は必要です)
void func( void )
{
   goto C_Dest;  /* Legal: correct case   */
   goto c_dest;  /* Error: incorrect case */

   goto A_Dest;  /* Legal: correct case   */
   goto a_dest;  /* Legal: incorrect case */

   __asm
   {
      jmp C_Dest ; Legal: correct case
      jmp c_dest ; Legal: incorrect case

      jmp A_Dest ; Legal: correct case
      jmp a_dest ; Legal: incorrect case

      a_dest:    ; __asm label
   }

   C_Dest:       /* C label */ 
   return;
}
int main()
{
}

目次へジャンプ。
[asm編]関数呼び出し規約とスタック  2005. 5.10 (01版)
(1) スタック(Stack)
プログラミングする上での重要な概念です。
特にアセンブラを使用する場合、常に念頭において置かなければなりません。
スタックとは、積み重ねたものを意味する言葉で、LIFO(Last-IN, First-Out)「後入れ先出し」のデータ構造 をした一時記憶領域のことを指します。※
筒状の入れ物に物を入れたり本を積み上げりした場合を思い浮かべてください。
つまり入り口が一つでそれが出口にもなっているようなデータ構造をしており、最後に入れたものを最初に取り出す ことができます(最後に積み上げた本が最初に持ち出すことができます)。
Stack
と言っても、何も特別なメモリではなくて主メモリの空いた部分が使われます。
通常はアプリ空間の空きメモリの後ろの方から前の方へ向かって逆方向に積み入れられます。
これは、ローカル変数や作業結果の一時保存避難所、関数へ受け渡すパラメタや戻りアドレス等に使われます。
現実の実装としては、スタックに積むのはpush、出すのはpop命令となります。
スタックは普通のメモリなので、積まれたデータは、push, pop以外でも通常のメモリアクセス命令(mov等)でも アクセスできます。

※ちなみに、FIFO(First-In,First-Out)「先入れ先だし」のデータ構造のことは「キュー」と言います。

(2) 呼び出し規約(Calling Conventions)
Visual C++には、いくつかの関数呼び出し規約があります。
C/C++言語ではあまり意識しなくても勝手に制御してくれていましたが、アセンブラでは自分で規約に合わせて
制御しなければいけません。

keyword __cdecl __stdcall __fastcall
C++のデフォルトの規約です
fuction labeling 関数名の先頭に'_'(アンダースコア)が付けられます 関数名の先頭に'_'(アンダースコア)が付けられます。
末尾には'@'に続いて引数サイズが10進で付けられます
関数名の先頭に'@'が付けられます。
末尾には'@'に続いて引数サイズが10進で付けられます
Stack cleanup Caller
引数渡しスタックは呼び出した側でクリアします
Callee
引数渡しスタックは呼び出された側でクリアします
Callee
引数渡しスタックは呼び出された側でクリアします
Parameter passing 逆順(右から左)にスタックにプッシュされます 逆順(右から左)にスタックにプッシュされます 32bit以下の2つまではレジスタ(ecx,edx)に、それからはスタックにプッシュされます
Retern address パラメタをプッシュ後にcallされるので、戻りアドレスは一番最後にプッシュされます。
つまり、スタックポインタは戻りアドレスを指しています。
Retern value 復帰値はeaxにセットします。32bitより大きい場合は、 一旦別の場所に置いてからそのポインタをeaxにセットします
他にもthiscall(ecxにthisポインタがセットされます)とかがありますが、省略します。

__stdcall関数のスタックの使われ方

目次へジャンプ。
[asm編]関数と復帰値   2005. 5.10 (01版)
ここでは、関数の復帰(値)を__asmブロック中で行う場合や、__asmブロック中から関数を呼び出す場合について
説明しましょう。(関数呼び出しはあまり使うこともないとは思いますが)

結論から言うと、__asmブロック中から C 関数は呼び出し可能ですが、C++関数はオーバーロードできないためダメだということです。 そういう場合には extern "C" リンケージ宣言するなりして C 関数として取り扱う必要があります。

復帰値ですが、EAXレジスタが使われます。つまりEAXレジスタに返したい値を入れて ret すればよいわけです。
なお、その場合、ワーニングレベル2以上では、復帰値が無いとコンパイラに警告される場合があります。
この場合の対処としては、次の2通りがあります。

(1) #pragmaを使って強制的にwarningを出さなくさせる
#pragma warning(disable:4035)
int __stdcall Add(int var_a, int var_b) {
    __asm {
        mov  eax, var_a       ;実際には、mov  eax, dword ptr _var_a$[ebp] と展開されます。
        mov  edx, var_b       ;  (_var_a$ = 8, _var_b$ = 12)
        add  eax, edx         ;このように関数の復帰値はeaxレジスタにセットします。
    }
    return;                   ;ret 8
}
#pragma warning(default:4035)
void main(void) {
    static const char OutputFormat[] = { "%d + %d = %d\n" };
    int rc;
    int a = 2;
    int b = 3;
    
    rc = Add(a, b);
    
    __asm {
       mov   eax, b
       push  eax
       mov   eax, a
       push  eax
       mov   eax, OFFSET OutputFormat
       push  eax
       call  printf
       pop   ebx                       ;スタック位置を元に戻す
       pop   ebx                       ;替わりに add esp, 12 でもよい。
       pop   ebx                       ;
    }
}

(2) __declspec(naked)宣言を使って関数のプロローグ、エピローグとも自前で記述する
int __declspec(naked) Func()
{
    // Naked functions must provide their own prolog...
    __asm {
        push ebp
        mov esp, ebp
        sub esp, __LOCAL_SIZE
    }
    
    // ... and epilog
    __asm {
        mov eax, 1   ;復帰値をeaxにセットする
        pop ebp
        ret
    }
}
上記のAdd()は以下のような感じになります。
int __declspec(naked) __stdcall Add(int var_a, int var_b) {
    __asm {
        mov  eax, var_a
        mov  edx, var_b
        add  eax, edx
        ret  8
    }
}

目次へジャンプ。


[PR]かんたんクイズで賞品GET:液晶TVや旅行券、DSも貰える!