2013-07-06 10 views
5

Nel mio codice programma ci sono vari oggetti piuttosto piccoli che vanno da un byte o da 2 fino a circa 16. E.g. Vector2 (2 * T), Vector3 (3 * T), Vector4 (4 * T), ColourI32 (4), LightValue16 (2), Tile (2), ecc. (Dimensioni in byte tra parentesi).Prestazioni oggetti piccoli C++

Stava facendo un po 'di profiling (basato su campioni) che mi ha portato ad alcune funzioni più lente del previsto, ad es.

//4 bits per channel natural light and artificial RGB 
class LightValue16 
{ 
... 
    explicit LightValue16(uint16_t value); 
    LightValue16(const LightValueF &); 
    LightValue16(int r, int g, int b, int natural); 

    int natural()const; 
    void natural(int v); 
    int artificialRed()const; 
    ... 
    uint16_t data; 
}; 
... 
LightValue16 World::getLight(const Vector3I &pos) 
{ ... } 

Questa funzione esegue alcuni calcoli di ricercare il valore tramite una coppia di matrici, con alcuni valori predefiniti per sopra la parte popolata del mondo. I contenuti sono ben adattati e osservando l'aspetto del disassemblaggio quanto di meglio si può ottenere. Con circa 100 istruzioni. Tuttavia una cosa spiccava, su tutti i siti di ritorno è stato attuato con qualcosa di simile:

mov eax, dword pyt [ebp + 8] 
mov cx, word ptr[ecx + edx * 2] ; or say mov ecx, Fh 
mov word ptr [eax], cx 
pop ebp 
ret 10h 

Per x64 ho visto più o meno la stessa cosa. Non ho controllato la mia build GCC, ma sospetto che faccia più o meno la stessa cosa.

Ho fatto un po 'di sperimentazione e ho trovato usando un tipo di ritorno uint16_t. In effetti, la funzione World :: getLight si è in linea (sembravano praticamente le stesse 80 istruzioni di base o così, nessun cheat con condizionali/cicli diversi) e l'utilizzo totale della CPU per la funzione esterna che stavo investigando per passare da 16,87 Da% a 14.04% Mentre posso farlo basandomi caso per caso (insieme a provare la forza di cose in linea, suppongo), ci sono dei modi pratici per evitare problemi di questo tipo per cominciare? Forse anche ottenere un paio di% più veloce attraverso l'intero codice?

Il meglio che riesco a pensare solo ora è quello di usare solo i tipi primitivi in ​​questi casi (< 4 o forse 8 byte oggetti) e spostare tutti gli elementi del membro corrente in funzioni non membro, quindi più come fatto in C , solo con namespace.

Pensando a questo credo che ci sia spesso un costo per cose come "t foo (const Vector3f & p)" over "t foo (float x, float y, z galleggiante)"? E se è così, su un programma che usa estensivamente il const &, potrebbe aggiungere una differenza significativa?

+0

bene , la differenza nel caso dichiarato è che si restituisce un oggetto con tutto il sovraccarico associato mentre si restituisce un int unsigned a 16 bit. Dato che per il primo caso, devi copiare l'intero oggetto piuttosto che solo l'int, mi aspetto che consumi un po 'più di tempo CPU anche quando RVO entra in gioco. –

+0

L'allocazione dell'oggetto in stack anziché heap può influire sulle prestazioni in questa situazione? –

+0

Timo: perché l'oggetto completo richiede più di 2 byte di memoria? Il compilatore non dovrebbe mettere un vtable in là, penserei. –

risposta

0

Nei commenti a questa domanda c'è già stata molta discussione, se il compilatore è autorizzato a gestire class LightValue16 come un semplice uint16_t per la funzione analizzata.

Se la classe contiene nessuna magia speciale (come funzioni virtuali) e tutta la classe è visibile alla funzione di analisi, il compilatore può produrre codice che è 100% altrettanto efficiente quindi solo con un `tipo uint16_t.

Il problema è "possibile". Sebbene tutti i compilatori decenti generino di solito un codice che è al 100% così veloce, ci saranno situazioni sporadiche in cui alcune ottimizzazioni non saranno applicate o almeno il codice risultante sarà diverso. Potrebbe essere solo un parametro di una modifica euristica (ad esempio non verrà applicato inline perché solo un po 'più codice in qualche passaggio di ottimizzazione rimane a causa della classe) o qualche passaggio di ottimizzazione richiede solo un semplice tipo numerico in questa fase , che non è nemmeno un vero bug nel compilatore. Ad esempio, se si aggiunge un "modello < bool NotUsed>" alla propria classe sopra, questo probabilmente cambierà i passaggi di ottimizzazione all'interno di un compilatore, sebbene semanticamente il programma non cambi.

Quindi, se si vuole essere sicuri al 100%, utilizzare solo int o double direttamente.Ma nel 90% delle volte sarà veloce al 100%, solo nel 10% sarà solo il 90% della performance, che dovrebbe essere O.K. per il 99% percento (ma non il 100%) di tutti i casi d'uso.

2

Dai uno sguardo allo Itanium C++ ABI. Mentre il tuo computer non ha sicuramente un processore Itanium, gcc modella l'ABI x86 e x86-64 molto simile a Itanium ABI. La sezione collegata afferma che

Tuttavia, se il tipo di valore di ritorno ha un costruttore di copia non banale o distruttore, [ritornare nella memoria del chiamante fornito accade]

Per scoprire che cosa non banale copiare il costruttore o il distruttore significa, dare un'occhiata a What are Aggregates and PODs and how/why are they special? e dare un'occhiata alle regole per una classe di essere "banalmente copiabile". Nel tuo caso, il problema è il costruttore di copie che hai definito. Non dovrebbe essere affatto necessario, il compilatore sintetizzerà un costruttore di copia che assegna solo il membro data secondo necessità. Se si vuole dichiarare esplicitamente che si desidera un costruttore di copia, e se si utilizza C++ 11, è possibile anche scrivere in giù come funzione di default, che non lo rende non banale:

LigthValue16(const LightValue16 & other) = default;