2017-10-16 6 views
0

Ich würde erwarten, dass SSE schneller ist als SSE nicht. Muss ich einige zusätzliche Compiler-Flags hinzufügen? Könnte es sein, dass ich keine Beschleunigung sehe, weil das Integer-Code ist und nicht Fließkomma?SSE: Keine Beschleunigung durch Verwendung von _mm_add_epi32

Aufruf/Ausgabe

$ make sum2 
clang -O3 -msse -msse2 -msse3 -msse4.1 sum2.c ; ./a.out 123 
n: 123 
    SSE Time taken: 0 seconds 124 milliseconds 
vector+vector:begin int: 1 5 127 0 
vector+vector:end int: 0 64 66 68 
NOSSE Time taken: 0 seconds 115 milliseconds 
vector+vector:begin int: 1 5 127 0 
vector+vector:end int: 0 64 66 68 

Compiler

$ clang --version 
Apple LLVM version 9.0.0 (clang-900.0.37) 
Target: x86_64-apple-darwin16.7.0 
Thread model: posix 

sum2.c

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

// add vector and return resulting value on stack 
__attribute__((noinline)) __m128i add_iv(__m128i *a, __m128i *b) { 
    return _mm_add_epi32(*a,*b); 
} 

// add int vectors via sse 
__attribute__((noinline)) void add_iv_sse(__m128i *a, __m128i *b, __m128i *out, int N) { 
    for(int i=0; i<N/sizeof(int); i++) { 
     //out[i]= _mm_add_epi32(a[i], b[i]); // this also works 
     _mm_storeu_si128(&out[i], _mm_add_epi32(a[i], b[i])); 
    } 
} 

// add int vectors without sse 
__attribute__((noinline)) void add_iv_nosse(int *a, int *b, int *out, int N) { 
    for(int i=0; i<N; i++) { 
     out[i] = a[i] + b[i]; 
    } 
} 

__attribute__((noinline)) 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]); 
} 

// print first 4 and last 4 elements of int array 
__attribute__((noinline)) void debug_print(int *h) { 
    printf("vector+vector:begin "); 
    p128_as_int(* (__m128i*) &h[0]); 
    printf("vector+vector:end "); 
    p128_as_int(* (__m128i*) &h[32764]); 
} 

int main(int argc, char *argv[]) { 
    int n = atoi (argv[1]); 
    printf("n: %d\n", n); 
    // sum: vector + vector, of equal length 
    int f[32768] __attribute__((aligned(16))) = {0,2,4}; 
    int g[32768] __attribute__((aligned(16))) = {1,3,n}; 
    int h[32768] __attribute__((aligned(16))); 
    f[32765] = 33; f[32766] = 34; f[32767] = 35; 
    g[32765] = 31; g[32766] = 32; g[32767] = 33; 

    // https://stackoverflow.com/questions/459691/best-timing-method-in-c 
    clock_t start = clock(); 
     for(int i=0; i<CYCLE_COUNT; ++i) { 
      add_iv_sse((__m128i*)f, (__m128i*)g, (__m128i*)h, 32768); 
     } 
    int msec = (clock()-start) * 1000/CLOCKS_PER_SEC; 
    printf(" SSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000); 
    debug_print(h); 

    // process intense function again 
    start = clock(); 
     for(int i=0; i<CYCLE_COUNT; ++i) { 
      add_iv_nosse(f, g, h, 32768); 
     } 
    msec = (clock()-start) * 1000/CLOCKS_PER_SEC; 
    printf("NOSSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000); 
    debug_print(h); 

    return EXIT_SUCCESS; 
} 
+0

Auch Ihr Kommentar zu 'add_iv' (den Sie glücklicherweise nie verwenden) ist falsch: Ein' __m128i' Rückgabewert wird in XMM0 in der Aufrufkonvention x86-64 System V zurückgegeben, nicht auf dem Stapel. –

+0

Danke Peter! Gibt es eine Möglichkeit zu verhindern, dass der Compiler SSE-Anweisungen in bestimmten Blöcken verwendet? – AG1

+0

Ich aktualisierte meine Antwort mit einer Perfomanalyse des auto-vektorisierten Codes gegenüber Ihrer manuell-vektorisierten Schleife. Sie haben beide eine Menge Overhead, aber ich denke, dass Manual schneller hätte sein sollen, es sei denn, 4k-Aliasing würde seine Bandbreite beeinträchtigen. Vielleicht machen Turbo-Effekte den 2. Loop weniger wall-clock time, auch wenn es mehr CPU-Zyklen benötigt, oder vielleicht gibt es einen anderen Effekt. –

Antwort

4

Blick auf die ASM: Klappern -O2 oder -O3 wahrscheinlich auto-vektorisiert add_iv_nosse (mit einer Überprüfung auf Überlappung, seit Sie nicht verwendet int * restrict a und so weiter).

Verwenden Sie -fno-tree-vectorize, um auto Vektorisierung zu deaktivieren, ohne Sie von der Verwendung von intrinsics zu stoppen. Ich würde empfehlen, clang -march=native -mno-avx -O3 -fno-tree-vectorize zu testen, was ich glaube, dass Sie skalare Ganzzahl vs Legacy-SSE paddd testen möchten. (Es funktioniert in gcc und clang. In clang, AFAIK ist es ein Synonym für die Clang-spezifische -fno-vectorize.)

BTW, Timing beide in der gleichen ausführbaren tut weh der erste, weil die CPU nicht auf volle Turbo Rampe jetzt sofort. Sie befinden sich wahrscheinlich im zeitgesteuerten Bereich des Codes, bevor Ihre CPU auf Hochtouren läuft. (So ​​laufen Sie ein paar Mal hintereinander, mit for i in {1..10}; do time ./a.out; done.

Unter Linux würde ich perf stat -r5 ./a.out verwenden, um es 5 mal mit Leistungsindikatoren zu laufen (und ich würde es aufteilen, so ein Lauf getestet oder die andere, und so konnte ich für den gesamten Lauf bei perf Zählern sehen)


Code-Überprüfung:..

Sie stdint.h für uint32_t vergessen hatte ich hinzufügen, dass es zu compile on Godbolt to see the asm zu erhalten (Unter der Annahme, clang-. 5.0 ist so etwas wie die Apple-Version, die du benutzt, IDK, wenn Apples Klang eine Werbung impliziert Standard -mtune= Option, aber das würde Sinn machen, da es nur auf Mac zielt. Auch eine Basis-SSSE3 wäre für 64-Bit auf x86-64 OS X sinnvoll.)

Sie brauchen noinline auf debug_print. Außerdem würde ich einen anderen Namen für CYCLE_COUNT empfehlen. Zyklen in diesem Zusammenhang lassen mich an Taktzyklen denken, also nennen Sie es REP_COUNT oder REPEATS oder was auch immer.

Setzen Sie Ihre Arrays auf dem Stapel in main ist wahrscheinlich in Ordnung. Sie initialisieren beide Eingabearrays (meistens auf Null, aber die Leistung ist nicht datenabhängig).

Das ist gut, weil sie nicht initialisiert bleiben könnten, was bedeutet, dass mehrere 4k-Seiten jedes Arrays Copy-on-write auf dieselbe physische Nullseite abgebildet wurden, sodass Sie mehr als die erwartete Anzahl von L1D-Cache-Treffern erhalten würden.

Die SSE2-Schleife sollte Engpass auf L2/L3 Cache-Bandbreite, da die Arbeit eingestellt 4 * 32kiB * 3 = 384 kiB, so ist es etwa 1,5x der 256kB L2-Cache in Intel-CPUs.

clang könnte die automatische Vektorschleife ausrollen, mehr als die manuelle Intrinsic-Schleife. Das könnte eine bessere Leistung erklären, da nur 16B-Vektoren (nicht 32B AVX2) die Cachebandbreite möglicherweise nicht sättigen, wenn Sie nicht 2 Ladungen + 1 Speicher pro Takt erhalten.

Update: eigentlich ist die Schleife Overhead ziemlich extrem, mit 3 Zeigerinkrementen + einem Schleifenzähler, und nur um 2 abzurollen, um das zu amortisieren.


Die Auto-vektorisiert Schleife:

.LBB2_12:        # =>This Inner Loop Header: Depth=1 
    movdqu xmm0, xmmword ptr [r9 - 16] 
    movdqu xmm1, xmmword ptr [r9]   # hoisted load for 2nd unrolled iter 
    movdqu xmm2, xmmword ptr [r10 - 16] 
    paddd xmm2, xmm0 
    movdqu xmm0, xmmword ptr [r10] 
    paddd xmm0, xmm1 
    movdqu xmmword ptr [r11 - 16], xmm2 
    movdqu xmmword ptr [r11], xmm0 
    add  r9, 32 
    add  r10, 32 
    add  r11, 32 
    add  rbx, -8    # add/jne macro-fused on SnB-family CPUs 
    jne  .LBB2_12 

So ist es 12 verschmolzenen Domain Uops und kann bestenfalls zwei Vektoren pro 3 Uhren, Engpaß auf der Front-End-Ausgabe Bandbreite von 4 Uops laufen pro Uhr.

Es ist nicht mit ausgerichteten Lasten, da der Compiler nicht, dass Informationen nicht haben, ohne inlining in main, wo die Ausrichtung bekannt ist, und Sie nicht Ausrichtung garantieren mit p = __builtin_assume_aligned(p, 16) oder irgendetwas in der Stand-alone-Funktion. Ausgerichtete Lasten (oder AVX) würden paddd einen Speicheroperanden statt einer separaten movdqu Last verwenden lassen.

Die manuell vektorisierte Schleife verwendet ausgerichtete Lasten zum Speichern von Front-End-Ups, hat jedoch mehr Loop-Overhead vom Schleifenzähler.

.LBB1_7:        # =>This Inner Loop Header: Depth=1 
    movdqa xmm0, xmmword ptr [rcx - 16] 
    paddd xmm0, xmmword ptr [rax - 16] 
    movdqu xmmword ptr [r11 - 16], xmm0 

    movdqa xmm0, xmmword ptr [rcx] 
    paddd xmm0, xmmword ptr [rax] 
    movdqu xmmword ptr [r11], xmm0 

    add  r10, 2    # separate loop counter 
    add  r11, 32    # 3 pointer incrmeents 
    add  rax, 32 
    add  rcx, 32 
    cmp  r9, r10    # compare the loop counter 
    jne  .LBB1_7 

Also es ist 11 Fused-Domain-Uops. Es sollte schneller als die auto-vektorisierte Schleife ausgeführt werden. Wahrscheinlich hat Ihre Timing-Methode das Problem verursacht.

(Es sei denn, das Mischen von Ladungen und Speichern macht es tatsächlich weniger optimal. Die auto-vektorisierte Schleife hat 4 Ladungen und dann 2 Speicher. Eigentlich könnte das erklären. Ihre Arrays sind ein Vielfaches von 4kiB, und alle haben das gleiche relative Ausrichtung. So Sie 4k Aliasing hier könnte bekommen, was bedeutet, dass die CPU nicht sicher ist, dass ein Geschäft nicht eine Last überlappt. ich denke, ein Performance-Zähler gibt es für Sie, dass überprüfen.)


Siehe auch Agner Fog's microarch guide (and instruction tables + optimization guide und andere Links im Tag-Wiki , insbesondere Intel Optimierungshandbuch.

Es gibt auch einige gute SSE/SIMD Anfänger Sachen im Tag Wiki.

+0

Mit Clang verwende ich normalerweise '-fno-vectorize'. Warum sollte '-fno-tree-vectorize' verwendet werden (außer um mit GCC konsistent zu sein)? –

+0

@Zboson: Ich wusste nicht, Clang hatte einen anderen Namen für diese Option, danke. –

Verwandte Themen