Movatterモバイル変換


[0]ホーム

URL:


MUGI COM

この広告は、90日以上更新していないブログに表示しています。

SIMD演算

C言語でSSEやSSE2を使う方法について。ポイントがいくつかあります。

  • xmmintrin.hやemmintrin.hをインクルードする

 SSE命令のみならxmmintrin.h、SSE2命令も使うならemmintrin.h

  • SIMD命令でアクセスするメモリは16バイトアラインメントにする

 gccの場合、_mm_malloc関数を使えばよい。

  • MMX命令はAMDのCPUでは使えない

Pentium4以降に限定されますが、SSEやSSE2を使うのが良いでしょう。


これらを踏まえて、画像の各ピクセルのRGB値を反転させる例を示します。
ピクセルは8bitのRGB値が並んでいるものとします。8bitの整数演算を行うので、8bit計算を16個並列で行うSSE2命令を使用します。


最初はSSE2のインクルードファイル。

#include <emmintrin.h>


つづいて16バイトアラインメントれたメモリを確保するために、_mm_mallocを使ってメモリを確保します。

  data = _mm_malloc(SIZE,sizeof(__m128i));

__m128iは128bit(=16バイト)のデータをあらわす型です。


ピクセルの反転は255-R, 255-G,255-Bで行います。SSE2なので8bitの16個並列引き算命令が使えます。n1を255、*pInは各ピクセルのR,G,B値とすると以下のようになります。

  _mm_subs_epu8 (n1 , *pIn)


コンパイルgccの場合 -msse2オプションをつけます。

  gcc -O2 -msse2 img_inv.c


以下のソースでそれぞれの処理を100回ループした結果は以下のとおり。SSEが速い。でも、16個同時だからもっと速くなると思ったけどこのくらいなのね。

通常     2.0[秒]SSE2使用 1.5[秒]


以下はソースコードhttp://www.codeproject.com/KB/recipes/mmxintro.aspxを参考にしました。

/*  gcc -O2 -msse2 img_inv.c */#include <stdio.h>#include <time.h>#include <emmintrin.h>void InvertImage(unsigned char* pSource,                 unsigned char* pDest,                 int nNumberOfPixels){  int i;  for(i = 0; i < nNumberOfPixels; i++ ) {    *pDest = 255 - *pSource;    pDest++;    pSource++;  }}void InvertImage_SSE2(unsigned char* pSource,                     unsigned char* pDest,                     int nNumberOfPixels){  int nLoop = nNumberOfPixels/sizeof(__m128i);  int i;  /* ポインタとして扱う */  __m128i* pIn  = (__m128i*) pSource;  __m128i* pOut = (__m128i*) pDest;   /* r0-r16のレジスタに255を設定 */  __m128i n1 = _mm_set1_epi8 (255);  for(i = 0; i < nLoop; i++ ) {    *pOut = _mm_subs_epu8 (n1 , *pIn);  /* 255 - *pIn*/    /*次の16バイト*/    pIn++;    pOut++;  }}long diffMillis(struct timespec* t1, struct timespec* t2){  long diff = 0;  diff = (t1->tv_sec - t2->tv_sec) * 1000;  diff = diff + (t1->tv_nsec - t2->tv_nsec) / 1000000;  return diff;}#define SIZE 3*3200*2400int main(void){  struct timespec t1, t2;     unsigned char *data;  unsigned char *out;  /* 16バイトアラインメントでメモリを確保 */  data = _mm_malloc(SIZE,sizeof(__m128i));  out  = _mm_malloc(SIZE,sizeof(__m128i));  clock_gettime(CLOCK_REALTIME, &t1);  InvertImage(data,out,SIZE);  clock_gettime(CLOCK_REALTIME, &t2);  printf("InvertImage=%d\n",diffMillis(&t2,&t1));  clock_gettime(CLOCK_REALTIME, &t1);  InvertImage_SSE2(data,out,SIZE);  clock_gettime(CLOCK_REALTIME, &t2);  printf("InvertImage_SSE2=%d\n",diffMillis(&t2,&t1));}

アセンブラコード
gcc -Sでアセンブラのコードを見てみました。
まずは、通常計算。あれ?notbで計算してる!反転命令使ってるじゃん。恐るべし最適化。

L5:movzbl(%ebx), %eaxaddl$1, %ebxnotb%almovb%al, (%ecx)addl$1, %ecxsubl$1, %edxjneL5

つづいてSSE2。こちらはコードに書いたとおり_mm_subs_epu8がpsubusbとなっている。通常の演算がnotbを使ってたんで、XORを計算する_mm_xor_si128を使ってみたけど、実行速度は変わらず。

L15:movdqa%xmm1, %xmm0psubusb(%ebx), %xmm0addl$16, %ebxmovdqa%xmm0, (%ecx)addl$16, %ecxsubl$1, %eaxjneL15

SSE2使用で1.5秒はあんまり速くない気がする。このデータ量だと512Mピクセル/sの計算を行っていることになる。メモリはread/write行うので 512×3×2=3G/s の帯域を使っている。このくらいが限界なのか?
CPUキャッシュに入るくらいのデータ量にすると、SSE2使用が3Gピクセル/s、通常計算が390Mピクセル/sとなり7倍程度の差が出た。メモリ帯域がネックのようだ。


ちょっと思い当たって以下のところを見直してみた。

*pOut = _mm_subs_epu8 (n1 , *pIn);  /* 255 - *pIn*/

これ、元データを破壊していいなら以下のように書ける

*pIn = _mm_subs_epu8 (n1 , *pIn);  /* 255 - *pIn*/

こっちの方がキャッシュが効くので速くないか?と思って試したら、かなり速くなった。

通常     2.0[秒]SSE2使用 1.2[秒]

もうちょっと調べたらSSE2にはキャッシュにデータを残さないでメモリに転送する命令があった。これを使ったコードは以下のとおり。

n2 = _mm_subs_epu8(n1, *pIn);  /* 255 - *pIn*/_mm_stream_si128(pIn,n2);

結果としては 1.2[秒]→0.9[秒]に! SSEは奥が深いな。

検索

引用をストックしました

引用するにはまずログインしてください

引用をストックできませんでした。再度お試しください

限定公開記事のため引用できません。

読者です読者をやめる読者になる読者になる

[8]ページ先頭

©2009-2025 Movatter.jp