Posso suggerire l'ottimizzatore fornendo l'intervallo di un numero intero?


173

Sto usando un inttipo per memorizzare un valore. Dalla semantica del programma, il valore varia sempre in un intervallo molto piccolo (0 - 36) e int(non a char) viene utilizzato solo a causa dell'efficienza della CPU.

Sembra che molte ottimizzazioni aritmetiche speciali possano essere eseguite su un così piccolo intervallo di numeri interi. Molte chiamate di funzione su tali numeri interi potrebbero essere ottimizzate in un piccolo insieme di operazioni "magiche" e alcune funzioni potrebbero persino essere ottimizzate nelle ricerche di tabella.

Quindi, è possibile dire al compilatore che questo intè sempre in quel piccolo intervallo, ed è possibile per il compilatore fare queste ottimizzazioni?


4
l'ottimizzazione dell'intervallo di valori esiste in molti compilatori, ad es. LLVM ma io non sono a conoscenza di alcun accenno lingua di dichiararla.
Remus Rusanu,

2
Nota che se non hai mai numeri negativi, potresti avere piccoli guadagni nell'uso dei unsignedtipi poiché sono più facili da ragionare per il compilatore.
user694733

4
@RemusRusanu: Pascal consente di definire tipi di sottorange , ad es var value: 0..36;.
Edgar Bonet,

7
" int (non un carattere) viene utilizzato solo perché l'efficienza della CPU. " Questo vecchio pezzo di saggezza convenzionale di solito non è molto vero. I tipi stretti a volte devono essere estesi di zero o di segno per l'intera larghezza del registro, esp. se usato come indice di array, ma a volte ciò accade gratuitamente. Se si dispone di un array di questo tipo, la riduzione dell'ingombro della cache di solito supera qualsiasi altra cosa.
Peter Cordes,

1
Dimenticato di dire: inte unsigned intdeve essere esteso anche a segno o zero da 32 a 64 bit, sulla maggior parte dei sistemi con puntatori a 64 bit. Si noti che su x86-64, le operazioni sui registri a 32 bit registrano zero-extension gratuitamente a 64-bit (non l'estensione del segno, ma l'overflow con segno è un comportamento indefinito, quindi il compilatore può usare solo matematica con segno a 64 bit se lo desidera). Quindi vedi solo istruzioni extra per args di funzioni a 32 bit a estensione zero, non risultati di calcolo. Per tipi non firmati più stretti.
Peter Cordes,

Risposte:


230

Sì, è possibile. Ad esempio, gccpuoi usare __builtin_unreachableper dire al compilatore di condizioni impossibili, in questo modo:

if (value < 0 || value > 36) __builtin_unreachable();

Possiamo racchiudere la condizione sopra in una macro:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

E usalo così:

assume(x >= 0 && x <= 10);

Come puoi vedere , gccesegue ottimizzazioni basate su queste informazioni:

#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

produce:

func(int):
    mov     eax, 17
    ret

Un aspetto negativo, tuttavia, è che se il tuo codice rompe tali assunzioni, ottieni un comportamento indefinito .

Non ti avvisa quando ciò accade, anche nelle build di debug. Per eseguire il debug / test / catturare più facilmente i bug con ipotesi, è possibile utilizzare una macro ibrida assume / assert (crediti a @David Z), come questa:

#if defined(NDEBUG)
#define assume(cond) do { if (!(cond)) __builtin_unreachable(); } while (0)
#else
#include <cassert>
#define assume(cond) assert(cond)
#endif

Nelle build di debug (con NDEBUG non definito), funziona come un normale assertmessaggio di errore di stampa e abortprogramma, e nelle build di rilascio fa uso di un presupposto, producendo codice ottimizzato.

Nota, tuttavia, che non sostituisce i normali assert: condrimane nelle build di rilascio, quindi non dovresti fare qualcosa del genere assume(VeryExpensiveComputation()).


5
@Xofo, non l'ho capito, nel mio esempio questo sta già accadendo, come return 2ramo è stato eliminato dal codice dal compilatore.

6
Tuttavia, sembra che gcc non sia in grado di ottimizzare le funzioni per operazioni magiche o ricerca di tabelle come previsto da OP.
jingyu9575,

19
@ user3528438, __builtin_expectè un suggerimento non rigoroso. __builtin_expect(e, c)dovrebbe essere letto come " eè più probabile che valuti c" e può essere utile per ottimizzare la previsione del ramo, ma non si limita ea esserlo sempre c, quindi non consente all'ottimizzatore di eliminare altri casi. Guarda come sono organizzati i rami in assemblea .

6
In teoria qualsiasi codice che causa incondizionatamente un comportamento indefinito potrebbe essere usato al posto di __builtin_unreachable().
CodesInChaos,

14
A meno che non ci sia qualche stranezza che non conosco che lo rende una cattiva idea, potrebbe avere senso combinarlo con assert, ad esempio definire assumecome assertquando NDEBUGnon è definito e come __builtin_unreachable()quando NDEBUGè definito. In questo modo si ottiene il vantaggio del presupposto nel codice di produzione, ma in una build di debug è ancora presente un controllo esplicito. Ovviamente devi quindi fare abbastanza test per assicurarti che l'assunzione sarà soddisfatta in natura.
David Z,

61

C'è un supporto standard per questo. Quello che dovresti fare è includere stdint.h( cstdint) e quindi usare il tipo uint_fast8_t.

Questo dice al compilatore che stai usando solo numeri tra 0 - 255, ma che è libero di usare un tipo più grande se questo dà un codice più veloce. Allo stesso modo, il compilatore può presumere che la variabile non avrà mai un valore superiore a 255 e quindi eseguire le ottimizzazioni di conseguenza.


2
Questi tipi non sono usati quasi quanto dovrebbero (io personalmente tendo a dimenticare che esistono). Danno un codice che è sia veloce che portatile, piuttosto brillante. E sono in giro dal 1999.
Lundin,

Questo è un buon suggerimento per il caso generale. La risposta di deniss mostra una soluzione più malleabile per scenari specifici.
Corse di leggerezza in orbita

1
Il compilatore ottiene solo le informazioni sull'intervallo 0-255 sui sistemi in cui uint_fast8_tè effettivamente un tipo a 8 bit (ad es. unsigned char) Come su x86 / ARM / MIPS / PPC ( godbolt.org/g/KNyc31 ). Su presto DEC Alpha prima 21164A , carichi di byte / negozi sono stati non sono supportati, in modo che qualsiasi implementazione sano di mente avrebbe utilizzato typedef uint32_t uint_fast8_t. AFAIK, non esiste alcun meccanismo per un tipo di avere limiti di portata extra con la maggior parte dei compilatori (come gcc), quindi sono abbastanza sicuro che uint_fast8_tsi comporterebbe esattamente lo stesso unsigned into qualunque cosa in quel caso.
Peter Cordes,

( boolè speciale ed è limitato a 0 o 1, ma è un tipo incorporato, non definito dai file di intestazione in termini di char, su gcc / clang. Come ho detto, non penso che la maggior parte dei compilatori abbia un meccanismo ciò renderebbe possibile ciò.)
Peter Cordes,

1
Ad ogni modo, uint_fast8_tè una buona raccomandazione, poiché utilizzerà un tipo a 8 bit su piattaforme in cui è efficiente come unsigned int. (Io sono in realtà non sono sicuro che i fasttipi dovrebbero essere veloce per , e se la cache impronta compromesso si suppone essere parte di esso.). x86 ha un ampio supporto per le operazioni sui byte, anche per eseguire l'aggiunta di byte con una sorgente di memoria, quindi non è nemmeno necessario eseguire un carico separato a estensione zero (che è anche molto economico). gcc crea uint_fast16_tun tipo a 64 bit su x86, che è folle per la maggior parte degli usi (rispetto a 32 bit). godbolt.org/g/Rmq5bv .
Peter Cordes,

8

La risposta attuale è utile nel caso in cui si sappia con certezza quale sia l'intervallo, ma se si desidera comunque un comportamento corretto quando il valore è al di fuori dell'intervallo previsto, non funzionerà.

In tal caso, ho scoperto che questa tecnica può funzionare:

if (x == c)  // assume c is a constant
{
    foo(x);
}
else
{
    foo(x);
}

L'idea è un compromesso dati-codice: stai spostando 1 bit di dati (se x == c) nella logica di controllo .
Ciò suggerisce all'ottimizzatore che xè in realtà una costante nota c, incoraggiandolo a incorporare e ottimizzare la prima invocazione fooseparatamente dal resto, possibilmente piuttosto pesantemente.

Assicurati di fattorizzare effettivamente il codice in una singola subroutine foo, tuttavia, non duplicare il codice.

Esempio:

Affinché questa tecnica funzioni, devi essere un po 'fortunato: ci sono casi in cui il compilatore decide di non valutare staticamente le cose e sono in qualche modo arbitrari. Ma quando funziona, funziona bene:

#include <math.h>
#include <stdio.h>

unsigned foo(unsigned x)
{
    return x * (x + 1);
}

unsigned bar(unsigned x) { return foo(x + 1) + foo(2 * x); }

int main()
{
    unsigned x;
    scanf("%u", &x);
    unsigned r;
    if (x == 1)
    {
        r = bar(bar(x));
    }
    else if (x == 0)
    {
        r = bar(bar(x));
    }
    else
    {
        r = bar(x + 1);
    }
    printf("%#x\n", r);
}

Basta usare -O3e notare le costanti pre-valutati 0x20e 0x30ein uscita assembler .


Non vorresti if (x==c) foo(c) else foo(x)? Se solo per catturare le constexprimplementazioni di foo?
MSalters,

@MSalters: sapevo che qualcuno lo avrebbe chiesto !! Mi è venuta in mente questa tecnica prima constexpre non mi sono mai preso la briga di "aggiornarla" in seguito (anche se non mi sono mai preoccupata di preoccuparmi constexprnemmeno dopo), ma il motivo per cui inizialmente non l'ho fatto era che volevo rendere più facile per il compilatore considerarli come codice comune e rimuovere il ramo se ha deciso di lasciarli come normali chiamate di metodo e non ottimizzare. Mi aspettavo che se mettessi dentro cè davvero difficile per il compilatore c (scusa, brutta battuta) che i due sono lo stesso codice, anche se non l'ho mai verificato.
user541686

4

Sto solo dicendo che se vuoi una soluzione che sia C ++ più standard, puoi usare l' [[noreturn]]attributo per scrivere la tua unreachable.

Quindi riproverò l' eccellente esempio di deniss per dimostrare:

namespace detail {
    [[noreturn]] void unreachable(){}
}

#define assume(cond) do { if (!(cond)) detail::unreachable(); } while (0)

int func(int x){
    assume(x >=0 && x <= 10);

    if (x > 11){
        return 2;
    }
    else{
        return 17;
    }
}

Che come puoi vedere , risulta in un codice quasi identico:

detail::unreachable():
        rep ret
func(int):
        movl    $17, %eax
        ret

Il rovescio della medaglia è ovviamente che si riceve un avviso che una [[noreturn]]funzione, in effetti, ritorna.


Funziona con clang, quando la mia soluzione originale non lo fa , così bello trucco e +1. Ma il tutto dipende molto dal compilatore (come ci ha mostrato Peter Cordes, in iccquanto può peggiorare le prestazioni), quindi non è ancora universalmente applicabile. Inoltre, nota minore: la unreachabledefinizione deve essere disponibile per l'ottimizzatore e incorporata affinché funzioni .
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.