typedef unsigned int uint32_t; typedef std::basic_string<uint32_t> u32stirng;などの定義をして文字列を使うことになるでしょう.これらの型はC++0xでは標準になる予定です. 実際には利便性のためにchar*との自然な変換メソッドなどを追加すると思いますが,ここでは省略します. boost::regexでもstd::stringと同様にテンプレートパラメータに文字の型を与えることができ,
typedef boost::basic_regex<uint32_t> u32regex; typedef boost::match_results<u32string::const_iterator> u32match;などと定義するとu32stringに対しても正規表現が使えるようになります. が,\w, \s, \dや[:digit:]などを使うとややこしい問題が発生します. これらを使うと現在の実装(boost 1.38)では内部的にuint32_tからcharにキャストしてから判別するようで,全然関係のない文字がマッチしてしまうことがあります(これはboostが悪いというわけではありません). たとえば /* これはUTF-8 */ const uint32_t str_[] = { 0x3053, 0x308c, 0x306f, 'U', 'T', 'F', '-', '8', 0 }; にたいして"\w+"をマッチさせると,先頭の一文字がマッチしてしまいます.これは0x3053はcharにキャストされ,0x53になり'S'と看做されてしまうからです. ここではこれを回避する最低限の修正について説明します. boost::regexでは上記の文字の型の判定にstd::ctype
namespace std { template<> bool ctype<uint32_t>::is(std::ctype_base::mask m, uint32_t c) const { if (c & ~0xFFU) return false; return use_facet<ctype<char> >(std::locale()).is(m, static_cast<char>(c)); } template<> uint32_t ctype<uint32_t>::tolower(uint32_t c) const { if ('A' <= c && c <= 'Z') return c - 'A' + 'a'; return c; } template<> uint32_t ctype<uint32_t>::widen(char c) const { return c; } }勝手にstd空間に関数を定義しているのは一見奇妙に思えますが,std空間にテンプレートの特殊化を追加することは規格上認められているので問題ありません. 本来はstd::ctype_base::maskにはspace, upperなど文字の特性を判別するフラグが入ってくるのでUnicodeの規格に応じて正しく分岐したほうがよいかもしれません. しかし純粋にasciiだけをチェックしたい場合もあるでしょうから,ここでは1byte文字でなければfalseにして,1byteのときは既存のcharに対するctypeを呼ぶことにしました. スペースをasciiの0x20だけでなく全角スペース(0x3000)や,他のUnicodeのスペースを判別したい場合はisを適宜修正してください. この特殊化を追加することで,上記のマッチングは正しく"UTF"を返すようになります. サンプルコード (注意)Linux上のgccではこれらの修正だけでは正しく動作しませんが,今回は触れません. なお,isの中のuse_facetをする部分
return use_facet<ctype<char> >(std::locale()).is(m, static_cast<char>(c));は極めて重たい処理ですので,static変数にキャッシュしておきそれを使うのがよいです.
static const std::ctype<char>& cache = use_facet<ctype<char> >(std::locale()); return cache.is(m, static_cast<char>(c));あるいは,std::ctype
static struct Custom : public std::ctype<char> { const mask * table() const { return std::ctype<char>::table(); } } custom; static const mask* maskTbl = custom.table(); return (maskTbl[(unsigned char)c] & m) != 0;などとするのがよいでしょう. ]]>
my SHA-1 | pajhome | JavaScriptでハッシュアルゴリズム | |
---|---|---|---|
IE6 | 0.94msec | 10.47msec | 6.25msec |
Firefox 3.0.1 | 0.63msec | 2msec | 1.39msec |
Chrome | 0.55msec | 0.44msec | 0.65msec |
size_t strlenSSE2(const char *p) { const char *const top = p; __m128i c16 = _mm_set1_epi8(0); /* 16 byte alignment */ size_t ip = reinterpret_cast(p); size_t n = ip & 15; if (n > 0) { ip &= ~15; __m128i x = *(const __m128i*)ip; __m128i a = _mm_cmpeq_epi8(x, c16); unsigned long mask = _mm_movemask_epi8(a); mask &= 0xffffffffUL << n; if (mask) { return bsf(mask) - n; } p += 16 - n; } /* thanks to egtra-san */ assert((reinterpret_cast (p) & 15) == 0); if (reinterpret_cast (p) & 31) { __m128i x = *(const __m128i*)&p[0]; __m128i a = _mm_cmpeq_epi8(x, c16); unsigned long mask = _mm_movemask_epi8(a); if (mask) { return p + bsf(mask) - top; } p += 16; } assert((reinterpret_cast (p) & 31) == 0); for (;;) { __m128i x = *(const __m128i*)&p[0]; __m128i y = *(const __m128i*)&p[16]; __m128i a = _mm_cmpeq_epi8(x, c16); __m128i b = _mm_cmpeq_epi8(y, c16); unsigned long mask = (_mm_movemask_epi8(b) << 16) | _mm_movemask_epi8(a); if (mask) { return p + bsf(mask) - top; } p += 32; } }
average length | 2 | 5 | 7 | 10 | 12 | 16 | 20 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
strlenANSI | 683.8 | 406.3 | 308.8 | 234.5 | 203.3 | 168.0 | 148.3 | 113.3 | 86.0 | 70.5 | 62.5 | 58.5 | 54.5 |
strlenBLOG | 835.8 | 449.3 | 347.5 | 269.5 | 234.3 | 195.3 | 175.8 | 140.5 | 109.3 | 89.8 | 82.0 | 78.3 | 82.3 |
strlenSSE2 | 765.8 | 355.5 | 269.5 | 199.3 | 172.0 | 133.0 | 109.5 | 78.3 | 47.0 | 27.3 | 19.8 | 15.5 | 7.8 |
memchrANSI | 1046.8 | 648.5 | 515.8 | 390.5 | 347.8 | 273.3 | 226.5 | 164.0 | 105.5 | 74.3 | 62.5 | 50.8 | 50.8 |
memchrSSE2 | 773.5 | 375.0 | 285.0 | 214.8 | 179.5 | 144.8 | 121.3 | 82.0 | 54.5 | 31.3 | 19.5 | 15.8 | 11.8 |
average length | 2 | 5 | 7 | 10 | 12 | 16 | 20 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
strlenANSI | 2019.6 | 933.9 | 728.7 | 576.8 | 512.3 | 430.5 | 386.6 | 316.5 | 261.2 | 235.6 | 219.4 | 214.0 | 212.9 |
strlenBLOG | 692.6 | 397.4 | 331.0 | 242.5 | 216.7 | 194.3 | 152.2 | 124.7 | 110.3 | 81.5 | 76.3 | 82.2 | 70.0 |
strlenSSE2 | 560.0 | 275.6 | 214.3 | 159.2 | 135.7 | 104.4 | 87.5 | 65.1 | 41.5 | 25.3 | 16.6 | 14.0 | 9.3 |
memchrANSI | 1152.4 | 609.5 | 487.4 | 375.0 | 325.6 | 260.5 | 229.9 | 152.4 | 95.5 | 72.8 | 56.8 | 49.3 | 48.0 |
memchrSSE2 | 574.6 | 282.1 | 224.3 | 161.0 | 139.1 | 108.5 | 90.0 | 63.3 | 45.1 | 23.9 | 15.8 | 10.0 | 11.3 |
average length | 2 | 5 | 7 | 10 | 12 | 16 | 20 | 32 | 64 | 128 | 256 | 512 | 1024 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
strlenANSI | 1039.4 | 568.0 | 460.9 | 353.7 | 306.9 | 254.9 | 212.2 | 145.2 | 82.0 | 50.3 | 35.3 | 26.4 | 24.5 |
strlenBLOG | 795.4 | 474.9 | 366.4 | 291.0 | 263.9 | 227.6 | 201.7 | 178.3 | 144.5 | 125.9 | 117.2 | 112.0 | 110.5 |
strlenSSE2 | 703.0 | 340.0 | 267.6 | 196.3 | 167.2 | 131.6 | 108.9 | 78.7 | 47.4 | 30.2 | 20.4 | 15.0 | 14.5 |
memchrANSI | 1280.7 | 736.4 | 594.2 | 472.2 | 423.8 | 338.0 | 294.3 | 211.0 | 127.2 | 90.4 | 67.3 | 55.4 | 55.1 |
memchrSSE2 | 1001.4 | 456.3 | 336.4 | 241.3 | 206.4 | 159.5 | 138.5 | 93.0 | 57.5 | 35.6 | 23.3 | 17.0 | 15.7 |
64ビットアプリケーションではx87命令・MMX命令及び3DNow!命令をサポートしない(x87レジスタをコンテキストスイッチの際にセーブしない)という記述があるのですが,試したところちゃんとセーブされているようです. テスト方法 test_mmx.cppをコンパイルして(binary) コマンドプロンプトを二つ開いて
test_mmx 1 test_mmx 3と実行します.それぞれスレッドを起動して,1, 2, 3, 4という数値が表示され続けました. もしMMXレジスタがコンテクストスイッチで保存されていなければ全て同じものになるはずなので,保存されていると推測されます. Legacy Floating-Point Supportにも
The MMX and floating-point stack registers (MM0-MM7/ST0-ST7) are preserved across context switchesとあるので保存されているように思われます. 一つ気になるのは私がテストしたのは32bit WindowsのVMware上の64bit Xpだったという点です.素の64bit環境でテストした場合どうなるかどなたか試していただけますでしょうか. 追記 奥さんから英語版Wikipediaには使えると書いてますよという情報をいただきました. #見るの忘れてました.迂闊だった…. ]]>
命令(R = A or B) | 意味 |
---|---|
vldiR, imm | R = imm |
vldR, idx / vstR, idx | R = mem[idx] / mem[idx] = R |
vaddiR, imm / vsubiR imm | R += imm / R -= imm |
vaddR, idx / vsubR, idx | R += mem[idx] / R -= mem[idx] |
vputR | print R |
vjnzR, offset | if (R != 0) then jmp(PC += offset(signed)) |
void decode(uint32& code, uint32& r, uint32& imm, uint32 x) { code = x >> 24; r = (x >> 16) & 0xff; imm = x & 0xffff; } void encode(Code code, Reg r, uint16 imm = 0) { uint32 x = (code << 24) | (r << 16) | imm; code_.push_back(x); }
void vldi(Reg r, uint16 imm) { encode(LDI, r, imm); } void vld(Reg r, uint16 idx) { encode(LD, r, idx); } void vst(Reg r, uint16 idx) { encode(ST, r, idx); } void vadd(Reg r, uint16 idx) { encode(ADD, r, idx); } void vaddi(Reg r, uint16 imm) { encode(ADDI, r, imm); } void vsub(Reg r, uint16 idx) { encode(SUB, r, idx); } void vsubi(Reg r, uint16 imm) { encode(SUBI, r, imm); } void vjnz(Reg r, int offset) { encode(JNZ, r, static_cast(offset)); } void vput(Reg r) { encode(PUT, r); }
void run() { uint32 reg[2] = { 0, 0 }; // A, B const uint32 end = code_.size(); uint32 pc = 0; for (;;) { uint32 code, r, imm; decode(code, r, imm, code_[pc]); switch (code) { ... } pc++; if (pc >= end) break; } // for (;;) }基本構造は上記のようになります. pc(プログラムカウンタ)を0から順に増やしつつ,4byteずつcode_からデータを読みます. 読んだデータをdecode()でパラメータに分解し,codeに従って各命令を実行させるswitch文に突入します. そのあとpcを一つ増やして繰り返します. switch文の中身は各命令に対して実際行う処理を書きます.
switch (code) { case LDI: reg[r] = imm; break; case LD: reg[r] = mem_[imm]; break; case ST: mem_[imm] = reg[r]; break; case ADD: reg[r] += mem_[imm]; break; case ADDI: reg[r] += imm; break; case SUB: reg[r] -= mem_[imm]; break; case SUBI: reg[r] -= imm; break; case PUT: printf("%c %8d(0x%08x)\n", 'A' + r, reg[r], reg[r]); break; case JNZ: if (reg[r] != 0) pc += static_cast<signed short>(imm); break; default: assert(0); break; }とくに難しいところは無いでしょう.これでVM自体は完成です.なんと簡単.
void fibC(uint32 n) { uint32 p, c, t; p = 1; c = 1; lp: t = c; c += p; p = t; n--; if (n != 0) goto lp; printf("c=%d(0x%08x)\n", c, c); }このコードが正しく動作することを確認したら,これをtoyVMのアセンブリ言語で書きます. その前に変数をどう扱うかを決めておく必要があります. toyVMにレジスタは二つありますが,両方をフィボナッチで使う変数に割り当てると困るのでとりあえずcをAレジスタに割り当てることにします. Bはテンポラリに残しておきましょう. あと,fibCにはp, t, nという変数があるのでこれらはtoyVMのメモリ上に置くことにします. ここではmem[0] : p, mem[1] : t, mem[2] : nとしました. ではfibCのアセンブリ言語版を書きます.
Fib(int n) { vldi(A, 1); // c = 1 vst(A, 0); // p = 1 vldi(B, n); vst(B, 2); // n // lp vst(A, 1); // t = c vadd(A, 0); // c += p vld(B, 1); // mem[1]の値をBを経由してmem[2]に移動する vst(B, 0); // p = t vld(B, 2); vsubi(B, 1); vst(B, 2); // n-- vjnz(B, -8); // PCを8減らせばlpのところにもどる. vput(A); }ちょっと分かりにくいかもしれませんが,1行ずつfibCと比べれば同じ処理をしようとしていることがわかるでしょう. 実行してみます.
Fib fib(10); fib.run(); >A 144(0x00000090)正しく動作しているようです.
void recompile() { push(ebx); push(esi); push(edi); const Reg32 reg[2] = { esi, edi }; const Reg32 mem(ebx); xor(reg[0], reg[0]); xor(reg[1], reg[1]); mov(mem, (int)mem_); const uint32 end = code_.size(); uint32 pc = 0; uint32 labelNum = 0; for (;;) { uint32 code, r, imm; decode(code, r, imm, code_[pc]); L(toStr(labelNum++)); switch (code) { ... pc++; if (pc >= end) break; } // for (;;) pop(edi); pop(esi); pop(ebx); ret();違うのはx86用のコードを生成させるところです.と言っても見た目はそれほど変わりません.
switch (code) { case LDI: mov(reg[r], imm); break; case LD: mov(reg[r], ptr[mem + imm * 4]); break; case ST: mov(ptr [mem + imm * 4], reg[r]); break; case ADD: add(reg[r], ptr [mem + imm * 4]); break; case ADDI: add(reg[r], imm); break; case SUB: sub(reg[r], ptr [mem + imm * 4]); break; case SUBI: sub(reg[r], imm); break;概ねrun()と一対一に対応していることがわかるでしょう.分岐のみちょっと変わったことをする必要があります. toyVMでは命令長が4byte固定だったので命令数だけポインタを減らせば分岐ができたのですが,x86ではそうではありません. ここでは簡単にすませるために,一命令毎に数値のラベルを生成させて,そのラベルへ分岐するようにしました.
L(toStr(labelNum++)); switch (code) { ... case JNZ: test(reg[r], reg[r]); jnz(toStr(labelNum + static_cast<signed short>(imm))); break;以下は実行時にrecompile()して得たx86のコードです.
.lp: mov dword ptr [ebx+4],esi add esi,dword ptr [ebx] mov edi,dword ptr [ebx+4] mov dword ptr [ebx],edi mov edi,dword ptr [ebx+8] sub edi,1 mov dword ptr [ebx+8],edi test edi,edi jne .lp問題なさそうです.
通常VM | JIT | native C(fibC) |
---|---|---|
1216K | 136K | 84K |
const Reg32 memTbl[] = { eax, ecx, edx }; const size_t memTblNum = NUM_OF_ARRAY(memTbl); for (size_t i = 0; i < memTblNum; i++) xor(memTbl[i], memTbl[i]);そしてrecompileでメモリにアクセスする部分を変更します.
case ADD: if (imm < memTblNum) { // add(reg[r], memTbl[imm]); // 追加部分 } else { // add(reg[r], ptr [mem + imm * 4]); } break;mem[]の先頭にアクセスするときのみレジスタへのアクセスに変更しました. これによりリコンパイルで生成されるコードは以下のようになりました.
.lp: mov ecx,esi add esi,eax mov edi,ecx mov eax,edi mov edi,edx sub edi,1 mov edx,edi test edi,edi jne .lpすっきりしました. ベンチマークをとってみます.
通常VM | JIT | 改良版JIT | native C(fibC) |
---|---|---|---|
1216K | 136K | 101K | 84K |
typedef unsigned int uint32; #ifdef WIN32 uint32 bsr(uint32 x) { __asm { bsr eax, x } } #else #define bsr(x) __builtin_clz(x) #endif void put(uint32 x) { for (int i = 0; i < 32; i++) { putchar((x & (1 << (31 - i))) ? '1' : '0'); } printf("\n"); } int main() { uint32 x = 0; int len = 31; for (int i = 0; i < 32; i++) { put(x); uint32 s = 31 - bsr(~x); x = ((x << s) + (1 << len)) >> s; } }という感じのコードで,これはこれで面白いです.ただ今回のケースに限っては資料に書いた方法の方が命令数が短くて速いですね. ]]>
lea eax, [eax + eax*8] lea eax, [eax + eax*4]45 = 9 * 5 = (8 + 1) * (4 + 1)を利用する.乗算命令が速くなったとはいえ,まだadd/sub/shift/leaの組み合わせを使った方が速いことが多い.
cdq xor eax, edx sub eax, edx条件分岐は可能な限り使わない.次の二つの恒等式
-x = x ^ (-1) - (-1) x = x ^ 0 - 0と
unsigned int m = (x < 0) ? -1 : 0を組み合わせることで絶対値を
unsigned int m = x >> 31; return x ^ m - m;と実現する.ここでmを取得する部分は
mov edx, eax sar edx, 31であるが,これに限り32bit除算の準備のために用意されたcdqを使うことができる.
cdq ⇔ edx = eax < 0 ? -1 : 0P4系ではどちらも同じμOPに分解されるがcdqの方がコードサイズは小さくてすむ.
/* 3.1 */ .label: ... shr eax, 1 shr eax, 1 jnz .labelCore系では二つの1bitシフトに分割したほうが速くなることがある.
mov edx, [esp+4] bsr eax, edx cmovz eax, edx retbsr命令は入力値edxが0のとき出力値eaxが不定となる仕様であるため単純には使えない. そのため
mov eax, [esp+4] test eax, eax jz .skip bsr eax, eax jmp .exit .skip: mov eax, [0の時の特殊値] .exit:という形にしたくなる.が,bsrは入力値が0のときZF(ゼロフラグ)を1にする.これを利用すれば,ZF == 1のときのみ移動する条件付き移動命令cmovzを使って
mov edx, [esp+4] bsr eax, edx cmovz eax, [0の時の特殊値]とできる.更に特殊値が0ならば答えのように短くできる.
sub esp, 8192 mov [esp+4096], eax ; dummy write mov [esp], eax add esp, 8192などのように4KBずつアクセス(readでも可)しておく必要がある.
cwOrg dw ; デフォルトのFPU制御ワード cwTrunc dw ; cwOrg |= (3 << 10) ; truncate to 0 ; ループ前 fldcw [cwTrunc] ; 丸めモードを0への切り捨てに変更する ; ループ内 fistp dword [...] ; ループ後 fldcw [cwOrg] ; デフォルトモードに復元
mxcsrOrg dd ; デフォルトのMXCSRレジスタ mxcsrTrunc dd msxcrOrg |= (3 << 11) ; truncate to 0 ; ループ前 ldmscsr [mxcsrTrunc] ; 丸めモードを0への切り捨てに変更する ; ループ内 cvtps2pi [...] ; float x 2 → int x 2 ; ループ後 ldmxcsr [mxcsrOrg] ; デフォルトモードに復元ただし,FPU制御ワードの変更に比べてmscsrの変更はコストが大きい.ループが小さい場合はSSEを使わずFPUで閉じた方がよいケースもある.
cvttps2piを使えばMSCSRレジスタを変更することなく切り捨てを行える
fisttpを使えばFPU制御ワードを変更することなく切り捨てを行える
proc log2A mov edx, [esp+4] mov eax, [esp+8] mov ecx, [esp+12] push ebx .lp: mov ebx, [eax] add eax, byte 4 bsr ebx, ebx mov [edx], ebx add edx, byte 4 sub ecx, byte 1 jnz .lp pop ebx retまたxを2e * f(1 <= f < 2)という形で考えると,bsrはeを求めることに相当する.したがってxをdoubleに変換し,その指数部を取り出す方法もある.doubleの仮数部は11bit,指数部は52bitなので52(=32+20)bit右シフトした後,1023を引けばよい.bsrはレイテンシが大きいのでこちらの方が速い.
_1023 dd 1023, 1023, 1023, 1023 proc log2B mov edx, [esp+4] mov eax, [esp+8] mov ecx, [esp+12] .lp: cvtpi2pd xm0, [eax] ; [D1_H:D1_L:D0_H:D0_L] cvtpi2pd xm1, [eax+8] ; [D3_H:D3_L:D2_H:D2_L] add eax, 16 shufps xm0, xm1, (3 * 64 + 1 * 16 + 3 * 4 + 1) ; [D3_H:D2_H:D1_H:D0_H] ; 32bit右シフト相当 psrld xm0, 20 ; 20bit右シフト psubd xm0, [_1023] movaps [edx], xm0 add edx, 16 sub ecx, byte 4 jnz .lp retなお,もし0 < x < (1 << 23)という条件があるならばdoubleではなくfloatに変換することで更に効率よく処理を行える.floatの仮数部は8bit,指数部は23bitなので23bit右シフトした後,127を引けばよい.
_127 dd 127, 127, 127, 127 proc log2C mov edx, [esp+4] mov eax, [esp+8] mov ecx, [esp+12] .lp: cvtpi2ps xm0, [eax] ; [*:*:F1:F0] cvtpi2ps xm1, [eax + 8] ; [*:*:F3:F2] punpckldq xm0, xm1 ; [F3:F2:F1:F0] add eax, 16 psrld xm0, 23 psubd xm0, [_127] movaps [edx], xm0 add edx, 16 sub ecx, byte 4 jnz .lp retよく「アセンブラを使っても最近の最適化コンパイラに勝てない」という意見が聞かれるが,それは単に「自分では確認せず,おうむ返しをしている」か,「コンパイラと同じ(or それ以下の)コードを書いている」かのどちらかであろう.もちろん「コストに見合わない」こともあるがそれはまた別の話である. 一般に最適化ではQ6, 7, 10のような,コンパイラで自動生成されることのない部分を考慮しつつ,さぼれるだけさぼれるロジックを考える.そしてそういうロジックを自在に記述するためにアセンブリ言語やイントリンジック関数を使う.実際,これらのテクニックは音声や動画のコーデックの開発でしばしば使われる.その際Q3などの知識がクリティカルとなることはまずない.瑣末な知識を覚えるよりも(無論覚えるに越したことはない --- 実践に暗記は必要である),常に代替ロジックが無いかを考える方がよりよいコードに繋がると思う. ]]>
; input : eax ; output: eax ; destroy: edx mov edx, eax sar edx, 31 xor eax, edx sub eax, edx
/* 3.1 */ .label: ... shr eax, 2 jnz .label
/* 3.2 */ .label: ... sub ecx, byte 1 jnz .label*すいません,jzはjnzのtypoでした.
magic dd 0x01800000 x dd 1.5 ; float movss xm0, [x] paddd xm0, [magic]
int getBit(unsigned int x) { for (int i = 31; i >= 0; i--) { if (x & (1 << i)) return i; } return 0; }をbsrを使って最適化せよ.
sub esp, 8192 mov [esp], eax add esp, 8192
/* -1 <= in[i] <= 1とする. */ void func(int *out, const float *in, int n) { for (int i = 0; i < n; i++) { out[i] = int(in[i] * 32767); } }
void log2(int *dest, const int *src, int n) { for (int i = 0; i < n; i++) { #if 0 /* わずかな誤差で意図しない値になることがあったので修正 */ // dest[i] = (int)(log(src[i]) / log(2)); #else int j; int x = src[i]; for (j = -1; x > 0; j++) { x >>= 1; } dest[i] = j; #endif } }]]>
double func(double a, double b);とするよりは
void func(double *out, const double *a, const double *b);とすれば移植性が高くなるということです(もちろんasmとのやりとりが必要な部分のみです).原理的に後者の方がやや遅くなる可能性がありますが,高速化を目的とした処理では通常,関数内に大きなループが存在するため,この二つの差が問題になるケースはまずありません.逆に,問題になるぐらいならその前後の処理も含めてasmの対象したほうがよい可能性が高いです.
int func0(int a, char *b, double *c);の場合は
push(ポインタcの値); push(ポインタbの値); push(aの値); call(int(func0)); add(esp, 3 * 4); /* 引数が3個なので3 * 4 */とします.引数がなければpushとaddは省略できます.たとえば
void func();の場合は
call(int(func));となります.
esp + 8 : c esp + 4 : b esp + 0 : aなのでしょうか.実はちょっと違いまして,callを実行したとき,こっそりと実行していた命令の次の命令の先頭アドレスがスタックに格納されています.つまり正解は
esp + 12: c esp + 8 : b esp + 4 : a esp + 0 : 次の命令のアドレスとなります.デバッガで確認してみましょう.
extern "C" void func(int a, int b, int c) { } int main() { func(1, 2, 3); }をfuncのところでステップ実行してみます.下記はcallを実行する直前でのレジスタとスタックの状況です.スタック領域に1, 2, 3が格納されていることを確認してください.
<asm> 00401B97 push 3 00401B99 push 2 00401B9B push 1 00401B9D call @ILT+1400(func) (0040157d) 00401BA2 add esp,0Ch <レジスタ> EAX = CCCCCCCC EBX = 7FFDF000 ECX = 00000000 EDX = 00371538 ESI = 00000000 EDI = 0012FF70 EIP = 00401B9D ESP = 0012FADC EBP = 0012FF80 EFL = 00000212 <スタック> 0012FADC 01 00 00 00 02 00 00 00 03 00 00 00 00 00 00 00 0012FAEC 00 00 00 00 00 F0 FD 7F CC CC CC CC CC CC CC CC 0012FAFC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CCここでcallを実行します.
<レジスタ> EAX = CCCCCCCC EBX = 7FFDF000 ECX = 00000000 EDX = 00371538 ESI = 00000000 EDI = 0012FF70 EIP = 00401B30 ESP = 0012FAD8 EBP = 0012FF80 EFL = 00000212 <スタック> 0012FAD8 A2 1B 40 00 01 00 00 00 02 00 00 00 03 00 00 00 0012FAE8 00 00 00 00 00 00 00 00 00 F0 FD 7F CC CC CC CC 0012FAF8 CC CC CC CC CC CC CC CC CC CC CC CC CC CC CC CCespの値が4減ってスタック領域にcallの次の命令のアドレス0x00401ba2が格納されたことがわかります.ret()でfunc()を抜けるときに,この値を参照して帰るべき場所を決定します.そのとき自動的にespも4増えています. 今回は関数の呼び出し規約について説明しました.納得するまで何度もデバッガで確認するのがよいと思います. ]]>
push((int)"Hello Xbyak!"); // (C1) call((int)puts); // (C2) add(esp, 4); // (C3) ret(); // (C4)このプログラムはCでいうところの
void hello() { puts("Hello Xbyak!"); }と同じです(関数名は関係ないですがとりあえずhelloとします).
(gdb) al 0x804b190: push 0x804a756 0x804b195: call 0x8048710 <puts@plt> 0x804b19a: add esp,0x4 0x804b19d: ret (gdb) p $esp $1 = (void *) 0xbffff2ac // (*) (gdb) si 0x0804b195 in ?? () // (C1)を実行した (gdb) p $esp $2 = (void *) 0xbffff2a8 // (*)に比べてespの値が4減っている (gdb) ni // (C2)callを実行する Hello Xbyak! // 文字列が表示された 0x0804b19a in ?? () (gdb) si 0x0804b19d in ?? () // (C3)を実行 (gdb) p $esp $3 = (void *) 0xbffff2ac // espの値が(*)に戻った (gdb) ni main () at t.cpp:19 19 return 0;今回はアドレス,レジスタとスタック,puts("Hello Xbyak!")の中身を説明しました.次回はサンプルプログラムの説明を続けます. ]]>
#include "xbyak/xbyak.h" #include <stdio.h> struct HelloGenerator : public Xbyak::CodeGenerator { // (A) HelloGenerator() // (B) { push((int)"Hello Xbyak!"); // (C1) call((int)puts); // (C2) add(esp, 4); // (C3) ret(); // (C4) } }; int main() { HelloGenerator hello; // (D) void (*code)() = (void (*)())hello.getCode(); // (E) code(); // (F) return 0; }VC系ではコンソールアプリとしてコンパイルしてください. gccでは-fno-operator-namesをつけるのを忘れないでください.これはandやorを演算子ではなく関数として扱いたいためです.
> g++ t.cpp -g -fno-operator-names > ./a.out > Hello Xbyak!# C++的には
#include <stdio.h> int main(int argc, char *argv[]) { argc == 1 and puts("need option"); }ってできたのご存じでした? それはともかくコードの説明に入ります.Xbyakでは実行時に命令をアセンブルしてバイトコード(以下マシン語と呼ぶことにします)を生成します.それを生成するためのクラスがXbyak::CodeGeneratorであり,これを継承する(A)ことでそのクラス内でx86の殆どの命令を記述できるようになります. 今後も繰り返すことになるかと思いますが,コンパイル時ではなく実行時であることに注意してください.ここではHelloGeneratorのコンストラクタにXbyak命令(面倒なので以後asmと呼びます)を記述している(B)ので,main()内のインスタンスhelloが生成されたとき(D)にアセンブルされます. アセンブルされたマシン語はgetCode()メソッドで取得できます(E).この返り値は生成されたマシン語へのポインタですので,適当な関数ポインタ(詳細は後述)にキャストし,その関数を呼び出すことでマシン語を実行します(F). 以上がXbyakを使った場合の大まかな流れです. 続いて実行時の挙動を追いかけてみましょう. VC系ではDebugモードでコンパイルし,(F)のところにブレークポイントを置いてデバッグ実行します.停止したところでVC6なら[表示]→[デバッグウィンドウ]→[混合モード],VC8なら[デバッグ]→[ウィンドウ]→[逆アセンブル]で
19: code(); 00401696 8B F4 mov esi,esp 00401698 FF 95 F4 FE FF FF call dword ptr [ebp-10Ch] 0040169E 3B F4 cmp esi,esp 004016A0 E8 2B 14 01 00 call __chkesp (00412ad0)というようなウィンドウに入ってください.[F10]を一度押し,callのところで[F11]を押すと
003730A4 68 B0 01 44 00 push offset string "Hello Xbyak!" (004401b0) 003730A9 E8 62 FA 09 00 call puts (00412b10) 003730AE 83 C4 04 add esp,4 003730B1 C3 retが表示されると思います.左端の数値は違うかもしれません.意味も今は分からなくて結構です.ただ上記サンプルの(C1)~(C4)に対応していることを確認してください. gccの場合はgdbを使っておっかけましょう.まず起動してgetCode()のところでブレークポイントを置きます.
>gdb ./a.out >b HelloGenerator.getCode Breakpoint 1 at 0x804b2b6: file xbyak/xbyak.h, line 1307.実行した後,[n]と[Enter]でステップ実行し,code()の直前まで進みます.
(gdb) r Starting program: /home/shigeo/Program/xbyak/a.out Breakpoint 1, Xbyak::CodeGenerator::getCode (this=0xbffff2e0) at xbyak/xbyak.h:1307 1307 assert(!hasUndefinedLabel()); (gdb) n 1309 return top_; (gdb)([Enter]を押す) 1310 } ([Enter]を押す) (gdb) ([Enter]を押す) main () at t.cpp:19 19 code();ここでコードがどうなっているか見ます.その前にデバッグに見やすいaliasを作っておきます.
(gdb) define al Type commands for definition of "al". End with a line saying just "end". >x/11i $pc >endと定義してみてください.
(gdb) al 0x8048904 <main+52>: mov eax,DWORD PTR [ebp-12] 0x8048907 <main+55>: call eax 0x8048909 <main+57>: mov ebx,0x0 0x804890e <main+62>: lea eax,[ebp-0x108] 0x8048914 <main+68>: mov DWORD PTR [esp],eax 0x8048917 <main+71>: call 0x8049c2c <~HelloGenerator> 0x804891c <main+76>: mov DWORD PTR [ebp-0x10c],ebx 0x8048922 <main+82>: jmp 0x8048952 <main+130> 0x8048924 <main+84>: mov DWORD PTR [ebp-0x110],eax 0x804892a <main+90>: mov ebx,DWORD PTR [ebp-0x110] 0x8048930 <main+96>: lea eax,[ebp-0x108]siを使ってasmレベルでのステップ実行をします.
(gdb) si 0x08048907 19 code(); (gdb) ([Enter]を押す) 0x0804c190 in ?? ()call eaxが実行された瞬間からXbyakが生成したマシン語に突入しています.alで確認しましょう.
(gdb) al 0x804c194: push 0x804b44b 0x804c199: call 0x8048734 <puts@plt> 0x804c19e: add esp,0x4 0x804c1a1: retやはり上記サンプルの(C1)~(C4)に対応していることを確認してください. うっかり行き過ぎたら,最初からやり直してマシン語を一行ずつ実行する感じをつかんでください.gdbを使うときはアセンブラで遊ぶ時に便利なgdb設定を参考にすると便利だと思います.上記alもこのリンク先の.gdbinitで定義されているのを拝借しました. 今回はXbyakの基本的な書き方と実行時に確認する方法の説明をしました.長くなりましたので一端区切ります.次はコードの説明に入ります. ]]>
>./test0 xbyak version=1.06 0 + ... + 0 = 0 0 + ... + 1 = 1 0 + ... + 2 = 3 0 + ... + 3 = 6 0 + ... + 4 = 10 0 + ... + 5 = 15 0 + ... + 6 = 21 0 + ... + 7 = 28 0 + ... + 8 = 36 0 + ... + 9 = 45 0 + ... + 10 = 55 0 + 0 = 0 1 + 1 = 2 2 + 2 = 4 3 + 3 = 6 4 + 4 = 8 5 + 5 = 10 6 + 6 = 12 7 + 7 = 14 8 + 8 = 16 9 + 9 = 18 call atoi("123") = 123 jmp atoi("456") = 456 OKの様に表示されればOKです.もし,ビルドエラーになる,動かないなどがあれば環境とエラー内容を教えていただけるとありがたいです. インストールするにはLinux,Macの場合はrootになって
make installとすると/usr/local/include/xbyakにインストールされます. WindowsのVisual Studio 6の場合は[オプション]→[ディレクトリ],Visual Studio 2005 Expressionなどの場合は[ツール]→[オプション]→[プロジェクトおよびソリューション]→[VC++ディレクトリ]→[インクルードファイル]でincludeディレクトリに展開したxbyakディレクトリを追加してください. サンプルプログラムについて
add3 : function(y) { return y + 3; }と
addF : function(x) { var z = 2; for (var i = 0; i < 3; i++) { z += this.add3(i); } return z; }という関数があるとします. addF()に対してblowfish.js(仮実装なので実用性はまだありません)にあるunrollLoop()を適用すると,addF()が書き換えられて
function(x) { var z = 2; z += this.add3(0); z += this.add3(1); z += this.add3(2); return z; }とループ展開されます.更にforceInline()を適用すると,
function(x) { var z = 2; { var _y = 0; z+=(_y + 3); } { var _y = 1; z+=(_y + 3); } { var _y = 2; z+=(_y + 3); } return z; }とインライン展開されます. ここではBlowfish暗号化のコア部分の最適化を各種方法で試した速度比較をするデモ(JavaScriptを有効にしてください)を用意しました. 比較対象はoriginal(リファレンス実装),自動インライン展開,自動ループ展開,自動インライン展開 + ループ展開,手動最適化の五つです.それらの処理時間を測定し,オリジナルに対する速度比を表示します.
original | forceInline | unrollLoop | unrollLoop + forceInline | 手動最適化(参考) | |
---|---|---|---|---|---|
IE | 1.0 | 1.03 | 1.64 | 1.73 | 2.20 |
Fx | 1.0 | 1.13 | 1.28 | 1.41 | 1.46 |