2015-11-05 10 views
11

Sto sperimentando l'assemblaggio generato e ho trovato una cosa interessante. Ci sono due funzioni che eseguono un calcolo identico. L'unica differenza tra loro è il modo in cui i risultati vengono sommati.Differenze dure nell'assemblaggio generato di confronti a virgola mobile < and > =

#include <cmath> 

double func1(double x, double y) 
{ 
    double result1; 
    double result2; 

    if (x*x < 0.0) result1 = 0.0; 
    else 
    { 
    result1 = x*x+x+y; 
    } 

    if (y*y < 0.0) result2 = 0.0; 
    else 
    { 
    result2 = y*y+y+x; 
    } 

    return (result1 + result2) * 40.0; 
} 

double func2(double x, double y) 
{ 
    double result = 0.0; 

    if (x*x >= 0.0) 
    { 
    result += x*x+x+y; 
    } 

    if (y*y >= 0.0) 
    { 
    result += y*y+y+x; 
    } 

    return result * 40.0; 
} 

Il complesso generato da x86 clang 3,7 con -O2 interruttore gcc.godbolt.org è ancora molto diverso e inaspettato. (Compilazione sui risultati del GCC in assemblaggio simile)

.LCPI0_0: 
    .quad 4630826316843712512  # double 40 
func1(double, double):        # @func1(double, double) 
    movapd %xmm0, %xmm2 
    mulsd %xmm2, %xmm2 
    addsd %xmm0, %xmm2 
    addsd %xmm1, %xmm2 
    movapd %xmm1, %xmm3 
    mulsd %xmm3, %xmm3 
    addsd %xmm1, %xmm3 
    addsd %xmm0, %xmm3 
    addsd %xmm3, %xmm2 
    mulsd .LCPI0_0(%rip), %xmm2 
    movapd %xmm2, %xmm0 
    retq 

.LCPI1_0: 
    .quad 4630826316843712512  # double 40 
func2(double, double):        # @func2(double, double) 
    movapd %xmm0, %xmm2 
    movapd %xmm2, %xmm4 
    mulsd %xmm4, %xmm4 
    xorps %xmm3, %xmm3 
    ucomisd %xmm3, %xmm4 
    xorpd %xmm0, %xmm0 
    jb .LBB1_2 
    addsd %xmm2, %xmm4 
    addsd %xmm1, %xmm4 
    xorpd %xmm0, %xmm0 
    addsd %xmm4, %xmm0 
.LBB1_2: 
    movapd %xmm1, %xmm4 
    mulsd %xmm4, %xmm4 
    ucomisd %xmm3, %xmm4 
    jb .LBB1_4 
    addsd %xmm1, %xmm4 
    addsd %xmm2, %xmm4 
    addsd %xmm4, %xmm0 
.LBB1_4: 
    mulsd .LCPI1_0(%rip), %xmm0 
    retq 

func1 compila a un assemblaggio senza rami, che coinvolge molto meno di quanto le istruzioni func2. quindi func2 dovrebbe essere molto più lento di func1.

Qualcuno può spiegare questo comportamento?

+6

La differenza più ovvia è che l'ottimizzatore sa 'x * x <0.0' può essere ottimizzato per' false' ma non sa 'x * x> = 0.0' può essere ottimizzato per' true'. Nell'osservare un sacco di assembly ottimizzati da diversi compilatori, non ho mai visto alcun modello sano di quello che gli ottimizzatori fanno o non capiscono. – JSF

risposta

15

La ragione di questo comportamento degli operatori di confronto < o >= differisce se il double è NaN o meno di un NaN. Tutti i confronti in cui uno degli operandi è NaN restituisce false. Quindi il tuo x*x < 0.0 sarà sempre essere falso indipendentemente dal fatto che x sia NaN oppure no. Quindi il compilatore può ottimizzarlo in sicurezza. Tuttavia, il caso di x * x >= 0 si comporta in modo diverso per i valori NaN e non NaN, pertanto il compilatore lascia i salti condizionali nell'assieme.

Questo è ciò cppreference dice sul confronto con NaN coinvolte:

i valori degli operandi dopo la conversione sono confrontati nel senso matematico (tranne che zeri positivi e negativi confronta uguale e ogni confronto che coinvolge un NaN valore restituisce zero)