64bitコードへの移植心構え その2

で、昨日書いた文章にちょっと予定外の抜けがあったのでその部分を追加しておきます。

さらに追加する可能性もあるので「その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コードへの移植とインラインアセンブラの関係についてちょっと詳しく見てみました。あまり意味がないかもしれませんが・・・。

One thought on “64bitコードへの移植心構え その2

  1. 通りすがり

    インラインアセンブラが使えなくなるのはVC++だけの話なので
    ICCを使うってのが逃げ道としてあったり

    返信

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

この記事のトラックバック用URL