SSE4.2対応のCPU
- Intel系はNehalemマイクロアーキテクチャ以降
- AMD系はBulldozerマイクロアーキテクチャ以降
SSE4.2対応のCPUかどうかのチェック
(2012-11-03 大幅書き換え)当然だが、SSE4.2 に対応してないCPUでは crc32c 機械語命令は使えないので、事前に SSE4.2 に対応しているかどうかをチェックする必要がある。
以下の関数 sse4_2_is_supported() を呼び出して、戻り値が 1 ならばSSE4.2対応、0の場合は非対応。
#define CPUID_ECX_BIT_SSE4_2 (1u << 20)
/* GNU C Compiler */
#if defined __GNUC__
#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3)
#include <cpuid.h>
/* gcc version >= 4.3 */
int sse4_2_is_supported(void)
{
unsigned int eax, ebx, ecx, edx;
__cpuid(1, eax, ebx, ecx, edx);
return (ecx & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#else
/* gcc version < 4.3 */
int sse4_2_is_supported(void)
{
unsigned int ecx;
#if defined(__i386__) && defined(__PIC__)
__asm("movl $1, %%eax;"
"pushl %%ebx;"
"cpuid;"
"popl %%ebx;"
: "=c" (ecx)
:
: "eax", "edx");
#else
__asm("movl $1, %%eax;"
"cpuid;"
: "=c" (ecx)
:
: "eax", "ebx", "edx");
#endif
return (ecx & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#endif
/* Microsoft Visual C++ */
#elif defined _MSC_VER
#if _MSC_VER >= 1400
#include <intrin.h>
/* msvc version >= 2005 */
int sse4_2_is_supported(void)
{
int cpuinfo[4];
__cpuid(cpuinfo, 1);
return (cpuinfo[2] & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#else
/* msvc version < 2005 */
int sse4_2_is_supported(void)
{
unsigned int c;
__asm {
mov eax, 1
cpuid
mov c, ecx
}
return (c & CPUID_ECX_BIT_SSE4_2) ? 1 : 0;
}
#endif
#else
#error unsupported compiler
#endif
crc32c命令の呼び出し
(2012-11-03 大幅書き換え)gcc 4.3以上、icc 12.0 以上、Visual C++ 2008 以上ならば、 以下のIntrinsicsが使える。
- unsigned int _mm_crc32_u8 (unsigned int crc, unsigned char v);
- unsigned int _mm_crc32_u16 (unsigned int crc, unsigned short v);
- unsigned int _mm_crc32_u32 (unsigned int crc, unsigned int v);
- unsigned long long _mm_crc32_u64 (unsigned long long crc, unsigned long long v);(Linux)
- unsigned __int64 _mm_crc32_u64 (unsigned __int64 crc, unsigned __int64 v);(Windows)
#include <nmmintrin.h>
unsigned int crc32c_sse4_2(unsigned int crc32c, const char *buffer, size_t length)
{
size_t quotient;
#if 0 /* 未検証だがアライメントを揃える必要はなさそう。 */
/* buffer の先頭部分のアライメントに沿ってない部分の処理 */
if ((((size_t)buffer) & 1) && length >= 1) {
crc32c = _mm_crc32_u8(crc32c, *(unsigned char*)buffer);
buffer += 1;
length -= 1;
}
if ((((size_t)buffer) & 2) && length >= 2) {
crc32c = _mm_crc32_u16(crc32c, *(unsigned short*)buffer);
buffer += 2;
length -= 2;
}
#if defined(__x86_64) || defined(_M_X64)
if ((((size_t)buffer) & 4) && length >= 4) {
crc32c = _mm_crc32_u32(crc32c, *(unsigned int*)buffer);
buffer += 4;
length -= 4;
}
#endif
#endif
/* アライメントに沿っている部分の処理 */
quotient = length / sizeof(size_t);
while (quotient--) {
#if defined(__x86_64) || defined(_M_X64)
crc32c = _mm_crc32_u64(crc32c, *(size_t*)buffer);
#else
crc32c = _mm_crc32_u32(crc32c, *(size_t*)buffer);
#endif
buffer += sizeof(size_t);
}
/* buffer の後ろの部分のアライメントに沿ってない部分の処理 */
#if defined(__x86_64) || defined(_M_X64)
if (length & 4) {
crc32c = _mm_crc32_u32(crc32c, *(unsigned int*)buffer);
buffer += 4;
}
#endif
if (length & 2) {
crc32c = _mm_crc32_u16(crc32c, *(unsigned short*)buffer);
buffer += 2;
}
if (length & 1) {
crc32c = _mm_crc32_u8(crc32c, *(unsigned char*)buffer);
}
return crc32c;
}
アライメントを気にした処理をしているが、ちなみに gcc の場合コンパイルオプションに -msse4.2 を付ける必要がある。
SSE4.2の有無により呼び出す関数を分ける方法
例えば SSE4.2 があるときはcrc32c_sse4_2()を呼び出して、SSE4.2 がないときは C で実装した関数crc32c_implemented_by_c()を呼び出したい場合、以下のように関数ポインタを使用することが多い。
unsigned int (*crc32c)(unsigned int crc32, const char *p, size_t len);
int main()
{
...初期化時に関数ポインタ crc32c に実際に呼び出す関数を設定 ...
if (sse4_2_is_supported()) {
crc32c = crc32c_sse4_2;
} else {
crc32c = crc32c_implemented_by_c;
}
...
crc = crc32c(crc, buf, buflen); // crc32c関数の呼び出し
}
最近の gcc ならば、 ifunc というアトリビュートがあり、これを使うとプログラムのロード時にどの関数を呼び 出すかをチェックできる。
// この関数はプログラムのロード時、constructor の呼び出し前に実行される。
unsigned int (*resolve_crc32c(void))(unsigned int crc32, const char *p, size_t len)
{
if (sse4_2_is_supported()) {
return crc32c_sse4_2;
} else {
return crc32c_implemented_by_c;
}
}
unsigned int crc32c(unsigned int crc32, const char *p, size_t len) __attribute__ ((ifunc ("resolve_crc32c")));
int main()
{
...
crc = crc32c(crc, buf, buflen); // crc32c関数の呼び出し
}
で、出力されたアセンブリを見てみると、、、crc32c のような重い関数の場合、関数内部の負荷と比べると、関数呼び出し部分の微細な最適化はあまり意味がないような気がしてきた。