Un confronto 1 <10 è meno costoso di 1 <1000000?


65

Ho appena usato ~ 1 miliardo come conteggio per un z-indexCSS e pensavo ai confronti che dovevano continuare. C'è una differenza nelle prestazioni a livello di ALU nei confronti tra numeri molto grandi rispetto a numeri molto piccoli?

Ad esempio, uno di questi due frammenti sarebbe più costoso dell'altro?

snippet 1

for (int i = 0; i < 10000000; i++){
    if (i < 10000000000000) {
        //do nothing
    }
}

snippet 2

for (int i = 0; i < 10000000; i++){
    if (i < 1000) {
        //do nothing
    }
}


12
OP non chiede quanto tempo impiegherà la ramificazione. Chiaramente, l'esempio ha lo scopo di garantire che impieghi esattamente lo stesso tempo in entrambi i frammenti. La domanda è se le singole CMPistruzioni della macchina saranno più lente se più igrandi.
Kilian Foth,

18
Dato che ciò avviene nei CSS, la conversione di una stringa in un numero intero probabilmente dominerà l'operazione di confronto stessa in termini di tempo impiegato per l'esecuzione.

58
Se dovevi usare 1000000000 come indice z in un file CSS, hai fatto qualcosa di sbagliato.
Bergi,

6
Per i CSS, l'overhead della conversione del testo in un numero intero dipenderà dal numero di cifre da convertire (dove un numero di 6 cifre come 1000000 può essere circa 6 volte più costoso di un numero di 1 cifra come 1); e questo sovraccarico può essere ordini di grandezza più grandi del sovraccarico di confronti interi.
Brendan,

Risposte:


82

Ogni processore su cui ho lavorato fa il confronto sottraendo uno degli operandi dall'altro, scartando il risultato e lasciando soli i flag del processore (zero, negativo, ecc.). Poiché la sottrazione viene eseguita come una singola operazione, il contenuto degli operandi non ha importanza.

Il modo migliore per rispondere con certezza alla domanda è compilare il codice in assembly e consultare la documentazione del processore di destinazione per le istruzioni generate. Per le attuali CPU Intel, sarebbe il Manuale per gli sviluppatori del software per architetture Intel 64 e IA-32 .

La descrizione CMPdell'istruzione ("confronta") si trova nel volume 2A, pagina 3-126 o pagina 618 del PDF e descrive il suo funzionamento come:

temp ← SRC1 − SignExtend(SRC2);
ModifyStatusFlags; (* Modify status flags in the same manner as the SUB instruction*)

Ciò significa che, se necessario, il secondo operando viene esteso con il segno, sottratto dal primo operando e il risultato collocato in un'area temporanea nel processore. Quindi i flag di stato vengono impostati nello stesso modo in cui verrebbero per l' SUBistruzione ("sottrazione") (pagina 1492 del PDF).

Nella documentazione CMPo nella SUBdocumentazione non si fa menzione del fatto che i valori degli operandi incidono sulla latenza, pertanto qualsiasi valore utilizzato è sicuro.


5
Cosa succede se il numero diventa troppo grande per l'aritmetica a 32 bit? Non sarebbe quindi suddiviso in un calcolo più lento?
Falco,

3
@Falco Non su una CPU con un ALU a 64 bit (che è praticamente tutti tranne lo spazio incorporato in questi giorni.)
Reirab

8
@Falco: Sì, ma poiché la domanda si pone sulle prestazioni di ALU, l'implicazione è che i valori si adattano alla dimensione delle parole della CPU o alle capacità di qualsiasi istruzione SIMD che potrebbe avere. Operare su un numero maggiore di quello dovrebbe essere implementato con più istruzioni al di fuori della CPU. Questo era molto comune 30 anni fa quando avevi solo registri a 8 o 16 bit con cui lavorare.
Blrfl

6
@Falco Come richiederebbe il debug? Non è un bug; è solo un po 'più lento eseguire operazioni a 64 bit su una CPU che non supporta nativamente operazioni a 64 bit. Suggerire che non si dovrebbe mai usare un numero superiore a 2 ^ 31-1 sembra un po 'ridicolo.
reirabio

2
@Falco Detto questo, i motori di rendering nei browser usano persino numeri interi per rappresentare gli z-index? La maggior parte dei motori di rendering che conosco utilizzano float a precisione singola per tutto (fino alla fase finale di rasterizzazione), ma non ho studiato i motori di rendering del browser.
Reirab

25

C'è una differenza nelle prestazioni a livello di ALU nei confronti tra numeri molto grandi rispetto a numeri molto piccoli?

È molto improbabile, a meno che passare da un numero piccolo a un numero elevato cambi il tipo numerico, diciamo da inta a long. Anche allora, la differenza potrebbe non essere significativa. È più probabile vedere una differenza se il tuo linguaggio di programmazione passa silenziosamente all'aritmetica di precisione arbitraria sotto le copertine.

Tuttavia, il tuo particolare compilatore potrebbe eseguire alcune intelligenti ottimizzazioni di cui non sei a conoscenza. Il modo in cui lo scopri è misurare. Esegui un profiler sul tuo codice; vedere quali confronti richiedono più tempo. O semplicemente avviare e arrestare un timer.


Va detto che i numeri proposti nella domanda sono di diverso tipo numerico in un tipico tipo intero a 32 bit ...
Falco,

19

Molti processori dispongono di "piccole" istruzioni che possono eseguire operazioni aritmetiche, compresi i confronti, su determinati operandi specificati immediatamente. Operandi diversi da questi valori speciali devono utilizzare un formato di istruzione più grande o, in alcuni casi, utilizzare un'istruzione "Carica valore dalla memoria". Nel set di istruzioni ARM Cortex-M3, ad esempio, ci sono almeno cinque modi in cui un valore può essere confrontato con una costante:

    cmp r0,#1      ; One-word instruction, limited to values 0-255

    cmp r0,#1000   ; Two-word instruction, limited to values 0-255 times a power of 2

    cmn r0,#1000   ; Equivalent to comparing value with -1000
                   ; Two-word instruction, limited to values 0-255 times a power of 2

    mov r1,#30000  ; Two words; can handle any value 0-65535
    cmp r0,r1      ; Could use cmn to compare to values -1 to -65535

    ldr r1,[constant1000000] ; One or two words, based upon how nearby the constant is
    cmp r0,r1
    ...

constant1000000:
    dd  1000000

La prima forma è la più piccola; la seconda e la terza forma possono o non possono essere eseguite così rapidamente, a seconda della velocità della memoria da cui viene prelevato il codice. La quarta forma sarà quasi certamente più lenta delle prime tre e la quinta forma anche più lenta, ma quest'ultima può essere utilizzata con qualsiasi valore a 32 bit.

Sui vecchi processori x86, le istruzioni di confronto in formato breve verrebbero eseguite più velocemente di quelle in formato lungo, ma molti processori più recenti convertiranno sia i moduli lunghi che quelli corti nella stessa rappresentazione quando vengono recuperati per la prima volta, e memorizzeranno quella rappresentazione uniforme nella cache. Pertanto, mentre i controller integrati (come quelli presenti su molte piattaforme mobili) avranno una differenza di velocità, molti computer basati su x86 non lo faranno.

Si noti inoltre che in molti casi in cui una costante viene utilizzata pesantemente all'interno di un ciclo, un compilatore dovrà caricare la costante in un registro solo una volta - prima dell'inizio del ciclo - rendendo discutibili le distinzioni temporali. D'altra parte, ci sono alcune situazioni, anche in piccoli anelli, in cui ciò non accadrà sempre; se un ciclo è piccolo ma eseguito pesantemente, a volte può esserci una prestazione importante tra confronti che coinvolgono valori immediati brevi e quelli che coinvolgono valori più lunghi.


Su MIPS puoi avere solo immediati a 16 bit, quindi sicuramente il confronto con 1 sarà più breve e (probabilmente) più veloce di 1000000. Forse lo stesso con Sparc e PowerPC. E penso di aver letto da alcune fonti che Intel ottimizza anche le operazioni su piccoli immediati in diversi casi, ma non sono sicuro per il confronto o no
phuclv,

@ LưuVĩnhPhúc: un registro può essere caricato prima del loop. A quel punto, il confronto effettivo sarà lo stesso numero di istruzioni in entrambi i casi.
cao

Dato che il loop era solo un esempio dell'op e la domanda era ad esempio un indice z, se si hanno 1000 oggetti, ognuno con il proprio indice z e li si imposta su 100000000 ... 1000000999 o su 10000 ... 10999 e li si passa sopra per l'ordinamento prima del rendering, ci sono molti confronti e molte istruzioni di caricamento. Lì potrebbe fare la differenza!
Falco,

@Falco: In tal caso, gli immediati non avrebbero nemmeno preso in considerazione; il caricamento e il confronto con un registro sembra praticamente inevitabile.
cHao,

@cHao: se uno sta confrontando gli indici Z uno con l'altro, sarebbero nei registri. Se si gestiscono in modo diverso determinati intervalli di indici, ciò potrebbe comportare confronti immediati. Normalmente le costanti verrebbero caricate prima dell'inizio di un ciclo, ma se ad esempio uno avesse un ciclo che doveva leggere coppie di valori dalla memoria e confrontare il primo valore di ciascuna coppia con cinque diverse costanti (non uniformemente distanziate) nell'intervallo 100000 a 100499 e l'altro valore con altre cinque costanti del genere, potrebbe essere molto più veloce sottrarre 100250 (tenuto in un registro) e confrontarlo con i valori da -250 a 250 ...
supercat

5

La risposta breve a questa domanda è, no , non c'è alcuna differenza di tempo per confrontare due numeri in base alla grandezza di quei numeri supponendo che siano memorizzati nello stesso tipo di dati (ad es. Entrambi in 32 bit o entrambi in 64 bit).

Inoltre, fino alla dimensione della parola dell'ALU , è incredibilmente improbabile che il confronto di due numeri interi tra loro richieda mai più di 1 ciclo di clock, poiché si tratta di un'operazione banale equivalente a una sottrazione. Penso che ogni architettura con cui abbia mai avuto a che fare abbia avuto un confronto di interi a ciclo singolo.

Gli unici casi a cui riesco a pensare che ho riscontrato in cui un confronto di due numeri non era un'operazione a ciclo singolo sono i seguenti:

  • Istruzioni in cui esiste effettivamente una latenza di memoria nel recupero degli operandi, ma ciò non ha nulla a che fare con il funzionamento del confronto stesso (e generalmente non è possibile sulle architetture RISC, sebbene sia generalmente possibile sui progetti CISC, come x86 / x64).
  • I confronti in virgola mobile possono essere multi-ciclo, a seconda dell'architettura.
  • I numeri in questione non si adattano alla dimensione delle parole della ALU e, quindi, il confronto deve essere suddiviso in più istruzioni.

4

@ La risposta di RobertHarvey è buona; considera questa risposta un supplemento alla sua.


Dovresti anche considerare Branch Prediction :

Nell'architettura del computer, un predittore di diramazione è un circuito digitale che tenta di indovinare in quale direzione andrà un ramo (ad es. Una struttura if-then-else) prima che questo sia noto. Lo scopo del predittore di diramazione è migliorare il flusso nella pipeline di istruzioni. I predittori di filiali svolgono un ruolo fondamentale nel raggiungimento di elevate prestazioni efficaci in molte architetture moderne a microprocessore pipeline come x86.

Fondamentalmente, nel tuo esempio, se l' ifistruzione all'interno del ciclo restituisce sempre la stessa risposta, il sistema può ottimizzarla indovinando correttamente in che modo si diramerà. Nel tuo esempio, poiché l' ifistruzione nel primo caso restituisce sempre lo stesso risultato, verrà eseguita leggermente più velocemente del secondo caso.

Ottima domanda Stack Overflow sull'argomento


La previsione del ramo influenza il tempo di ramificazione, ma non il tempo di confronto stesso.
Reirab

3

Dipende dall'attuazione, ma sarebbe molto, molto improbabile .

Ammetto di non aver letto i dettagli di implementazione dei vari motori di browser e che CSS non specifica alcun tipo particolare di archiviazione per i numeri. Ma credo che sia sicuro presumere che tutti i principali browser stiano utilizzando numeri a virgola mobile a precisione doppia a 64 bit ("double", per prendere in prestito un termine da C / C ++) per gestire la maggior parte delle loro esigenze numeriche nei CSS , perché questo è ciò che JavaScript utilizza per i numeri e quindi l'utilizzo dello stesso tipo semplifica l'integrazione.

Dal punto di vista del computer, tutti i doppi portano la stessa quantità di dati: 64 bit, sia che il valore sia 1 o -3,14 o 1000000 o 1e100 . Il tempo necessario per eseguire un'operazione su questi numeri non dipende dal valore effettivo di tali numeri, perché funziona sempre sulla stessa quantità di dati. C'è un compromesso nel fare le cose in questo modo, in quanto i doppi non possono rappresentare accuratamente tutti i numeri (o anche tutti i numeri all'interno del loro intervallo), ma possono avvicinarsi abbastanza per la maggior parte degli argomenti e il tipo di cose che CSS non fa numericamente - abbastanza esigente da richiedere più precisione di così. Combina questo con i vantaggi della compatibilità diretta con JavaScript e avrai un caso abbastanza forte per i doppi.

Non è impossibile che qualcuno possa implementare CSS usando una codifica a lunghezza variabile per i numeri. Se qualcuno ha utilizzato una codifica a lunghezza variabile, quindi confrontandoli piccoli numeri sarebbe meno costoso rispetto al confronto contro grandi numeri, perché grandi numeri hanno più dati da macinati . Questi tipi di codifiche possono essere più precisi di quelli binari, ma sono anche molto più lenti e, in particolare per i CSS, i guadagni di precisione probabilmente non sono sufficienti per valere il risultato prestazionale. Sarei molto sorpreso di apprendere che qualsiasi browser ha fatto le cose in questo modo.

Ora, in teoria, c'è una possibile eccezione a tutto ciò che ho detto sopra: il confronto con lo zero è spesso più veloce rispetto al confronto con altri numeri . Questo non è perché zero è breve (se quello era il motivo, allora 1 dovrebbe essere altrettanto veloce, ma non lo è). È perché zero ti lascia imbrogliare. È l'unico numero in cui tutti i bit sono disattivati, quindi se sai che uno dei valori è zero, non devi nemmeno guardare l'altro valore come un numero: se uno dei bit su allora non è uguale a zero, quindi devi solo guardare un bit per vedere se è maggiore o minore di zero.


0

Se questo codice viene interpretato ogni volta che viene eseguito, ci sarebbe una differenza in quanto impiega più tempo a tokenizzare e interpretare 10000000000000rispetto a 1000. Tuttavia, questa è l'ovvia prima ottimizzazione degli interpreti in questo caso: tokenizzare una volta e interpretare i token.

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.