CPUIDの処理もいろいろと変わったのね

まあ、知ってはいましたが。

VisualStudioのバージョンを2010に変更したことでCPUIDを参照するための組み込み命令も拡張されましたのでそれの対応処理をいろいろと書いていました。そのための資料も一新したのでちょっと書いてみたいと思います。

 

まずは基礎。CPUIDとは

で、何も知らない人のためにCPUIDの説明から。CPUIDとはx86系のCPUが持つ命令の一つで、CPUをある状態にして実行することで対象のCPUが持つ様々な情報を得ることができる命令です。特に昔(10年ほど前)だとADVシステムを作成するときにCPUの拡張命令であるMMXやSSE2を持っているかどうかという情報がゲームの実行速度(特に描画関係)に大きな影響を与えるために拡張命令が使用できるかどうかを調べて命令を切り替える、という作業が必要であったり、サポートのために実行しているPCの実行環境を出すために必要な命令として使用していたものです。

x86と書いているのでIntelのCPUでもAMDのCPUでも実装していますし、x86-64環境でも動作する命令です。もちろんこれはCPU上で直接実行する命令ですので組み込み命令を記述できるプログラミング言語でしか直接の実行はできません。CPUIDの情報を受け取るような別の処理を持っているプログラム言語もあるかもしれませんがそれはまあそれということで。

今日ではほとんど話題にもならなくなってしまった命令でもあります。そこまで拡張命令にこだわるとすればCPUを使った科学技術計算や動画のエンコードなどCPUの処理を限界まで引き出すときに対象の命令が有効かどうかを調べたりCPU-Zなどで自分のCPUを調べるときにCPU-Z上で実行される、という形でしか見ることは無いと思います。

ちなみにこの命令は実行しているCPUがIntel製かAMD製か、などを直接判別するために使われる命令でもあります。そのため、CPUIDの結果はCPUの種類ごとにかなり異なったものとなる場合分けがとんでもなく必要な命令だったりします。CPUID命令実行時にどのような処理結果となるかを直接知りたい人は各社が公開している資料を見てみるといいと思います。Intelの場合は「Intel 64 and IA-32 Architectures Software Developer’s Manual 2A」に、AMDの場合は「AMD64 Architecture Programmer’s Manual Volume 3 General-Purpose and System Instructions」に記述があります。

 

命令のサポートフラグがいろんな場所に・・・

本当にいろんなところにフラグがあるんです・・・。CPUが実行可能な命令を取得するためにCPUIDを実行するわけですが、この実行手順が異様なほど面倒。今だとAVX2が実行可能であるかを調べる手順として考えられますが、どんな感じか見てみましょう。ここ数年で発売されているx86命令をサポートするCPUでは実装がほぼ当たり前となっているようなx86-64の動作フラグなどもこんな方法でチェックされます。なお、この手順では「CPUID命令は実行可能である」という条件を入れてあります。昔はこの条件がないことがありテストコードも書いていたものですが、今更そんなものもいらないでしょう。というわけで以下が手順。基本的にCPUIDで入出力するのは32bit値なのでそれに従います。

なお、もしインラインアセンブラなどで命令を記述する場合、使うレジスタはeax,ebx,ecx,edxの4つです。VisualStudioの組み込み命令の場合、第一引数に32bit変数x4の配列(命令が終了したとき、配列にはそれぞれeax,ebx,ecx,edxの値が返る)、第二引数にeaxに与える命令の初期値、__cpuidexの場合は第三引数にecxに与える命令の初期値、となります。

  1. eax=0としてcpuidを実行。eaxに基本情報として取得できるcpuidの最大値、ebx,edx,ecxの順番にCPUの制作元文字列(ASCII)が返る。
  2. 基本情報に1以上が含まれない場合はこれで終了。
  3. eax=0x80000000としてcpuidを実行。eaxに拡張情報として取得できるcpuidの最大値が返る。なお、eaxに(符号なしとして)0x80000000を超える値が返らないときは拡張情報は無し。
  4. eax=1としてcpuidを実行。eaxにCPUのバージョン情報、edxに拡張命令フラグその1、ecxに拡張命令フラグその4が返る。
  5. 拡張情報がない場合はこれで終了
  6. eax=0x80000001としてcpuidを実行。edxに拡張命令フラグその2、ecxに拡張命令フラグその3が返る
  7. eax=0x80000002~0x80000004まではCPUの名前情報が返る。
  8. 基本情報に7以上が含まれない場合はこれで終了。
  9. eax=7、ecx=0としてcpuidを実行。eaxにeax=7としたときにecxに与えることができる最大値、ebxに拡張命令フラグその5が返る。

とまあ、こんな感じになります。ちょっとおもしろいのが拡張命令フラグの順番ですよね。なぜこうなってしまったか?はフラグが何を指しているかを見ればわかります。

フラグ種類 拡張命令フラグとしてよく確認される機能
 拡張命令フラグその1  TSC(TimeStampCounter),MMX,SSE,SSE2,SMT(HyperThreading)
 拡張命令フラグその2   3DNow!,3DNow! Extensions, AMD64(IntelのCPUではIntel64)
 拡張命令フラグその3  SSE4A,FMA4
 拡張命令フラグその4  SSE3,SSSE3,SSE4.1,SSE4.2,AESNI,AVX
 拡張命令フラグその5  AVX2

見てわかるとおり、その2とその3はAMDが先行して取り入れた機能に関するフラグになっています。そのため、Intel側の資料を見るとフラグの状態が微妙になってしまっています。このあたりはCPUの開発競争によるものなのでこういうこともある、というわけですね。フラグの順序をこのようにしたわけもこれです。

ちなみに、今後増えるとしたら拡張命令フラグその5と同じような取得の方法の場所に増えていくのだと思われます。そのためにわざわざ枝番なんてものを使って取得するようになっているのですからね。

 

ちなみにコードにすると

こんな感じになります。

#include <intrin.h>

#define CPUCMDSET_TSC		0x00000001
#define CPUCMDSET_MMX		0x00000002
#define CPUCMDSET_SSE		0x00000004
#define CPUCMDSET_SSE2		0x00000008
#define CPUCMDSET_3DNOW		0x00000010
#define CPUCMDSET_3DNOWEXT	0x00000020
#define CPUCMDSET_SMT		0x00000040
#define CPUCMDSET_64BIT		0x00000080
#define CPUCMDSET_SSE3		0x00000100
#define CPUCMDSET_SSSE3		0x00000200
#define CPUCMDSET_SSE4A		0x00001000
#define CPUCMDSET_FMA4		0x00002000
#define CPUCMDSET_SSE41		0x00004000
#define CPUCMDSET_SSE42		0x00008000
#define CPUCMDSET_AESNI		0x00010000
#define CPUCMDSET_AVX		0x00040000
#define CPUCMDSET_AVX2		0x00080000

unsigned int cpuidcheck(char vendor[16],char name[64])
{
	int cpuinfo[4]; int *ptr; unsigned int flag; int basicmax,extendmax,structuredmax; int i;

	//初期化
	flag = 0x00000000; basicmax = extendmax = structuredmax = 0;

	//基本命令領域長およびCPUVendorの取得
	__cpuid(cpuinfo,0x00000000);
	basicmax = cpuinfo[0]; ptr = (int *)(&vendor[0]);
	*ptr++ = cpuinfo[1]; *ptr++ = cpuinfo[3]; *ptr++ = cpuinfo[2]; *ptr = 0;
	
	//拡張情報領域長の取得
	__cpuid(cpuinfo,0x80000000);
	extendmax = cpuinfo[0];

	//拡張命令フラグその1
	__cpuid(cpuinfo,0x00000001);
	if(cpuinfo[3] & (1 << 4)) flag |= CPUCMDSET_TSC;
	if(cpuinfo[3] & (1 << 23)) flag |= CPUCMDSET_MMX;
	if(cpuinfo[3] & (1 << 25)) flag |= CPUCMDSET_SSE;
	if(cpuinfo[3] & (1 << 26)) flag |= CPUCMDSET_SSE2;
	if(cpuinfo[3] & (1 << 28)) flag |= CPUCMDSET_SMT;

	//拡張命令フラグその4
	if(cpuinfo[2] & (1 << 0)) flag |= CPUCMDSET_SSE3;
	if(cpuinfo[2] & (1 << 9)) flag |= CPUCMDSET_SSSE3;
	if(cpuinfo[2] & (1 << 19)) flag |= CPUCMDSET_SSE41;
	if(cpuinfo[2] & (1 << 20)) flag |= CPUCMDSET_SSE42;
	if(cpuinfo[2] & (1 << 25)) flag |= CPUCMDSET_AESNI;
	if(cpuinfo[2] & (1 << 28)) flag |= CPUCMDSET_AVX;

	//拡張領域がない場合は以降を行わない
	if((unsigned int)extendmax < (unsigned int)0x800000000) return flag;

	//拡張命令フラグその2
	__cpuid(cpuinfo,0x80000001);
	if(cpuinfo[3] & (1 << 31)) flag |= CPUCMDSET_3DNOW;
	if(cpuinfo[3] & (1 << 30)) flag |= CPUCMDSET_3DNOWEXT;
	if(cpuinfo[3] & (1 << 29)) flag |= CPUCMDSET_64BIT;

	//拡張命令フラグその3
	if(cpuinfo[2] & (1 << 6)) flag |= CPUCMDSET_SSE4A;
	if(cpuinfo[2] & (1 << 16)) flag |= CPUCMDSET_FMA4;
	
	//CPUNameの取得
	ptr = (int *)(&name[0]);
	__cpuid(cpuinfo,0x80000002); for(i = 0;i < 4;i++){ *ptr++ = cpuinfo[i]; }
	__cpuid(cpuinfo,0x80000003); for(i = 0;i < 4;i++){ *ptr++ = cpuinfo[i]; }
	__cpuid(cpuinfo,0x80000004); for(i = 0;i < 4;i++){ *ptr++ = cpuinfo[i]; }

	//基本領域がない場合は以降を行わない
	if(basicmax < 7) return flag;

	//拡張命令フラグその5
	__cpuidex(cpuinfo,7,0);
	structuredmax = cpuinfo[0];
	if(cpuinfo[1] & (1 << 5)) flag |= CPUCMDSET_AVX2;

	return flag;
}

最後の__cpuidexの組み込み命令がVS2005にはなかったので実装できなかったのですが、今ならばできるわけですね。

 

でも今だとCPUIDを使ってそこまで確認することの意味って薄いよね

組み替えを行っていて思ったことだったりします。一応組んではいますが、ADVゲームレベルでこれだけ調べてもあまり意味はありませんし、SSE2が使えれば十分、という速度まで処理を引き上げないとどうかと思いますし。

 

コメントを残す

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

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