2015-07-09 13 views
6

Ho riscontrato un problema molto sottile su SSE. Qui è il caso, voglio ottimizzare il mio ray tracer con SSE in modo che possa avere un'idea di base su come migliorare le prestazioni con SSE.Come evitare il flusso della pipeline SSE?

Mi piacerebbe iniziare con questa stessa funzione.

Vector3f Add(const Vector3f& v0 , Vector3f& v1); 

(In realtà ho cercato di ottimizzare CrossProduct prima, aggiungendo viene mostrato qui per semplicità ed ho capito che non è il collo di bottiglia del mio ray tracer.)

Ecco una parte della definizione della struct :

struct Vector3f 
{ union { struct{ float x ; float y ; float z; float reserved; }; __m128 data; }; 

il problema è che ci saranno SSE registrati a filo con questa stessa dichiarazione, il compilatore non è abbastanza intelligente per tenere quelli registro SSE per ulteriori utilizzi. E con la seguente dichiarazione, evita il flussaggio.

__m128 Add(__m128 v0_data, __m128 v1_data); 

posso andare con questo modo su questo caso, tuttavia sarebbe brutto disegno per Matrix che contiene quattro __m128 dati. E non è possibile che l'operatore lavori sullo stesso Vector3f ma sui suoi dati :((

La cosa più inquietante è che dovrai modificare il codice di livello superiore ovunque per adattare il cambiamento. tramite SSE non c'è assolutamente un'opzione per qualcosa di grande come un enorme motore di gioco, cambierai un'enorme quantità di codice prima che funzioni ..

Senza evitare lo svuotamento del registro SSE, la sua potenza verrà scaricata da quegli inutili comandi di lavaggio che rende inutile SSE, suppongo.

+0

Ho dimenticato di menzionare che Vector3f ha 16 bit allineati in memoria. – JerryCao1985

+0

Non dimenticare l'occasionale rossore di cortesia. –

+1

@ JerryCao1985 Puoi dirlo direttamente nella tua domanda modificandolo, invece di aggiungerlo come commento. – Borgleader

risposta

1

Sembra che l'unione sia una cosa brutta da usare qui. Finché un compilatore vede __m128 unificato con qualcosa, ha problemi con h capire quando aggiornare i valori, portando a operazioni di memoria eccessive.

MSVC non è il compilatore con le peggiori prestazioni in questa situazione. Basta controllare lo the code generated by GCC 5.1.0, funziona 12 volte più lentamente del codice generato da MSVC2013 (che è con spargimento di registri) sulla mia macchina e 20+ volte più lento del codice ottimale.

È interessante notare che la maggior parte dei compilatori inizia a fare cose stupide solo quando si utilizzano realmente i membri x, , z per accedere ai propri dati. Ad esempio, MSVC2013 si registra solo quando le leggi tramite membri scalari dopo il calcolo (suppongo che questi membri siano effettivi). Il terribile comportamento di GCC visto sopra scompare se si impostano i valori iniziali con _mm_setr_ps invece di scriverli direttamente nei membri.

In questo caso è meglio evitare i sindacati. Sembra che l'OP sia giunto alla stessa decisione (vedi current Vector3fv code). Rendere più difficile l'accesso a una singola coordinata ha un buon effetto "psicologico": una persona ci penserebbe due volte prima di scrivere il codice scalare. Si può facilmente scrivere setter/getter sia con estratto/intrinseche di inserimento (che rende compilatore genera queste istruzioni), oppure con la semplice aritmetica puntatore (il che rende compilatore scegliere qualche modo):

float getX() const { return ((float*)&data)[0]; } 

Quando rimuovo unione e utilizzare semplicemente __m128, il codice generato diventa migliore su tutti i compilatori.Tuttavia, MSVC2013 ha ancora movimenti non necessari: uno spostamento del registro inutile per ciascuna operazione aritmetica. Suppongo che questa sia una inefficienza nell'algoritmo di allineamento del compilatore. È possibile rimuovere queste mosse in MSVC2013 dichiarando tutte le funzioni come __vectorcall. Si noti che l'utilizzo di questa nuova convenzione di chiamata consente anche di evitare la fuoriuscita del registro nel caso in cui le funzioni simd non siano state affatto delineate.

Problemi correlati