2012-07-21

SSE4.2 の crc32c 命令の呼び出し

SSE4.2 に追加された crc32c 用の機械語命令を使うのに必要そうな機能をメモ書き。 ちなみに、SSE4.2 対応の CPU を持ってないので、未検証の項目多い。

SSE4.2対応のCPU

  • Intel系はNehalemマイクロアーキテクチャ以降
  • AMD系はBulldozerマイクロアーキテクチャ以降
Wikipedia によるとNehalemマイクロアーキテクチャは「 主に2008年〜2011年ごろに発売された。」と書いてあるので、最近のデスクトップ用のCPUならば大丈夫?

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)
SSE4.2 対応のCPUを持ってないので、実機での試験はしてないが、以下のコードでOKなのではないかと思う。
#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;
}
アライメントを気にした処理をしているが、アライメントを揃える必要が本当にあるかは不明...。Intel の Optimization Reference マニュアルの CRC32 のサンプルはアライメントを気にしてないので、アライメントを揃える必要はなさそう。
ちなみに 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 のような重い関数の場合、関数内部の負荷と比べると、関数呼び出し部分の微細な最適化はあまり意味がないような気がしてきた。

No comments:

Post a Comment