AVX, AVX2の関数を使うには、至って簡単で、ヘッダファイル(immintrin.h)をインクルードするのみで、使えるようになります。
#include <immintrin.h>
AVX, AVX2で処理を行うデータは、通常、PCのメインメモリに格納されているかと思いますが、このデータをレジスタと呼ばれる高速で処理を行える領域にデータを読み込み、処理を行います。
このレジスタの領域でAVX, AVX2の処理を行い、画像処理などのように大きいメモリのデータを処理する場合は、このレジスタ上で行われた処理結果を、再度、PCのメインメモリへ書き込む必要があります。
メモリからレジスタへ読込
↓
AVX,AVX2処理
↓
レジスタからメモリへ書き込み
AVX命令の多くは、128bit(16byte)で処理を行うので、メインからのデータの読み書きは16バイトごとに行います。
同様にAVX2命令の多くは、256bit(32byte)で処理を行うので、32バイトごとに行います。
この簡単なサンプルを以下に示します。
アライメントされていないメモリの処理
#include <stdio.h>
#include <immintrin.h> // AVX, AVX2を使うには、これをインクルードする
int main() {
int len = 64;
// メモリの確保(確保されたメモリはアライメントされていない)
unsigned char* src1 = (unsigned char*)malloc(len * sizeof(unsigned char));
unsigned char* src2 = (unsigned char*)malloc(len * sizeof(unsigned char));
unsigned char* dst = (unsigned char*)malloc(len * sizeof(unsigned char));
// 評価用データの代入
for (int i = 0; i < len; i++) {
src1[i] = i;
src2[i] = i * 2;
}
// AVX処理
for (int i = 0; i < len; i += 16) { // 16バイトごとに行う
// メモリからレジスタへデータの読込
__m128i a = _mm_loadu_si128((__m128i*)(src1 + i));
__m128i b = _mm_loadu_si128((__m128i*)(src2 + i));
// AVX処理(a と b を足す)
__m128i c = _mm_add_epi8(a, b);
// レジスタからメモリへ書き込み
_mm_storeu_si128((__m128i*)(dst + i), c);
}
// 結果の表示
for (int i = 0; i < len; i++) {
printf("%d, %d, %d\n", src1[i], src2[i], dst[i]);
}
// 処理結果
//0, 0, 0
//1, 2, 3
//2, 4, 6
//3, 6, 9
//4, 8, 12
//5, 10, 15
//6, 12, 18
//7, 14, 21
// 以下、省略
// メモリの解放
free(src1);
free(src2);
free(dst);
}
上記のサンプルでは、malloc関数でメモリを確保し、AVXの_mm_loadu_si128関数で、メモリからレジスタへデータを読み込みしています。
今回は_mm_add_epi8関数で、8bitごとのデータの足し算を行い、処理結果を_mm_storeu_si128関数でレジスタからメモリへ戻しています。
malloc関数で確保したメモリは、アドレスがアライメントされているとは限らず、アライメントされていないメモリからレジスタへ読込を行うには、関数名に u の付いた _mm_loadu_si128 などを用います。
同様に、レジスタからメモリへの書き込みは _mm_storeu_si128 などを用います。
AVX, AVX2の処理では、メモリのアライメントを行うと、より高速に読込/書き込みを行うことができ、アライメントされたメモリを確保するには Windowsの場合、_aligned_malloc関数を用います。
アライメントされたメモリの処理
#include <stdio.h>
#include <immintrin.h> // AVX, AVX2を使うには、これをインクルードする
int main() {
int len = 64;
// メモリの確保(確保されたメモリはアライメントされている)
unsigned char* src1 = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);
unsigned char* src2 = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);
unsigned char* dst = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);
// 評価用データの代入
for (int i = 0; i < len; i++) {
src1[i] = i;
src2[i] = i * 2;
}
// AVX処理
for (int i = 0; i < len; i += 16) { // 16バイトごとに行う
// メモリからレジスタへデータの読込
__m128i a = _mm_load_si128((__m128i*)(src1 + i));
__m128i b = _mm_load_si128((__m128i*)(src2 + i));
// AVX処理(a と b を足す)
__m128i c = _mm_add_epi8(a, b);
// レジスタからメモリへ書き込み
_mm_store_si128((__m128i*)(dst + i), c);
}
// 結果の表示
for (int i = 0; i < len; i++) {
printf("%d, %d, %d\n", src1[i], src2[i], dst[i]);
}
// 処理結果
//0, 0, 0
//1, 2, 3
//2, 4, 6
//3, 6, 9
//4, 8, 12
//5, 10, 15
//6, 12, 18
//7, 14, 21
// 以下、省略
// 解放
_aligned_free(src1);
_aligned_free(src2);
_aligned_free(dst);
}
アライメントされたメモリの確保をしているのは
unsigned char* src1 = (unsigned char*)_aligned_malloc(len * sizeof(unsigned char), 32);
の部分で、_aligned_malloc関数の第二引数に32を指定すると、メモリのアドレスが32バイト境界にアライメントされたメモリを確保する事ができます。
AVXの関数(128bit処理)を使用するには16(32でも可)、AVX2の関数(256bit処理)では32を指定する必要があります。
_aligned_mallocで確保したメモリは_aligned_freeで解放する必要があります。
注意点
アライメントされていないメモリに対して_mm_load_si128関数や_mm_store_si128関数を使うと、プログラムが落ちます。
ただし、malloc関数などを使ってメモリを確保したとき、アライメントがたまたま合っているアドレスで確保される事も多いので、アライメントの対応を間違っていても、プログラムとしては動く場合もあります。
そのため、たまにプログラムが落ちる場合には、まずは、このアライメント周りを疑った方が良いかと思います。
コメント