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

SIMD命令を使って高速に計算する


このエントリーをはてなブックマークに追加

このスライドに出てくるSIMD命令を試したくなったので試してみました。

IntelのCPUでSIMD命令を使う場合、SSEという命令セットと、その後継であるAVXという命令セットのどちらかを使うことになると思います。私の手持ちのWindows機に載ってるCPUはCore i7-870とやや古く、SSEしか使えません。

kawa0810 のブログにあるサンプルはAVXを用いたものだったので、コードを改変してSSEでも動くようにしたコードが以下です。Windows 7 + Visual Studio 2013 Expressでビルドできることを確認しています。コードの正当性に自信が持てませんが、とりあえずSSEを使う場合と使わない場合とで結果は一致しています。

#include <stdio.h>
#include <stdlib.h>
#include <immintrin.h>
#include <chrono>
#include <iostream>


void vec_add(const size_t n, float *z, const float *x, const float *y){
    static const size_t single_size = 4; //単精度は4つずつ計算
    const size_t end = n / single_size;

    __m128 *vz = (__m128 *)z;
    __m128 *vx = (__m128 *)x;
    __m128 *vy = (__m128 *)y;

    for (size_t i = 0; i < end; ++i)
        vz[i] = _mm_add_ps(vx[i], vy[i]);
}


void print_elapsed_time(std::chrono::time_point<std::chrono::system_clock>& start,
    std::chrono::time_point<std::chrono::system_clock>& end)
{
    std::cout << "elapsed time = "
        << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
        << " msec."
        << std::endl;
}

int main(void){
    const size_t n = 1024 * 1024 * 64;
    float *x, *y, *z1, *z2;

    //メモリのアライメントを 16byte 境界に揃える
    x = (float *)_mm_malloc(sizeof(float)* n, 16);
    y = (float *)_mm_malloc(sizeof(float)* n, 16);
    z1 = (float *)_mm_malloc(sizeof(float)* n, 16);
    z2 = (float *)_mm_malloc(sizeof(float)* n, 16);

    for (size_t i = 0; i<n; ++i) x[i] = i + 0.2;
    for (size_t i = 0; i<n; ++i) y[i] = i + 1.7;
    for (size_t i = 0; i<n; ++i) z1[i] = 0.0;
    for (size_t i = 0; i<n; ++i) z2[i] = 0.0;

    // SIMDを使って求める
    auto start = std::chrono::system_clock::now();
    vec_add(n, z1, x, y);
    auto end = std::chrono::system_clock::now();
    print_elapsed_time(start, end);

    // 普通に計算する
    start = std::chrono::system_clock::now();
    for (size_t i = 0; i < n; ++i) {
        z2[i] = x[i] + y[i];
    }
    end = std::chrono::system_clock::now();
    print_elapsed_time(start, end);

    // 答えが一致しているかどうか調べる
    for (size_t i = 0; i < n; ++i) {
        if (z1[i] != z2[i]) {
            printf("答が一致していません!");
            exit(1);
        }
    }

    //_mm_malloc で確保した領域は _mm_free で解放する
    _mm_free(x);
    _mm_free(y);
    _mm_free(z1);
    _mm_free(z2);

    return 0;
}

Debugモードでビルドし、実行して処理時間を比較しました。1行目がSIMD命令使用、2行目が使用なしです。

elapsed time = 137 msec.
elapsed time = 305 msec.

SIMD命令を使うことで処理時間は45%程度になっています。

ただし、Releaseモードでビルドすると、以下のように時間に差が出ませんでした。最適化されてしまってるんでしょうか??

elapsed time = 85 msec.
elapsed time = 90 msec.