Il modo più veloce per determinare se un numero intero è compreso tra due numeri interi (inclusi) con insiemi di valori noti


390

Esiste un modo più veloce rispetto x >= start && x <= enda C o C ++ per verificare se un numero intero è compreso tra due numeri interi?

AGGIORNAMENTO : la mia piattaforma specifica è iOS. Questo fa parte di una funzione di sfocatura del riquadro che limita i pixel a un cerchio in un determinato quadrato.

AGGIORNAMENTO : Dopo aver provato la risposta accettata , ho ottenuto un ordine di accelerazione dell'entità su una riga di codice rispetto a farlo normalmente x >= start && x <= end.

AGGIORNAMENTO : Ecco il codice after e before con assemblatore da XCode:

NUOVO MODO

// diff = (end - start) + 1
#define POINT_IN_RANGE_AND_INCREMENT(p, range) ((p++ - range.start) < range.diff)

Ltmp1313:
 ldr    r0, [sp, #176] @ 4-byte Reload
 ldr    r1, [sp, #164] @ 4-byte Reload
 ldr    r0, [r0]
 ldr    r1, [r1]
 sub.w  r0, r9, r0
 cmp    r0, r1
 blo    LBB44_30

VECCHIO MODO

#define POINT_IN_RANGE_AND_INCREMENT(p, range) (p <= range.end && p++ >= range.start)

Ltmp1301:
 ldr    r1, [sp, #172] @ 4-byte Reload
 ldr    r1, [r1]
 cmp    r0, r1
 bls    LBB44_32
 mov    r6, r0
 b      LBB44_33
LBB44_32:
 ldr    r1, [sp, #188] @ 4-byte Reload
 adds   r6, r0, #1
Ltmp1302:
 ldr    r1, [r1]
 cmp    r0, r1
 bhs    LBB44_36

Abbastanza sorprendente come la riduzione o l'eliminazione delle ramificazioni possa fornire una velocità così drammatica.


28
Perché sei preoccupato che questo non sia abbastanza veloce per te?
Matt Ball,

90
Chi se ne frega perché, è una domanda interessante. È solo una sfida per il gusto di una sfida.
David dice di reintegrare Monica il

46
@SLaks Quindi dovremmo semplicemente ignorare tutte queste domande alla cieca e dire semplicemente "lasciare che l'ottimizzatore lo faccia?"
David dice Reinstate Monica il

87
non importa perché la domanda venga posta. È una domanda valida, anche se la risposta è no
martedì

42
Questo è un collo di bottiglia in una funzione di una delle mie app
jjxtra,

Risposte:


528

C'è un vecchio trucco per farlo con un solo confronto / ramo. Se migliorerà davvero la velocità può essere discutibile, e anche se lo fa, probabilmente è troppo poco per accorgersene o preoccuparsene, ma quando inizi solo con due confronti, le possibilità di un enorme miglioramento sono piuttosto remote. Il codice è simile a:

// use a < for an inclusive lower bound and exclusive upper bound
// use <= for an inclusive lower bound and inclusive upper bound
// alternatively, if the upper bound is inclusive and you can pre-calculate
//  upper-lower, simply add + 1 to upper-lower and use the < operator.
    if ((unsigned)(number-lower) <= (upper-lower))
        in_range(number);

Con un tipico computer moderno (ovvero qualsiasi cosa che usa il complemento a due), la conversione in unsigned è davvero un nop - solo un cambiamento nel modo in cui vengono visualizzati gli stessi bit.

Si noti che in un caso tipico, è possibile pre-calcolare upper-loweral di fuori di un ciclo (presunto), in modo che normalmente non contribuisca un tempo significativo. Oltre a ridurre il numero di istruzioni del ramo, anche questo (generalmente) migliora la previsione del ramo. In questo caso, viene preso lo stesso ramo se il numero è al di sotto dell'estremità inferiore o al di sopra dell'estremità superiore dell'intervallo.

Per quanto riguarda il modo in cui funziona, l'idea di base è piuttosto semplice: un numero negativo, se visto come un numero senza segno, sarà più grande di tutto ciò che è iniziato come un numero positivo.

In pratica questo metodo si traduce numbere l'intervallo al punto di origine e controlla se numberè nell'intervallo [0, D], dove D = upper - lower. Se numbersotto il limite inferiore: negativo e se sopra il limite superiore: maggiore diD .


8
@ TomásBadan: Saranno entrambi un ciclo su qualsiasi macchina ragionevole. Ciò che è costoso è la filiale.
Oliver Charlesworth,

3
La ramificazione aggiuntiva viene eseguita a causa di corto circuito? In tal caso, lower <= x & x <= upper(invece di lower <= x && x <= upper) si tradurrebbe anche in prestazioni migliori?
Markus Mayr,

6
@ AK4749, jxh: bello come questo pepita, esito a votare, perché purtroppo non c'è nulla che suggerisca che questo sia più veloce in pratica (fino a quando qualcuno non fa un confronto tra l'assemblatore risultante e le informazioni di profilazione). Per quanto ne sappiamo, il compilatore del PO può rendere il codice del PO con un unico opcode di ramo ...
Oliver Charlesworth,

152
WOW!!! Ciò ha comportato un ordine di miglioramento della grandezza nella mia app per questa specifica riga di codice. Precompilando in alto-in basso il mio profilo è passato dal 25% del tempo di questa funzione a meno del 2%! Il collo di bottiglia ora è operazioni di addizione e sottrazione, ma penso che ora potrebbe essere abbastanza buono :)
jjxtra

28
Ah, ora @PsychoDad ha aggiornato la domanda, è chiaro perché questo è più veloce. Il vero codice ha un effetto collaterale nel confronto, motivo per cui il compilatore non è riuscito a ottimizzare il cortocircuito.
Oliver Charlesworth,

17

È raro essere in grado di fare significative ottimizzazioni per codificare su così piccola scala. Grandi guadagni in termini di prestazioni derivano dall'osservazione e dalla modifica del codice da un livello superiore. Potresti essere in grado di eliminare del tutto la necessità del test di intervallo, oppure esegui solo O (n) invece di O (n ^ 2). Potresti essere in grado di riordinare i test in modo che una parte della disuguaglianza sia sempre implicita. Anche se l'algoritmo è ideale, è più probabile che si verifichino guadagni quando vedi come questo codice esegue il test dell'intervallo 10 milioni di volte e trovi un modo per raggrupparli e utilizzare SSE per eseguire molti test in parallelo.


16
Nonostante i downvotes, rispondo alla mia risposta: l'assembly generato (vedere il link pastebin in un commento alla risposta accettata) è piuttosto terribile per qualcosa nel ciclo interno di una funzione di elaborazione dei pixel. La risposta accettata è un trucco chiaro, ma il suo effetto drammatico è ben oltre ciò che è ragionevole aspettarsi per eliminare una frazione di un ramo per iterazione. Qualche effetto secondario sta dominando, e mi aspetto ancora che un tentativo di ottimizzare l'intero processo su questo test lascerebbe i guadagni di un confronto intelligente della gamma nella polvere.
Ben Jackson,

17

Dipende da quante volte si desidera eseguire il test sugli stessi dati.

Se si esegue il test una sola volta, probabilmente non esiste un modo significativo per accelerare l'algoritmo.

Se lo stai facendo per un set di valori molto limitato, puoi creare una tabella di ricerca. L'esecuzione dell'indicizzazione potrebbe essere più costosa, ma se puoi adattare l'intera tabella nella cache, puoi rimuovere tutte le ramificazioni dal codice, il che dovrebbe accelerare le cose.

Per i tuoi dati la tabella di ricerca sarebbe 128 ^ 3 = 2.097.152. Se riesci a controllare una delle tre variabili in modo da considerare tutte le istanze in cui start = Ncontemporaneamente, la dimensione del working set si riduce a 128^2 = 16432byte, il che dovrebbe adattarsi bene alla maggior parte delle cache moderne.

Dovresti comunque confrontare il codice effettivo per vedere se una tabella di ricerca senza rami è sufficientemente più veloce dei confronti ovvi.


Quindi memorizzeresti una sorta di ricerca dato un valore, inizio e fine e conterrebbe un BOOL che ti dice se fosse nel mezzo?
jjxtra,

Corretta. Sarebbe una tabella di ricerca 3D: bool between[start][end][x]. Se sai come apparirà il tuo modello di accesso (ad esempio x sta aumentando monotonicamente) puoi progettare la tabella per preservare la località anche se l'intera tabella non si adatta alla memoria.
Andrew Prock,

Vedrò se riesco a provare questo metodo e vedere come va. Sto programmando di farlo con un bit vettore per riga in cui il bit verrà impostato se il punto è nel cerchio. Pensi che sarà più veloce di un byte o int32 rispetto al mascheramento dei bit?
jjxtra,

2

Questa risposta è di riferire su un test fatto con la risposta accettata. Ho eseguito un test a intervallo chiuso su un grande vettore di numeri interi casuali ordinati e con mia sorpresa il metodo di base di (basso <= num && num <= alto) è in effetti più veloce della risposta accettata sopra! Il test è stato eseguito su HP Pavilion g6 (AMD A6-3400APU con ram da 6 GB. Ecco il codice principale utilizzato per i test:

int num = rand();  // num to compare in consecutive ranges.
chrono::time_point<chrono::system_clock> start, end;
auto start = chrono::system_clock::now();

int inBetween1{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (randVec[i - 1] <= num && num <= randVec[i])
        ++inBetween1;
}
auto end = chrono::system_clock::now();
chrono::duration<double> elapsed_s1 = end - start;

rispetto a quanto segue che è la risposta accettata sopra:

int inBetween2{ 0 };
for (int i = 1; i < MaxNum; ++i)
{
    if (static_cast<unsigned>(num - randVec[i - 1]) <= (randVec[i] - randVec[i - 1]))
        ++inBetween2;
}

Fai attenzione che randVec sia un vettore ordinato. Per qualsiasi dimensione di MaxNum il primo metodo batte il secondo sulla mia macchina!


1
I miei dati non sono ordinati e i miei test sono su CPU arm iPhone. I risultati con dati e CPU diversi potrebbero differire.
jjxtra,

ordinato nel mio test era solo per assicurarmi che il limite superiore non fosse inferiore al limite inferiore.
Rezeli,

1
I numeri ordinati indicano che la previsione dei rami sarà molto affidabile e farà in modo che tutti i rami siano corretti, tranne alcuni nei punti di commutazione. Il vantaggio del codice branchless è che eliminerà questo tipo di previsioni errate su dati imprevedibili.
Andreas Klebinger,

0

Per qualsiasi controllo dell'intervallo variabile:

if (x >= minx && x <= maxx) ...

È più veloce utilizzare l'operazione bit:

if ( ((x - minx) | (maxx - x)) >= 0) ...

Ciò ridurrà due rami in uno.

Se ti interessa digitare sicuro:

if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...

È possibile combinare più intervalli variabili controllando insieme:

if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...

Ciò ridurrà 4 rami in 1.

È 3,4 volte più veloce di quello vecchio in gcc:

inserisci qui la descrizione dell'immagine


-4

Non è possibile eseguire semplicemente un'operazione bit a bit sull'intero?

Poiché deve essere compreso tra 0 e 128, se è impostato l'ottavo bit (2 ^ 7) è 128 o più. Il caso limite sarà un dolore, tuttavia, poiché si desidera un confronto inclusivo.


3
Vuole sapere se x <= end, dove end <= 128. Non x <= 128.
Ben Voigt,

1
Questa affermazione " Poiché deve essere compresa tra 0 e 128, se è impostato l'8 ° bit (2 ^ 7) è 128 o più " è errata. Prendi in considerazione 256.
Happy Green Kid Naps,

1
Sì, a quanto pare non ci ho pensato abbastanza. Scusate.
acqua ghiacciata
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.