🧺

C言語でSIMD命令によりプログラムの高速化を可視化する

に公開

サマリ

単純な加算をする際のSIMD(Single Instruction/Multiple Data)命令との処理時間の比較を行いました。
コードはLLMに書いていただきました。

マシンスペック

MacBook Air M2 arm64

通常のforループにより加算

共通ヘッダ

/* bench_common.h : どちらのソースからもインクルード */

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <mach/mach_time.h>

#define N         100000000        // 1 億要素
#define FLUSH_MB  32               // 32 MB で L1/L2 を追い出し

static inline double ns_since(uint64_t t0)
{
    mach_timebase_info_data_t info;
    mach_timebase_info(&info);
    uint64_t dt = mach_absolute_time() - t0;
    return (double)dt * info.numer / info.denom;   // ns
}

static void flush_dcache(void)
{
    static uint8_t *dummy;
    const size_t SZ = FLUSH_MB * 1024 * 1024;
    if (!dummy) dummy = aligned_alloc(64, SZ);
    for (size_t i = 0; i < SZ; i += 64) dummy[i]++;
}
#include "bench_common.h"

__attribute__((noinline))
void scalar_add(const float *a, const float *b, float *c, size_t n)
{
#pragma clang loop vectorize(disable)          // ★ 自動 SIMD を無効化
    for (size_t i = 0; i < n; ++i)
        c[i] = a[i] + b[i];                    // 素直な a+b
}

int main(void)
{
    float *a = aligned_alloc(16, N * sizeof(float));
    float *b = aligned_alloc(16, N * sizeof(float));
    float *c = aligned_alloc(16, N * sizeof(float));

    for (size_t i = 0; i < N; ++i) { a[i] = (float)i;  b[i] = (float)(N - i); }

    flush_dcache();
    uint64_t t0 = mach_absolute_time();
    scalar_add(a, b, c, N);
    printf("scalar time = %.2f ms\n", ns_since(t0)/1e6);

    free(a); free(b); free(c);
    return 0;
}

コンパイルして実行します。

clang -O3 -arch arm64 norm.c -o norm
./norm

結果は

scalar time = 114.01 ms

SIMD命令による加算

#include "bench_common.h"
#include <arm_neon.h>

__attribute__((noinline))
void neon_add(const float *a, const float *b, float *c, size_t n)
{
    size_t i = 0;
    for (; i + 16 <= n; i += 16) {              // 16 要素一括
        float32x4_t a0 = vld1q_f32(a + i     );
        float32x4_t a1 = vld1q_f32(a + i +  4);
        float32x4_t a2 = vld1q_f32(a + i +  8);
        float32x4_t a3 = vld1q_f32(a + i + 12);

        float32x4_t b0 = vld1q_f32(b + i     );
        float32x4_t b1 = vld1q_f32(b + i +  4);
        float32x4_t b2 = vld1q_f32(b + i +  8);
        float32x4_t b3 = vld1q_f32(b + i + 12);

        vst1q_f32(c + i     , vaddq_f32(a0, b0));
        vst1q_f32(c + i +  4, vaddq_f32(a1, b1));
        vst1q_f32(c + i +  8, vaddq_f32(a2, b2));
        vst1q_f32(c + i + 12, vaddq_f32(a3, b3));
    }
    for (; i < n; ++i)                          // 端数
        c[i] = a[i] + b[i];
}

int main(void)
{
    float *a = aligned_alloc(16, N * sizeof(float));
    float *b = aligned_alloc(16, N * sizeof(float));
    float *c = aligned_alloc(16, N * sizeof(float));

    for (size_t i = 0; i < N; ++i) { a[i] = (float)i;  b[i] = (float)(N - i); }

    flush_dcache();
    uint64_t t0 = mach_absolute_time();
    neon_add(a, b, c, N);
    printf("neon  time  = %.2f ms\n", ns_since(t0)/1e6);

    free(a); free(b); free(c);
    return 0;
}

同様にコンパイルして実行します。

clang -O3 -arch arm64 simd.c -o simd

結果は

neon  time  = 48.31 ms

Discussion