Quali sono le differenze di prestazioni tra numeri interi senza segno e con segno? [chiuso]


42

Sono consapevole del successo delle prestazioni quando si mescolano ints firmati con float.

È peggio mescolare ints non firmati con float?

C'è qualche hit nel mixare firmato / non firmato senza float?

Le diverse dimensioni (u32, u16, u8, i32, i16, i8) influiscono sulle prestazioni? Su quali piattaforme?


2
Ho rimosso il testo / tag specifico per PS3, perché questa è una buona domanda su qualsiasi architettura, e la risposta è vera per tutte le architetture che separano i registri di numeri interi e virgola mobile, che sono praticamente tutti.

Risposte:


36

La grande penalità nel mescolare ints (di qualsiasi tipo) e float è perché questi sono in set di registri diversi. Per passare da un registro impostato all'altro, è necessario scrivere il valore in memoria e rileggerlo, il che comporta uno stallo load-hit-store .

Passare tra dimensioni diverse o firmare ints mantiene tutto nello stesso set di registri, in modo da evitare la grande penalità. Potrebbero esserci penalità minori a causa delle estensioni dei segni, ecc., Ma sono molto più piccole di un negozio con hit da carico.


L'articolo che hai collegato afferma che il processore per celle PS3 è un'eccezione perché a quanto pare tutto è archiviato nello stesso set di registri (può essere trovato approssimativamente nel mezzo dell'articolo o cercare "Cella").
Bummzack,

4
@bummzack: vale solo per gli SPE, non per i DPI; le SPE hanno un ambiente a virgola mobile molto speciale e il cast è ancora relativamente costoso. Inoltre, i costi sono sempre gli stessi per gli interi con segno o senza segno.

Questo è un buon articolo ed è importante sapere di LHS (e lo sto votando per quello) ma la mia domanda riguarda le sanzioni relative ai segni. So che sono piccoli e probabilmente trascurabili, ma mi piacerebbe comunque vedere alcuni numeri reali o riferimenti su di essi.
Luis

1
@Luis - Stavo cercando di trovare alcuni documenti pubblici su questo, ma non riesco a trovarli al momento. Se hai accesso alla documentazione Xbox360, c'è un buon white paper di Bruce Dawson che copre parte di questo (ed è molto buono in generale).
Celion,

@Luis: ho pubblicato un'analisi di seguito, ma se ti soddisfa, per favore dai a celion la risposta: tutto ciò che ha detto è corretto, tutto ciò che ho fatto è stato eseguire GCC alcune volte.

12

Sospetto che le informazioni su Xbox 360 e PS3 in particolare saranno nascoste dietro muri solo per sviluppatori autorizzati, come la maggior parte dei dettagli di basso livello. Tuttavia, possiamo costruire un programma x86 equivalente e smontarlo per avere un'idea generale.

Innanzitutto, vediamo quali costi di ampliamento senza segno:

unsigned char x = 1;
unsigned int y = 1;
unsigned int z;
z = x;
z = y;

La parte pertinente si disassembla (utilizzando GCC 4.4.5):

    z = x;
  27:   0f b6 45 ff             movzbl -0x1(%ebp),%eax
  2b:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  2e:   8b 45 f8                mov    -0x8(%ebp),%eax
  31:   89 45 f4                mov    %eax,-0xc(%ebp)

Quindi praticamente lo stesso: in un caso spostiamo un byte, nell'altro spostiamo una parola. Il prossimo:

signed char x = 1;
signed int y = 1;
signed int z;
z = x;
z = y;

Diventa:

   z = x;
  11:   0f be 45 ff             movsbl -0x1(%ebp),%eax
  15:   89 45 f4                mov    %eax,-0xc(%ebp)
    z = y;
  18:   8b 45 f8                mov    -0x8(%ebp),%eax
  1b:   89 45 f4                mov    %eax,-0xc(%ebp)

Quindi il costo dell'estensione del segno è qualunque sia il costo movsblpiuttosto che lo movzblè - livello di sub-istruzione. Questo è sostanzialmente impossibile da quantificare sui processori moderni a causa del modo in cui funzionano i processori moderni. Tutto il resto, che va dalla velocità della memoria alla memorizzazione nella cache a ciò che era in precedenza nella pipeline, dominerà il runtime.

In ~ 10 minuti mi sono voluti scrivere questi test, avrei potuto facilmente trovare un vero bug di prestazione, e non appena accendo qualsiasi livello di ottimizzazione del compilatore, il codice diventa irriconoscibile per compiti così semplici.

Questo non è Stack Overflow, quindi spero che nessuno qui affermi che la microottimizzazione non ha importanza. I giochi spesso funzionano su dati molto grandi e molto numerici, quindi un'attenta attenzione alla ramificazione, ai cast, alla pianificazione, all'allineamento della struttura e così via può apportare miglioramenti molto critici. Chiunque abbia trascorso molto tempo a ottimizzare il codice PPC probabilmente ha almeno una storia horror sui negozi di successo. Ma in questo caso, non importa davvero. La dimensione di archiviazione del tipo intero non influisce sulle prestazioni, purché sia ​​allineata e si adatti a un registro.


2
(CW perché questo è davvero solo un commento sulla risposta di Celion, e perché sono curioso di sapere quali modifiche al codice le persone potrebbero avere per renderlo più illustrativo.)

Le informazioni sulla CPU PS3 sono prontamente e legalmente disponibili, quindi la discussione delle cose relative alla CPU relative a PS3 non è un problema. Fino a quando Sony non ha rimosso il supporto OtherOS, chiunque poteva attaccare Linux su una PS3 e programmarlo. La GPU era off limits, ma la CPU (comprese le SPE) va bene. Anche senza il supporto di OtherOS puoi facilmente prendere il GCC appropriato e vedere com'è il code-gen.
JasonD,

@Jason: ho contrassegnato il mio post come CW, quindi se qualcuno lo fa può fornire le informazioni. Tuttavia, chiunque abbia accesso al compilatore GameOS ufficiale di Sony - che è davvero l'unico che conta - è probabilmente escluso dal farlo.

In realtà l'intero con segno è più costoso su PPC IIRC. Ha un piccolo impatto sulle prestazioni, ma è lì ... anche molti dettagli su PP3 / SPU PS3 sono qui: jheriko-rtw.blogspot.co.uk/2011/07/ps3-ppuspu-docs.html e qui: jheriko-rtw.blogspot.co.uk/2011/03/ppc-instruction-set.html . Curioso che cos'è questo compilatore GameOS? Quello è il compier GCC o quello SNC? Oltre alle cose già menzionate, i confronti firmati hanno un sovraccarico quando si parla di ottimizzare i cicli più interni. Non ho accesso ai documenti che descrivono questo però - e anche se lo facessi ...
jheriko

4

Le operazioni con numeri interi firmati possono essere più costose su quasi tutte le architetture. Ad esempio, la divisione per una costante è più veloce se non firmata, ad esempio:

unsigned foo(unsigned a) { return a / 1024U; }

sarà ottimizzato per:

unsigned foo(unsigned a) { return a >> 10; }

Ma...

int foo(int a) { return a / 1024; }

ottimizzerà per:

int foo(int a) {
  return (a + 1023 * (a < 0)) >> 10;
}

o su sistemi in cui la ramificazione è economica,

int foo(int a) {
  if (a >= 0) return a >> 10;
  else return (a + 1023) >> 10;
}

Lo stesso vale per il modulo. Questo vale anche per i non-poteri-di-2 (ma l'esempio è più complesso). Se la tua architettura non ha una divisione hardware (ad es. La maggior parte di ARM), anche le divisioni senza segno di non-cons sono più veloci.

In generale, dire al compilatore che non possono derivare numeri negativi aiuterà l'ottimizzazione delle espressioni, specialmente quelle usate per la terminazione del loop e altri condizionali.

Per quanto riguarda i formati di dimensioni diverse, sì, c'è un leggero impatto, ma dovresti pesarlo rispetto a spostare meno memoria. In questi giorni probabilmente guadagni di più accedendo a meno memoria di quanto perdi dall'espansione delle dimensioni. Ti interessa molto la microottimizzazione a quel punto.


Ho modificato il tuo codice ottimizzato per riflettere maggiormente ciò che GCC effettivamente genera, anche su -O0. Avere un ramo è stato fuorviante quando un test + lea ti consente di farlo senza rami.

2
Su x86, forse. Su ARMv7 viene eseguito solo in modo condizionale.
John Ripley,

3

Le operazioni con int con o senza segno hanno lo stesso costo sui processori attuali (x86_64, x86, powerpc, arm). Sul processore a 32 bit, u32, u16, u8 s32, s16, s8 dovrebbero essere gli stessi. Puoi avere penalità con un cattivo allineamento.

Ma convertire int in float o float in int è un'operazione costosa. Puoi facilmente trovare un'implementazione ottimizzata (SSE2, Neon ...).

Il punto più importante è probabilmente l'accesso alla memoria. Se i tuoi dati non rientrano nella cache L1 / L2, perderai più cicli che conversioni.


2

Jon Purdy dice sopra (non posso commentare) che unsigned potrebbe essere più lento perché non può traboccare. Non sono d'accordo, l'aritmetica senza segno è l'aritmetica moolare semplice modulo 2 per il numero di bit nella parola. Le operazioni firmate in linea di principio possono subire overflow, ma di solito sono disattivate.

A volte puoi fare cose intelligenti (ma non molto leggibili) come impacchettare due o più elementi di dati in un int e ottenere più operazioni per istruzione (pocket arithmetic). Ma devi capire cosa stai facendo. Ovviamente MMX ti consente di farlo in modo naturale. Ma a volte l'uso della più grande dimensione delle parole supportata da HW e l'imballaggio manuale dei dati ti dà l'implementazione più veloce.

Fare attenzione all'allineamento dei dati. Sulla maggior parte delle implementazioni HW carichi e negozi non allineati sono più lenti. Allineamento naturale, significa che per dire una parola di 4 byte, l'indirizzo è un multiplo di quattro e gli indirizzi di parola di otto byte devono essere multipli di otto byte. Ciò si ripercuote su SSE (128 bit favorisce l'allineamento a 16 byte). AVX estenderà presto queste dimensioni dei registri "vettoriali" a 256 bit, quindi a 512 bit. E i carichi / depositi allineati saranno più veloci di quelli non allineati. Per i fanatici di HW, un'operazione di memoria non allineata può estendersi a cose come la cacheline e persino i confini della pagina, per i quali l'HW deve stare attento.


1

È leggermente meglio usare numeri interi con segno per gli indici di loop, perché l'overflow con segno non è definito in C, quindi il compilatore supporrà che tali loop abbiano un numero inferiore di casi angolari. Questo è controllato da "-fstrict-overflow" di gcc (abilitato di default) e l'effetto è probabilmente difficile da notare senza leggere l'output dell'assembly.

Oltre a ciò, x86 funziona meglio se non mescoli tipi, perché può usare operandi di memoria. Se deve convertire tipi (segno o zero estensioni) ciò significa un carico esplicito e l'uso di un registro.

Attenersi a int per le variabili locali e la maggior parte di ciò avverrà per impostazione predefinita.


0

Come sottolinea celion, il sovraccarico della conversione tra ints e float ha in gran parte a che fare con la copia e la conversione dei valori tra i registri. L'unico sovraccarico di ints non firmati in sé e per sé deriva dal loro comportamento avvolgente garantito, che richiede una certa quantità di controllo di overflow nel codice compilato.

Non vi è praticamente alcun sovraccarico nella conversione tra numeri interi con o senza segno. Diverse dimensioni di numeri interi possono essere (infinitesimalmente) più veloci o più lente per accedere a seconda della piattaforma. In generale, la dimensione dell'intero più vicino alla dimensione della parola della piattaforma sarà l' accesso più veloce , ma la differenza di prestazioni complessive dipende da molti altri fattori, in particolare la dimensione della cache: se si utilizza uint64_tquando tutto ciò che serve è uint32_t, potrebbe sia che meno dei tuoi dati si adatteranno immediatamente alla cache e potresti incorrere in un sovraccarico di carico.

È un po 'eccessivo anche solo pensarci. Se usi tipi appropriati per i tuoi dati, le cose dovrebbero funzionare perfettamente e la quantità di energia che si può ottenere selezionando tipi basati sull'architettura è comunque trascurabile.


A quale controllo di overflow ti riferisci? A meno che tu non intenda un livello inferiore a quello dell'assemblatore, il codice per aggiungere due ints è identico sulla maggior parte dei sistemi e non molto più a lungo su quelli che usano, ad esempio, magnitudo dei segni. Solo diverso.

@JoeWreschnig: Accidenti. Non riesco a trovarlo, ma so di aver visto esempi di output di assemblatori diversi che spiegano il comportamento avvolgente definito, almeno su alcune piattaforme. L'unico post correlato che ho trovato: stackoverflow.com/questions/4712315/…
Jon Purdy,

L'output dell'assembler diverso per un diverso comportamento avvolgente è dovuto al fatto che il compilatore può apportare ottimizzazioni nel caso con segno che, ad esempio se b> 0 quindi a + b> a, perché l'overflow con segno non è definito (e quindi non è possibile fare affidamento su di esso). È davvero una situazione completamente diversa.
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.