Perché uno switch non è ottimizzato allo stesso modo di quello incatenato se altro in c / c ++?


39

La seguente implementazione di square produce una serie di istruzioni cmp / je come mi aspetterei da un'istruzione if concatenata:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

E quanto segue produce una tabella di dati per il ritorno:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

Perché gcc non è in grado di ottimizzare quello superiore in quello inferiore?

Smontaggio per riferimento: https://godbolt.org/z/UP_igi

EDIT: è interessante notare che MSVC genera una tabella di salto anziché una tabella di dati per il caso switch. E sorprendentemente, clang li ottimizza allo stesso risultato.


3
Cosa intendi con "comportamento indefinito"? Fintanto che il comportamento osservabile è lo stesso, il compilatore può generare qualsiasi codice assembly / macchina che vuole
bolov

2
@ user207421 ignorando la returns; i casi non hanno breaks, quindi anche l'interruttore ha un ordine di esecuzione specifico. La catena if / else ha rendimenti in ogni ramo, la semantica in questo caso è equivalente. L'ottimizzazione non è impossibile . Come esempio di esempio, icc non ottimizza nessuna delle funzioni.
user1810087

9
Forse la risposta più semplice ... gcc non è in grado di vedere questa struttura e ottimizzarla (ancora).
user1810087

3
Sono d'accordo con @ user1810087. Hai semplicemente trovato l'attuale limite del processo di perfezionamento del compilatore. Un sotto-caso che attualmente non è riconosciuto come ottimizzabile (da alcuni compilatori). In effetti, non tutte le altre catene if possono essere ottimizzate in questo modo, ma solo il sottoinsieme in cui viene testata la variabile SAME rispetto a valori costanti.
Roberto Caboni,

1
L'if-if ha un ordine di esecuzione diverso, dall'alto verso il basso. Tuttavia, sostituendo il codice solo se le istruzioni non migliorano il codice macchina. D'altra parte, l'interruttore non ha un ordine di esecuzione predefinito ed è essenzialmente solo una tabella di goto jump glorificata. Detto questo, un compilatore può ragionare sul comportamento osservabile qui, quindi la scarsa ottimizzazione della versione if-else è piuttosto deludente.
Lundin

Risposte:


29

Il codice generato per switch-caseconvenzionalmente utilizza una tabella di salto. In questo caso, il ritorno diretto attraverso una tabella di ricerca sembra essere un'ottimizzazione che sfrutta il fatto che ogni caso qui comporta un ritorno. Sebbene lo standard non fornisca garanzie in tal senso, sarei sorpreso se un compilatore generasse una serie di confronti anziché una tabella di salto per un caso switch convenzionale.

Ora arriviamo a if-else, è esattamente l'opposto. Mentre switch-caseviene eseguito a tempo costante, indipendentemente dal numero di rami, if-elseè ottimizzato per un numero inferiore di rami. Qui, ti aspetteresti che il compilatore generi sostanzialmente una serie di confronti nell'ordine in cui li hai scritti.

Quindi, se avessi usato if-elseperché mi aspetto la maggior parte delle chiamate a square()essere per 0o 1e raramente per altri valori, poi 'ottimizzare' ad un tavolo di ricerca potrebbe effettivamente causare il mio codice per eseguire lento di quanto mi aspettavo, sconfiggendo il mio scopo per l'utilizzo di un ifposto di a switch. Quindi, sebbene sia discutibile, ritengo che GCC stia facendo la cosa giusta e il clang sia eccessivamente aggressivo nella sua ottimizzazione.

Qualcuno, nei commenti, aveva condiviso un link in cui clang esegue questa ottimizzazione e genera anche codice basato sulla tabella di ricerca if-else. Qualcosa di notevole accade quando riduciamo il numero di casi a solo due (e un valore predefinito) con clang. Ancora una volta genera codice identico sia per if che per switch, ma questa volta passa a confronti e si sposta invece dell'approccio della tabella di ricerca, per entrambi. Ciò significa che anche il clang favorito dal cambio sa che il modello 'if' è più ottimale quando il numero di casi è piccolo!

In sintesi, una sequenza di confronti per if-elsee una tabella di salto per switch-caseè il modello standard che i compilatori tendono a seguire e gli sviluppatori tendono ad aspettarsi quando scrivono codice. Tuttavia, per alcuni casi speciali, alcuni compilatori potrebbero scegliere di interrompere questo schema laddove ritengono che fornisca una migliore ottimizzazione. Altri compilatori potrebbero semplicemente scegliere di attenersi al modello comunque, anche se apparentemente non ottimale, affidando allo sviluppatore di sapere cosa vuole. Entrambi sono approcci validi con i loro vantaggi e svantaggi.


2
Sì, l'ottimizzazione è un'arma a più taglienti: ciò che scrivono, ciò che vogliono, ciò che ottengono e chi imprecano per quello.
Deduplicatore

1
"... allora" l'ottimizzazione "di questo in una ricerca di tabelle farebbe sì che il mio codice venga eseguito più lentamente di quanto mi aspettassi ..." Puoi fornire una giustificazione per questo? Perché una tabella di salto dovrebbe mai essere più lenta di due possibili rami condizionali (per verificare gli input rispetto a 0e 1)?
Cody Grey

@CodyGray Devo confessare che non sono arrivato al livello dei cicli di conteggio: ho semplicemente avuto la sensazione che il carico dalla memoria attraverso un puntatore potesse richiedere più cicli di un confronto e un salto, ma potrei sbagliarmi. Tuttavia, spero che tu sia d'accordo con me sul fatto che anche in questo caso, almeno per "0", ifè ovviamente più veloce? Ora, ecco un esempio di una piattaforma in cui sia 0 che 1 sarebbero più veloci quando si usa ifche quando si usa switch: godbolt.org/z/wcJhvS (Nota che ci sono anche molte altre ottimizzazioni in gioco qui)
th33lf

1
Bene, i cicli di conteggio non funzionano comunque sulle moderne architetture superscalar OOO. :-) I carichi dalla memoria non saranno più lenti dei rami male previsti, quindi la domanda è: quanto è probabile che il ramo sia previsto? Tale domanda si applica a tutti i tipi di rami condizionali, generati da ifdichiarazioni esplicite o automaticamente dal compilatore. Non sono un esperto ARM, quindi non sono davvero sicuro se l'affermazione che stai facendo riguardo switchall'essere più veloce di quanto ifsia vera. Dipenderebbe dalla penalità per i rami male previsti e ciò dipenderebbe effettivamente da quale ARM.
Cody Grey

0

Una possibile logica è che se numsono più probabili valori bassi di , ad esempio sempre 0, il codice generato per il primo potrebbe essere più veloce. Il codice generato per switch richiede lo stesso tempo per tutti i valori.

Confrontando i casi migliori, secondo questa tabella . Vedi questa risposta per la spiegazione della tabella.

Se num == 0per "if" hai xor, test, je (con salto), ret. Latenza: 1 + 1 + salto. Tuttavia, xor e test sono indipendenti, quindi la velocità effettiva di esecuzione sarebbe maggiore di 1 + 1 cicli.

Se num < 7per "switch" hai mov, cmp, ja (senza salto), mov, ret. Latenza: 2 + 1 + nessun salto + 2.

Un'istruzione di salto che non risulta saltare è più veloce di quella che risulta saltare. Tuttavia, la tabella non definisce la latenza per un salto, quindi non mi è chiaro quale sia la migliore. È possibile che l'ultimo sia sempre migliore e GCC semplicemente non è in grado di ottimizzarlo.


1
Hmm, teoria interessante, ma per ifs vs switch hai: xor, test, jmp vs mov, cmp jmp. Tre istruzioni ognuna delle quali l'ultima è un salto. Sembra uguale nel migliore dei casi, no?
Chacham15

3
"Un'istruzione di salto che non risulta saltare è più veloce di quella che risulta saltare.". È la previsione del ramo che conta.
geza,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.