2015-08-05 5 views
6

Quando ho bisogno di eseguire un'operazione vettoriale che ha un operando che è solo un float trasmesso a ogni componente, dovrei precomputare il __m256 o __m128 e caricarlo quando ne ho bisogno, o trasmettere il float al registro usando _mm_set1_ps ogni tempo ho bisogno del vettore?Per un vettore SSE con tutti gli stessi componenti, generare al volo o precomputer?

Ho precalcolato i vettori che sono molto importanti e molto usati e generano al volo quelli che sono meno importanti. Ma sto davvero guadagnando velocità con il precalcolo? Vale la pena?

Il _mm_set1_ps è implementato con una singola istruzione? Questo potrebbe rispondere alla mia domanda.

+0

Quando dici "carica" ​​presumo tu intenda copiando una variabile esistente (registro), giusto? Non dalla memoria? – Mehrdad

+0

@ Mehrdad Beh, principalmente intendo dalla memoria, a meno che non si trovi già nel registro. Per l'opzione precomputa, voglio dire impostare __m128 da qualche parte nel codice e quindi fare riferimento a esso ovunque ne abbiate bisogno, invece di generare lo stesso vettore con _mm_set1_ps. Questa è la spiegazione più semplice. – Thomas

+2

È difficile dire apertamente dato che 'mm_set1_ps' non corrisponde a nessuna singola istruzione. Potrebbe persino tradurre in ciò che stai suggerendo di fare. Perché non controllare l'assemblaggio e vedere? Inoltre, dal momento che hai taggato il tuo post con AVX, ti piacerebbe sapere che c'è una nuova intrinseca che corrisponde a una singola istruzione: '_mm_broadcast_ss' – hayesti

risposta

2

Naturalmente dipenderà molto dal codice, ma ho implementato due semplici funzioni utilizzando entrambi gli approcci. See code

__m128 calc_set1(float num1, float num2) 
{ 
    __m128 num1_4 = _mm_set1_ps(num1); 
    __m128 num2_4 = _mm_set1_ps(num2); 
    __m128 result4 = _mm_mul_ps(num1_4, num2_4); 

    return result4; 
} 

__m128 calc_mov(float* num1_4_addr, float* num2_4_addr) 
{ 
    __m128 num1_4 = _mm_load_ps(num1_4_addr); 
    __m128 num2_4 = _mm_load_ps(num2_4_addr); 
    __m128 result4 = _mm_mul_ps(num1_4, num2_4); 

    return result4; 
} 

e montaggio

calc_set1(float, float): 
    shufps $0, %xmm0, %xmm0 
    shufps $0, %xmm1, %xmm1 
    mulps %xmm1, %xmm0 
    ret 
calc_mov(float*, float*): 
    movaps (%rdi), %xmm0 
    mulps (%rsi), %xmm0 
    ret 

Si può vedere che la calc_mov() fa come quello che ci si aspetterebbe e la calc_set1() utilizza una singola istruzione shuffle.

A movps Un'istruzione può richiedere circa quattro cicli per la generazione di indirizzi e altro se la porta di caricamento della cache L1 è occupata + più nell'evento raro di una mancanza di cache.

shufps eseguirà un singolo ciclo su una delle recenti microarchitettura Intel. Credo che sia vero sia per SSE128 o AVX256. Pertanto suggerirei di utilizzare l'approccio mm_set1_ps.

Ovviamente, un'istruzione shuffle presuppone che il float si trovi già in un registro SSE/AVX. Nel caso in cui lo stiate caricando dalla memoria, la trasmissione sarà migliore dato che catturerà il meglio di movps e shufps in una singola istruzione.

3

Credo che sia generalmente meglio calcolare il vettore SSE dal codice (ad es. Loop) e utilizzarlo ogni volta che è necessario, assumendo attenzione a non forzarlo accidentalmente in memoria. (Ad esempio, se si prende il suo indirizzo o lo si passa facendo riferimento a un'altra funzione, può essere forzato in memoria e si può ottenere un comportamento strano.)
L'idea è che di solito è consigliabile evitare il trasferimento di valori in e fuori dai registri SSE, e se capita che questo non sia il caso nella tua particolare situazione, il compilatore sa già come è stato costruito il valore, e potrebbe rematerialize se necessario. Penso che questo sia molto più semplice di loop-invariant code motion in generale, che è l'ottimizzazione inversa (vale a dire dove il compilatore lo calcola per te) e che richiede al compilatore di dimostrare che il codice è effettivamente invariante del ciclo.

3

Stavo giocando con le trasmissioni per una risposta a fastest way to fill a vector (SSE2) with a certain value. Templates friendly. Dai un'occhiata ad alcune discariche di asm delle trasmissioni.

set1 ogni volta che viene utilizzato non dovrebbe fare molta differenza, purché il compilatore sappia che il valore da trasmettere non è alias nulla. (Se il compilatore non può assumere che non sia alias, dovrà ripetere la trasmissione dopo ogni scrittura su un array o puntatore che potrebbe essere alias.)

Di solito è un buon stile memorizzare il risultato set1 in una variabile denominata. Se il compilatore esaurisce i registri vettoriali, potrebbe rovesciare il vettore nello stack e ricaricarlo in un secondo momento, oppure potrebbe ri-trasmettere. Non sono sicuro che lo stile di codifica influenzerà questa decisione.

Non utilizzare una variabile static const per memorizzarla tra una chiamata e l'altra. (Ciò può portare al codice generatore del compilatore per verificare se la variabile è stata già inizializzata ogni chiamata)

Le trasmissioni di costanti in fase di compilazione a volte generano trasmissioni in fase di compilazione, pertanto il codice contiene solo 16B di dati const presenti in memoria.

Le trasmissioni AVX1 di un valore già in un registro sono le peggiori. AVX1 fornisce solo la fonte di memoria vbroadcastps (utilizza solo la porta di caricamento). Una trasmissione prende uno shufps/vinsertf128.

AVX2 è richiesto per vbroadcastps ymm, xmm (utilizza la porta shuffle)).