AVX vs. SSE: expect to see a larger speedup

Viewed 3401

I expected AVX to be about 1.5x faster than SSE. All 3 arrays (3 arrays * 16384 elements *4 bytes/element = 196608 bytes) should fit in L2 cache (256KB) on an Intel Core CPU (Broadwell).

Are there any special compiler directives or flags that I should be using?

Compiler Version

$  clang --version
Apple LLVM version 9.0.0 (clang-900.0.38)
Target: x86_64-apple-darwin16.7.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

Compile line

$  make avx
clang -O3 -fno-tree-vectorize -msse -msse2 -msse3 -msse4.1 -mavx -mavx2 avx.c ; ./a.out 123
n: 123
  AVX Time taken: 0 seconds 177 milliseconds
vector+vector:begin int: 1 5 127 0

  SSE Time taken: 0 seconds 195 milliseconds
vector+vector:begin int: 1 5 127 0

avx.c

#include <stdio.h>
#include <stdlib.h>
#include <x86intrin.h>
#include <time.h>
#ifndef __cplusplus
#include <stdalign.h>   // C11 defines _Alignas().  This header defines alignas()
#endif
#define REPS 50000
#define AR 16384

// add int vectors via AVX
__attribute__((noinline)) 
void add_iv_avx(__m256i *restrict a, __m256i *restrict b, __m256i *restrict out, int N) {

    __m256i *x = __builtin_assume_aligned(a, 32);
    __m256i *y = __builtin_assume_aligned(b, 32);
    __m256i *z = __builtin_assume_aligned(out, 32);

    const int loops = N / 8; // 8 is number of int32 in __m256i
    for(int i=0; i < loops; i++) { 
        _mm256_store_si256(&z[i], _mm256_add_epi32(x[i], y[i]));
    }
}

// add int vectors via SSE; https://en.wikipedia.org/wiki/Restrict
__attribute__((noinline)) 
void add_iv_sse(__m128i *restrict a, __m128i *restrict b, __m128i *restrict out, int N) {

    __m128i *x = __builtin_assume_aligned(a, 16);
    __m128i *y = __builtin_assume_aligned(b, 16);
    __m128i *z = __builtin_assume_aligned(out, 16);

    const int loops = N / sizeof(int);
    for(int i=0; i < loops; i++) { 
        //out[i]= _mm_add_epi32(a[i], b[i]); // this also works
        _mm_storeu_si128(&z[i], _mm_add_epi32(x[i], y[i]));
    } 
}

// printing
void p128_as_int(__m128i in) {
    alignas(16) uint32_t v[4];
    _mm_store_si128((__m128i*)v, in);
    printf("int: %i %i %i %i\n", v[0], v[1], v[2], v[3]);
}

__attribute__((noinline)) 
void debug_print(int *h) {
    printf("vector+vector:begin ");
    p128_as_int(* (__m128i*) &h[0] );
}

int main(int argc, char *argv[]) {
    int n = atoi (argv[1]);
    printf("n: %d\n", n);

    int *x,*y,*z;
    if (posix_memalign((void**)&x, 32, 16384*sizeof(int))) { free(x); return EXIT_FAILURE; }
    if (posix_memalign((void**)&y, 32, 16384*sizeof(int))) { free(y); return EXIT_FAILURE; }
    if (posix_memalign((void**)&z, 32, 16384*sizeof(int))) { free(z); return EXIT_FAILURE; }
    x[0]=0; x[1]=2; x[2]=4;
    y[0]=1; y[1]=3; y[2]=n;

    // touch each 4K page in x,y,z to avoid copy-on-write optimizations
    for (int i=512; i<AR; i+= 512) { x[i]=1; y[i]=1; z[i]=1; }

    // warmup
    for(int i=0; i<REPS; ++i) { add_iv_avx((__m256i*)x, (__m256i*)y, (__m256i*)z, AR); }
    // AVX
    clock_t start = clock();
    for(int i=0; i<REPS; ++i) { add_iv_avx((__m256i*)x, (__m256i*)y, (__m256i*)z, AR); }
    int msec = (clock()-start) * 1000 / CLOCKS_PER_SEC;
    printf("  AVX Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000);
    debug_print(z);

    // warmup
    for(int i=0; i<REPS; ++i) { add_iv_sse((__m128i*)x, (__m128i*)y, (__m128i*)z, AR); }
    // SSE
    start = clock();
    for(int i=0; i<REPS; ++i) { add_iv_sse((__m128i*)x, (__m128i*)y, (__m128i*)z, AR); }
    msec = (clock()-start) * 1000 / CLOCKS_PER_SEC;
    printf("\n  SSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000);
    debug_print(z);

    return EXIT_SUCCESS;
}
1 Answers

The problem is that that your data doesn't fit in the L1 cache. The L1 bandwidth of Broadwell is much larger than the L2 bandwidth. The L1 bandwidth is large enough to load two 32 byte vectors every cpu cycle. So, a better AVX vs. SSE speedup might be expected if your data set is much smaller. However, note that the combined L1 read/write bandwidth is less than 2*32(r)+32(w)=96 bytes per cycle. In practice 75 bytes per cycle is possible, see here.

The second graph on this page shows that indeed the L2 bandwidth is much smaller: At Test_block_size=128KB (=32KB per core) the bandwidth is 900GB/s. At Test_block_size=1MB (=256KB per core) the bandwidth is only 300GB/s. (Note that Haswell 4770k has more or less the same L1 and L2 cache architecture as Broadwell.)

Try to reduce AR to 2000 and to increase NREP to 1000000 and see what happens with the SSE vs. AVX speedup.

Related