Iniziare con le istruzioni SIMD Intel x86 SSE
Domanda
Voglio saperne di più sull'utilizzo di SSE.
Quali modi ci sono per imparare, oltre all'ovvia lettura del Manuali per sviluppatori di software per architetture Intel® 64 e IA-32?
Principalmente mi interessa lavorare con il Funzioni integrate di GCC X86.
Soluzione
In primo luogo, non mi consiglia di utilizzare le funzioni built-in - non sono portatili (attraverso i compilatori dello stesso arco).
intrinseci , GCC fa un ottimo lavoro ottimizzazione intrinseci SSE in codice ancora più ottimizzato. Si può sempre avere una sbirciatina al montaggio e vedere come utilizzare SSE al suo pieno potenziale.
Intrinsics sono facili - proprio come normale funzione chiamate:
#include <immintrin.h> // portable to all x86 compilers
int main()
{
__m128 vector1 = _mm_set_ps(4.0, 3.0, 2.0, 1.0); // high element first, opposite of C array order. Use _mm_setr_ps if you want "little endian" element order in the source.
__m128 vector2 = _mm_set_ps(7.0, 8.0, 9.0, 0.0);
__m128 sum = _mm_add_ps(vector1, vector2); // result = vector1 + vector 2
vector1 = _mm_shuffle_ps(vector1, vector1, _MM_SHUFFLE(0,1,2,3));
// vector1 is now (1, 2, 3, 4) (above shuffle reversed it)
return 0;
}
Usa _mm_load_ps
o _mm_loadu_ps
di caricare dati da array.
Naturalmente ci sono il modo più opzioni, SSE è davvero potente e, a mio parere relativamente facile da imparare.
Si veda anche https://stackoverflow.com/tags/sse/info per alcuni collegamenti a manuali.
Altri suggerimenti
Dato che hai chiesto risorse:
Una guida pratica all'utilizzo di SSE con C++:Buona panoramica concettuale su come utilizzare l’SSE in modo efficace, con esempi.
Elenco MSDN degli intrinseci del compilatore:Riferimento completo per tutte le vostre esigenze intrinseche.È MSDN, ma praticamente tutti gli elementi intrinseci elencati qui sono supportati anche da GCC e ICC.
Pagina SSE di Christopher Wright:Riferimento rapido sul significato dei codici operativi SSE.Immagino che i manuali Intel possano svolgere la stessa funzione, ma è più veloce.
Probabilmente è meglio scrivere la maggior parte del codice in intrinseci, ma controlla l'objdump dell'output del compilatore per assicurarti che stia producendo codice efficiente.La generazione di codice SIMD è ancora una tecnologia abbastanza nuova ed è molto probabile che in alcuni casi il compilatore possa sbagliare.
Trovo ricerca e di ottimizzazione guide del Dott Agner Fog molto prezioso! Ha anche alcune librerie e strumenti di test che non ho ancora provato. http://www.agner.org/optimize/
Passaggio 1: scrivere un po 'di assemblaggio manuale
Vi consiglio di provare prima di scrivere il proprio assemblaggio manuale di vedere e controllare esattamente ciò che sta accadendo quando si avvia l'apprendimento.
Quindi la domanda diventa come osservare ciò che sta accadendo nel programma, e le risposte sono:
- GDB
- utilizzare la libreria standard C per
print
e le coseassert
Uso della libreria standard C te stesso richiede un po 'di lavoro, ma niente di più. Ho per esempio fatto questo lavoro bene per voi su Linux nei seguenti file della mia configurazione di prova:
Utilizzando questi aiutanti, ho poi inizio a giocare in giro con le nozioni di base, come ad esempio:
-
dati
- load e store a / dalla memoria in registri SSE
- aggiungere numeri interi e numeri in virgola mobile di diverse dimensioni
- affermano che i risultati sono quello che mi aspetto
addpd.S
#include <lkmc.h>
LKMC_PROLOGUE
.data
.align 16
addps_input0: .float 1.5, 2.5, 3.5, 4.5
addps_input1: .float 5.5, 6.5, 7.5, 8.5
addps_expect: .float 7.0, 9.0, 11.0, 13.0
addpd_input0: .double 1.5, 2.5
addpd_input1: .double 5.5, 6.5
addpd_expect: .double 7.0, 9.0
.bss
.align 16
output: .skip 16
.text
/* 4x 32-bit */
movaps addps_input0, %xmm0
movaps addps_input1, %xmm1
addps %xmm1, %xmm0
movaps %xmm0, output
LKMC_ASSERT_MEMCMP(output, addps_expect, $0x10)
/* 2x 64-bit */
movaps addpd_input0, %xmm0
movaps addpd_input1, %xmm1
addpd %xmm1, %xmm0
movaps %xmm0, output
LKMC_ASSERT_MEMCMP(output, addpd_expect, $0x10)
LKMC_EPILOGUE
paddq.S
#include <lkmc.h>
LKMC_PROLOGUE
.data
.align 16
input0: .long 0xF1F1F1F1, 0xF2F2F2F2, 0xF3F3F3F3, 0xF4F4F4F4
input1: .long 0x12121212, 0x13131313, 0x14141414, 0x15151515
paddb_expect: .long 0x03030303, 0x05050505, 0x07070707, 0x09090909
paddw_expect: .long 0x04030403, 0x06050605, 0x08070807, 0x0A090A09
paddd_expect: .long 0x04040403, 0x06060605, 0x08080807, 0x0A0A0A09
paddq_expect: .long 0x04040403, 0x06060606, 0x08080807, 0x0A0A0A0A
.bss
.align 16
output: .skip 16
.text
movaps input1, %xmm1
/* 16x 8bit */
movaps input0, %xmm0
paddb %xmm1, %xmm0
movaps %xmm0, output
LKMC_ASSERT_MEMCMP(output, paddb_expect, $0x10)
/* 8x 16-bit */
movaps input0, %xmm0
paddw %xmm1, %xmm0
movaps %xmm0, output
LKMC_ASSERT_MEMCMP(output, paddw_expect, $0x10)
/* 4x 32-bit */
movaps input0, %xmm0
paddd %xmm1, %xmm0
movaps %xmm0, output
LKMC_ASSERT_MEMCMP(output, paddd_expect, $0x10)
/* 2x 64-bit */
movaps input0, %xmm0
paddq %xmm1, %xmm0
movaps %xmm0, output
LKMC_ASSERT_MEMCMP(output, paddq_expect, $0x10)
LKMC_EPILOGUE
Passaggio 2: scrivere alcune intrinseche
Per il codice di produzione tuttavia, è probabile che vuole utilizzare i intrinseci pre-esistenti, invece di assemblaggio crudo come accennato: https: / /stackoverflow.com/a/1390802/895245
Quindi, ora provo a convertire i precedenti esempi in codice C più o meno equivalenti con intrinseche.
addpq.c
#include <assert.h>
#include <string.h>
#include <x86intrin.h>
float global_input0[] __attribute__((aligned(16))) = {1.5f, 2.5f, 3.5f, 4.5f};
float global_input1[] __attribute__((aligned(16))) = {5.5f, 6.5f, 7.5f, 8.5f};
float global_output[4] __attribute__((aligned(16)));
float global_expected[] __attribute__((aligned(16))) = {7.0f, 9.0f, 11.0f, 13.0f};
int main(void) {
/* 32-bit add (addps). */
{
__m128 input0 = _mm_set_ps(1.5f, 2.5f, 3.5f, 4.5f);
__m128 input1 = _mm_set_ps(5.5f, 6.5f, 7.5f, 8.5f);
__m128 output = _mm_add_ps(input0, input1);
/* _mm_extract_ps returns int instead of float:
* * https://stackoverflow.com/questions/5526658/intel-sse-why-does-mm-extract-ps-return-int-instead-of-float
* * https://stackoverflow.com/questions/3130169/how-to-convert-a-hex-float-to-a-float-in-c-c-using-mm-extract-ps-sse-gcc-inst
* so we must use instead: _MM_EXTRACT_FLOAT
*/
float f;
_MM_EXTRACT_FLOAT(f, output, 3);
assert(f == 7.0f);
_MM_EXTRACT_FLOAT(f, output, 2);
assert(f == 9.0f);
_MM_EXTRACT_FLOAT(f, output, 1);
assert(f == 11.0f);
_MM_EXTRACT_FLOAT(f, output, 0);
assert(f == 13.0f);
/* And we also have _mm_cvtss_f32 + _mm_shuffle_ps, */
assert(_mm_cvtss_f32(output) == 13.0f);
assert(_mm_cvtss_f32(_mm_shuffle_ps(output, output, 1)) == 11.0f);
assert(_mm_cvtss_f32(_mm_shuffle_ps(output, output, 2)) == 9.0f);
assert(_mm_cvtss_f32(_mm_shuffle_ps(output, output, 3)) == 7.0f);
}
/* Now from memory. */
{
__m128 *input0 = (__m128 *)global_input0;
__m128 *input1 = (__m128 *)global_input1;
_mm_store_ps(global_output, _mm_add_ps(*input0, *input1));
assert(!memcmp(global_output, global_expected, sizeof(global_output)));
}
/* 64-bit add (addpd). */
{
__m128d input0 = _mm_set_pd(1.5, 2.5);
__m128d input1 = _mm_set_pd(5.5, 6.5);
__m128d output = _mm_add_pd(input0, input1);
/* OK, and this is how we get the doubles out:
* with _mm_cvtsd_f64 + _mm_unpackhi_pd
* https://stackoverflow.com/questions/19359372/mm-cvtsd-f64-analogon-for-higher-order-floating-point
*/
assert(_mm_cvtsd_f64(output) == 9.0);
assert(_mm_cvtsd_f64(_mm_unpackhi_pd(output, output)) == 7.0);
}
return 0;
}
GitHub monte.
paddq.c
#include <assert.h>
#include <inttypes.h>
#include <string.h>
#include <x86intrin.h>
uint32_t global_input0[] __attribute__((aligned(16))) = {1, 2, 3, 4};
uint32_t global_input1[] __attribute__((aligned(16))) = {5, 6, 7, 8};
uint32_t global_output[4] __attribute__((aligned(16)));
uint32_t global_expected[] __attribute__((aligned(16))) = {6, 8, 10, 12};
int main(void) {
/* 32-bit add hello world. */
{
__m128i input0 = _mm_set_epi32(1, 2, 3, 4);
__m128i input1 = _mm_set_epi32(5, 6, 7, 8);
__m128i output = _mm_add_epi32(input0, input1);
/* _mm_extract_epi32 mentioned at:
* https://stackoverflow.com/questions/12495467/how-to-store-the-contents-of-a-m128d-simd-vector-as-doubles-without-accessing/56404421#56404421 */
assert(_mm_extract_epi32(output, 3) == 6);
assert(_mm_extract_epi32(output, 2) == 8);
assert(_mm_extract_epi32(output, 1) == 10);
assert(_mm_extract_epi32(output, 0) == 12);
}
/* Now from memory. */
{
__m128i *input0 = (__m128i *)global_input0;
__m128i *input1 = (__m128i *)global_input1;
_mm_store_si128((__m128i *)global_output, _mm_add_epi32(*input0, *input1));
assert(!memcmp(global_output, global_expected, sizeof(global_output)));
}
/* Now a bunch of other sizes. */
{
__m128i input0 = _mm_set_epi32(0xF1F1F1F1, 0xF2F2F2F2, 0xF3F3F3F3, 0xF4F4F4F4);
__m128i input1 = _mm_set_epi32(0x12121212, 0x13131313, 0x14141414, 0x15151515);
__m128i output;
/* 8-bit integers (paddb) */
output = _mm_add_epi8(input0, input1);
assert(_mm_extract_epi32(output, 3) == 0x03030303);
assert(_mm_extract_epi32(output, 2) == 0x05050505);
assert(_mm_extract_epi32(output, 1) == 0x07070707);
assert(_mm_extract_epi32(output, 0) == 0x09090909);
/* 32-bit integers (paddw) */
output = _mm_add_epi16(input0, input1);
assert(_mm_extract_epi32(output, 3) == 0x04030403);
assert(_mm_extract_epi32(output, 2) == 0x06050605);
assert(_mm_extract_epi32(output, 1) == 0x08070807);
assert(_mm_extract_epi32(output, 0) == 0x0A090A09);
/* 32-bit integers (paddd) */
output = _mm_add_epi32(input0, input1);
assert(_mm_extract_epi32(output, 3) == 0x04040403);
assert(_mm_extract_epi32(output, 2) == 0x06060605);
assert(_mm_extract_epi32(output, 1) == 0x08080807);
assert(_mm_extract_epi32(output, 0) == 0x0A0A0A09);
/* 64-bit integers (paddq) */
output = _mm_add_epi64(input0, input1);
assert(_mm_extract_epi32(output, 3) == 0x04040404);
assert(_mm_extract_epi32(output, 2) == 0x06060605);
assert(_mm_extract_epi32(output, 1) == 0x08080808);
assert(_mm_extract_epi32(output, 0) == 0x0A0A0A09);
}
return 0;
GitHub monte.
Passaggio 3: andare a ottimizzare il codice e benchmark
Il passo finale, e più importante e difficile, è, naturalmente, di utilizzare effettivamente le intrinseci per rendere il codice veloce, e poi a valutare il vostro miglioramento.
In questo modo, è probabile che sarà necessario per imparare un po 'la microarchitettura x86, che io non mi conosco. CPU vs IO legato sarà probabilmente una delle cose che viene in su: Cosa significano i termini "CPU bound" e "i / O bound" significa?
Come accennato: https://stackoverflow.com/a/12172046/895245 questo coinvolgerà quasi inevitabilmente lettura la documentazione di Agner Fog, che sembrano essere meglio di qualsiasi cosa Intel si è pubblicato.
Si spera tuttavia i punti 1 e 2 serviranno come base per almeno sperimentare con aspetti funzionali non-prestazioni e rapidamente vedere cosa stanno facendo le istruzioni.
TODO:. Produrre un interessante esempio minimale di tale ottimizzazione qui