Sto cercando di ottimizzare del codice che dovrebbe leggere i float di precisione singola dalla memoria ed eseguire aritmetici su di essi in doppia precisione. Ciò sta diventando un collo di bottiglia significativo per le prestazioni, poiché il codice che memorizza i dati in memoria come precisione singola è sostanzialmente più lento rispetto al codice equivalente che memorizza i dati in memoria come precisione doppia. Qui di seguito è un programma giocattolo C++ che cattura l'essenza del mio problema:Perché GCC e Clang non usano cvtss2sd [memoria]?
#include <cstdio>
// noinline to force main() to actually read the value from memory.
__attributes__ ((noinline)) float* GetFloat() {
float* f = new float;
*f = 3.14;
return f;
}
int main() {
float* f = GetFloat();
double d = *f;
printf("%f\n", d); // Use the value so it isn't optimized out of existence.
}
Sia GCC e Clang eseguire il caricamento di *f
e la conversione a doppia precisione come due istruzioni separate, anche se l'istruzione cvtss2sd
supporta la memoria come argomento fonte . Secondo Agner Fog, cvtss2sd r, m
viene eseguito con la stessa velocità di movss r, m
sulla maggior parte delle architetture ed evita di dover eseguire afterwords cvtss2sd r, r
. Ciò nonostante, Clang genera il seguente codice per main()
:
main PROC
push rbp ;
mov rbp, rsp ;
call _Z8GetFloatv ;
movss xmm0, dword ptr [rax] ;
cvtss2sd xmm0, xmm0 ;
mov edi, offset ?_001 ;
mov al, 1 ;
call printf ;
xor eax, eax ;
pop rbp ;
ret ;
main ENDP
GCC genera codice simile inefficiente. Perché nessuno di questi compilatori genera semplicemente qualcosa come cvtss2sd xmm0, dword ptr [rax]
?
MODIFICA: Ottima risposta, Stephen Canon! Ho preso l'output in linguaggio assembly di Clang per il mio caso d'uso reale, l'ho incollato in un file sorgente come inline ASM, l'ho confrontato, poi ho apportato le modifiche qui discusse e l'ho confrontato di nuovo. Non potevo credere che cvtss2sd [memory]
sia effettivamente più lento.
Interessante, ma porta a due domande: 1. Perché i bit alti non vengono azzerati? Presumibilmente se stai usando questa istruzione il tuo intento è quello di scrivere codice non-vettorizzato. 2. GCC e Clang sembrano continuare a farlo anche quando i bit alti del registro xmm non vengono utilizzati, cioè quando si usano solo istruzioni non vettoriali successivamente. Perchè è questo? – dsimcha
1. Intel ha scelto di fare così; il perché non è terribilmente importante. È occasionalmente utile, ma probabilmente causa più problemi del suo valore. 2. Il pericolo di aggiornamento del registro parziale è presente anche se la parte alta dei registri XMM non viene mai utilizzata. Questo è ciò che lo rende così insidioso. –
La spiegazione più dettagliata nella tua modifica è fantastica! La mia unica domanda è, perché la logica della CPU non dipende dalla bassa quadricipia delle dipendenze della quadword superiore e capisce che le istruzioni xxxsd leggono solo da/write alla quadword bassa del registro? – dsimcha