で、昨日書いた文章にちょっと予定外の抜けがあったのでその部分を追加しておきます。
さらに追加する可能性もあるので「その2」という題名の付け方になっています。
インラインアセンブラの扱い
そもそも64bitコードでは基本的にインラインアセンブラを扱うことができません。
コンパイラから文句を言われます。これはコードの互換性を保つ上では必須ともいえるので仕方ないと思います。
これによって困るのは以下のような場合です。
- 自前で画像圧縮や音楽圧縮などのアルゴリズムを使っていて、その場所でSIMD演算を使用するパターン
- 描画ルーチンでARGBフォーマットでの2Dアルゴリズムを使用するときにSIMD演算を使用するパターン
- cpuid命令など直接CPUの情報が欲しいときにインラインアセンブラを使用するパターン
で、これらのパターンについてみていきます。
CPUID系の命令などを使いたいとき
このパターンではVisualStudio2005以降であればintrin.hというファイルに組み込み関数形式で必要な関数が定義されています。
たとえば、cpuid命令ですが、インラインアセンブラで記述しようとすると次のような形になります。
static void cpuidasm(int CPUInfo[4],int InfoType) { __asm{ mov eax,InfoType xor ebx,ebx xor ecx,ecx xor edx,edx mov edi,CPUInfo cpuid mov [edi],eax mov [edi + 4],ebx mov [edi + 8],ecx mov [edi + 12],edx } } cpuidasm(cpuinfo,1);
で、これはもちろん64bitコードでは「ソースコード内」には記述できません。そのときは代理としてコンパイラ組み込み命令として定義されている__cpuid関数を使用します。
intrin.hに定義されている関数のほとんどはコンパイラによって実際の機械語へと直接変換される特性を持っています。これを使って
#include <intrin.h> __cpuid(cpuinfo,1);
のような形で使用して回避します。
同じことができるアセンブラ命令としては例として以下のようなものがあります。
__rdtsc | プロセッサのタイムスタンプを取得する機械語 |
_InterlockedIncrement | 排他的に変数をインクリメントする機械語(WinAPIではInterlockedIncrement関数が関数形式で存在する) |
_InterlockedDecrement | 排他的に変数をデクリメントする機械語(WinAPIではInterlockedDecrement関数が関数形式で存在する) |
SIMD命令を使用するパターン
SMID命令も同様にして回避することができます。VisualStudio2005以上であればintrin.hを使うことで最低でもSSE2までの命令は組み込み命令としてサポートされますので
これを使ってインラインアセンブラを「書いた気になる」ことで回避できます。
ただし、64bitコードでは通常MMXを使用しません。これはMMXの上位命令セットとなるSSE2が常にサポートされていると仮定できるためで、通常はこちらを組み込み命令として使用します。
もちろん、64bitコード上で組み込みMMX命令が使用できないことはないですが、使用は推奨されていませんし、処理速度的にも移行した方がいいと思います。
ちなみに最新のSIMD命令を使いたいときはIntel C Compilerあたりを参照してください。ゲーム開発レベルではSSE2まであれば十分な速度のコードが作れると思います。
というかSSE2を使っても十分な速度にならないようなコードを作っちゃう方がまずいです。
とくにSIMDなどをインラインアセンブラの形式で記述していたコードの場合はその移植に手間をかけることになると思います。今から組む場合はこの時点で組み込みアセンブラを使う形式として
再度コードを組み替えた方がいいと思います。すでに組んでしまっているときは変数としてレジスタを宣言する形で無理矢理Cのコードに変換するという方法が使えます。
一度自分で組んだことがあるコードならそれほど手間はかからないはずです。実際にARGB8888からXRGB8888への描画ルーチンはMMXを使ったインラインアセンブラでは
void BltAlphaARGB8888ToXRGB8888(void *lpDest,void *lpSrc,long lDestPitch,long lSrcPitch,int nWidth,int nHeight) { __asm{ pxor mm2,mm2 pcmpeqd mm5,mm5 psrlw mm5,8 mov esi,lpSrc mov edi,lpDest mov ebx,4 heightloop: mov ecx,nWidth widthloop: mov eax,dword ptr [esi] movd mm0,dword ptr [edi] movd mm1,eax mov edx,eax shr edx,24 test edx,edx jz widthloopinc cmp edx,0x000000ff je widthloopstore mov eax,edx shl eax,16 or eax,edx movd mm3,eax movd mm4,eax psllq mm3,32 por mm3,mm4 movq mm4,mm5 punpcklbw mm0,mm2 punpcklbw mm1,mm2 psubw mm4,mm3 pmullw mm1,mm3 pmullw mm0,mm4 paddw mm1,mm0 paddw mm1,mm5 psrlw mm1,8 packuswb mm1,mm0 movd eax,mm1 or eax,0xff000000 widthloopstore: mov dword ptr [edi],eax widthloopinc: add esi,ebx add edi,ebx sub ecx,1 jnz widthloop mov esi,lpSrc mov edi,lpDest mov eax,nHeight add esi,lSrcPitch add edi,lDestPitch sub eax,1 mov lpSrc,esi mov lpDest,edi mov nHeight,eax jnz heightloop emms } }
これをSSE2を使って組み込み命令に変換すると
void BltAlphaARGB8888ToXRGB8888(void *lpDest,void *lpSrc,long lDestPitch,long lSrcPitch,int nWidth,int nHeight) { __m128i xmm0,xmm1,xmm2,xmm3,xmm4,xmm5; register unsigned long *esi,*edi; register int ecx,ebx; register long eax,edx; xmm2 = _mm_xor_si128(xmm2,xmm2); xmm5 = _mm_cmpeq_epi32(xmm5,xmm5); xmm5 = _mm_srli_epi16(xmm5,8); for(ebx = nHeight;ebx > 0;ebx--){ esi = (unsigned long *)lpSrc; edi = (unsigned long *)lpDest; for(ecx = nWidth;ecx > 0;esi++,edi++,ecx--){ eax = *esi; xmm0 = _mm_cvtsi32_si128(*edi); xmm1 = _mm_cvtsi32_si128(eax); edx = eax >> 24; if(edx != 0x00){ if(edx < 0xff){ eax = (edx << 16) | edx; xmm3 = _mm_cvtsi32_si128(eax); xmm4 = _mm_cvtsi32_si128(eax); xmm3 = _mm_slli_epi64(xmm3,32); xmm3 = _mm_or_si128(xmm3,xmm4); xmm4 = _mm_load_si128(&xmm5); xmm0 = _mm_unpacklo_epi8(xmm0,xmm2); xmm1 = _mm_unpacklo_epi8(xmm1,xmm2); xmm4 = _mm_sub_epi16(xmm4,xmm3); xmm1 = _mm_mullo_epi16(xmm1,xmm3); xmm0 = _mm_mullo_epi16(xmm0,xmm4); xmm1 = _mm_add_epi16(xmm1,xmm0); xmm1 = _mm_add_epi16(xmm1,xmm5); xmm1 = _mm_srli_epi16(xmm1,8); xmm1 = _mm_packus_epi16(xmm1,xmm0); eax = _mm_cvtsi128_si32(xmm1); eax |= 0xff000000; } *edi = eax; } } lpSrc = (unsigned char *)lpSrc + lSrcPitch; lpDest = (unsigned char *)lpDest + lDestPitch; } }
見ればわかりますが、命令の本質がまったく変わっていないと思います。(使っているSIMDの順番がそのままになってますし)
ま、これを繰り返す「だけ」なのですが、この「だけ」が非常に面倒だと思います。あらかじめそう組んでおくのが楽ですね・・・。
もちろんコンパイルされれば命令の順番が変わることもあるのでアセンブラコード(というか機械語のコード)でこの順番に命令が並ぶわけではないです。
ちなみに透過率を使って演算を行うところで誤差が入り込む余地があるルーチンですが、その辺は結果がそれほど間違っていなければ
演算の精度よりも高速な処理ができる方を優先するゲーム系のライブラリならではのコードになっています。
どうしてもアセンブラコードで書く必要があるコード
あるかどうかはわかりませんが、このときはインラインアセンブラではなくアセンブラコードファイル(.asm)を組み込む形にすれば無理矢理通すことはできると思います。
が、こんな必要があるか考える前に組み込み命令で対応するように変化する方がいいと思います。
もっと簡単にC#などを使ってそんなことを気にしないで組んでしまう
これは簡単で安全な方法ですね。互換がとりやすい言語で組んだ方がいいと思うときはその方がいいとおもいます。
いつのまにやら64bitOSだけではなく128bitOSとかそんなものまで計画されている時代ですので、64bitへの対応コードが一時しのぎになってしまうこともあり得ます。
それを避ける意味でも中間言語型やインタプリタ型のコードはそれらの影響を受けないので一考してみる価値があります。
ということで今回は64bitコードへの移植とインラインアセンブラの関係についてちょっと詳しく見てみました。あまり意味がないかもしれませんが・・・。
インラインアセンブラが使えなくなるのはVC++だけの話なので
ICCを使うってのが逃げ道としてあったり