5

Sono interessati a calcolare la sequenza triangolo, che è la sequenza di coppie (i, j): (0, 0), (1, 0), (1, 1), (2, 0), (2, 1) ... che itera se tutte le coppie (i, j) con la limitazione che i >= j. La stessa sequenza con ma con la restrizione i> j è anche interessante.generano rapidamente il "sequenza triangolo": evitando previsioni sbagliate avvengono

Queste sequenze rappresentano, tra le altre cose, tutti i modi per scegliere 2 elementi (possibilmente identici) da un insieme di n elementi (per la sequenza fino a ), o gli indici degli elementi triagular inferiori di una matrice . La sequenza di valori per i da sola è A003056 in OEIS, mentre j da solo è A002262. La sequenza si presenta frequentemente in algoritmi combinatori, in cui le loro prestazioni possono essere critiche.

Un modo semplice ma ramoso per generare il valore successivo nella sequenza è:

if (i == j) { 
    j = 0; 
    i++; 
    } else { 
    j++; 
    }  
} 

Tuttavia, questo soffre di numerosi mispredicts durante il calcolo degli elementi iniziali della sequenza, quando si verifica la condizione (i == j) - generalmente un errore ogni volta che i viene incrementato. All'aumentare della sequenza, il numero di imprevisti si riduce poiché viene incrementato con con frequenza ridotta, pertanto il ramo j++ domina ed è ben previsto. Tuttavia, alcuni tipi di ricerca combinatoria ripetono ripetutamente i termini piccoli nella sequenza , quindi sto cercando un approccio privo di branch o un altro approccio che soffra meno di previsioni errate.

Per molti usi, l'ordine delle sequenze non è così importante, quindi la generazione dei valori in ordine diverso da quello sopra è ammissibile se porta a una soluzione migliore. Ad esempio, j potrebbe eseguire il conto alla rovescia anziché aumentare: (0, 0), (1, 1), (1, 0), (2, 2), (2, 1), ....


Mi interessa sapere che cosa il nome giusto per questa sequenza è (forse così faccio un titolo migliore per questa domanda) anche. Ho appena creato una "sequenza triangolare".

Qui, la versione i >= j rappresenta sotto-multinsiemi (ripetizione consentita), mentre la variante i > j rappresenta sottoinsiemi normali (senza ripetizione).

Qui, la versione i >= j include diagonale principale, mentre la variante i > j esclude.

+0

Perché non si srotolano i casi piccoli-n? E se stai facendo più di qualche istruzione del codice dell'applicazione con ogni coppia, non sarà dominante? –

+0

Sì, sul "altro lavoro potrebbe dominare" punto - ma in almeno uno scenario chiave, la quantità di lavoro è solo poche istruzioni in modo che il sovraccarico del triangolo è una grande parte. – BeeOnRope

+0

Il problema con lo srotolamento è che il codice effettivo non è in un ciclo (ad esempio utilizzando l'iterazione esterna) ma utilizzato invece nel rientro di un iteratore. Inoltre, il limite non è fisso, quindi non so al momento della compilazione se sono nel caso small n o big n, e anche i diversi casi piccoli n necessitano di diversi srotoli. – BeeOnRope

risposta

5

Qui ci sono due approcci senza filiali che non utilizzano eventuali calcoli costosi. Primo usa comparatore e logica AND:

const bool eq = i == j; 
i += eq; 
j = (j + 1) & (eq - 1); 

seconda utilizza comparatore e moltiplicazione:

"moltiplicazione" variante
const bool eq = i == j; 
i += eq; 
j = (j + 1) * (1 - eq); 

In teoria dovrebbe essere più lento "logico", ma misurazioni mostrano poca differenza .

Entrambi gli approcci risulterebbero in un codice senza ramo solo per processori che consentono confronti senza branchie (ad esempio x86). Anche questi approcci presuppongono di essere implementati utilizzando un linguaggio in cui i risultati delle espressioni condizionali potrebbero essere facilmente convertiti in numeri interi (ad esempio C/C++, dove i confronti "falsi" vengono convertiti in numeri interi zero e quelli "veri" in interi uguali a " 1").

L'unico problema con questi approcci è la prestazione. Potrebbero in teoria sovraperformare il codice branchy, ma solo quando i misundict sono molto frequenti. Un semplice test in cui non c'è altro lavoro oltre alla generazione di "sequenza triangolare" (vedi su ideone) mostra una miserabile velocità di errore e quindi entrambi i metodi senza ramo circa 3 volte più lenti di uno ramificato. La spiegazione è semplice: non ci dovrebbero essere molti imprevedibili per sequenze più lunghe; per quanto riguarda quelli più corti, i processori moderni hanno ottimi predittori di branca che quasi mai falliscono in caso di pattern a diramazioni corte; quindi non abbiamo molti imprevedibili, il codice ramificato esegue quasi sempre solo 2 istruzioni (confronto, incremento), mentre il codice senza ramo esegue sia "rami" attivi che incitativi più alcune istruzioni specifiche per l'approccio senza rami.


Nel caso in cui si desidera repeatedly iterate over the small terms in the sequence, probabilmente altro approccio sarebbe preferibile. Si calcola la sequenza solo una volta, quindi si legge ripetutamente dalla memoria.

+0

Risposta eccellente, lo digerirò più presto. Hai ragione riguardo a previsioni errate - ma avevo trascurato la parte in cui le sequenze più brevi venivano effettivamente memorizzate dai predittori. Le nuove CPU Intel (da qualche parte intorno a Haswell) hanno un modo migliore di prevedere che può memorizzare sequenze di rami molto più lunghe rispetto alle generazioni precedenti. – BeeOnRope

+0

Le mie lingue di destinazione sono in realtà Java e C++. Il primo non ha la conversione implicita di booleano a '0/1' che il tuo codice usa, ma se scrivi la conversione con l'operatore ternario il compilatore è spesso abbastanza intelligente da creare una versione libera da branch. – BeeOnRope

0

Questo sembra così semplice sono sicuro di aver perso molto ...ma non è vero solo in cerca di un ciclo annidato come questo (pseudocodice)

for(i=0, i<4, i++) 
    for(j=0, j<i+1, j++) 
     print(i,j) 
+0

Questa è solo una riscrittura del loop che ho dato e subisce le stesse previsioni errate. In particolare, la condizione di uscita per il ciclo interno (il controllo 'j BeeOnRope

+0

Ho modificato la domanda per rimuovere il ciclo, per rendere chiaro il codice in questione genera solo la coppia _next_ '(i, j) data quella corrente. – BeeOnRope

0

È possibile derivare j da I:

...set val... 
old_j = j; 
j = (j + 1) % (i + 1); 
if (i == old_j) { 
    i++; 
} 
...loop if more... 

E inoltre derivare i INCREMENTO da j e corrente i:

...set val... 
old_j = j; 
j = (j + 1) % (i + 1); 
i = i + (i/old_j); 
...loop if more... 

(non possibile verificare in questo momento ... si prega di rivedere)

+0

La prima idea è essenzialmente la stessa di quella che ho proposto sopra, ma con l'incremento condizionale di 'j' sostituito dall'operazione mod. Tuttavia, il condizionale rimane per il caso 'i ++', quindi ci sono tanti rami come prima e altrettanti errori di previsione. Fondamentalmente la rimozione della condizione 'else' di una condizione' if-else' mal formulata non è di grande aiuto in quanto il numero oi rami e il comportamento delle diramazioni in realtà non cambiano. – BeeOnRope

+0

La seconda idea è infatti completamente priva di brack, ma sfortunatamente gli operatori '/' e '%' sono in generale lenti come un errore, quindi questa soluzione rischia di essere più lenta, tranne che su hardware molto insolito. Modificherò la domanda per chiarire che la prestazione è la preoccupazione principale qui. – BeeOnRope

+1

@BeeOnRope Era intesa una singola idea spiegata in due passaggi. Non ero sicuro della tua piattaforma. Alcuni standard di lingua GPU non hanno una ramificazione inversa a causa della maggiore complessità dell'hardware. Ok, sono le prestazioni che vuoi ... – Andreas

1

In Python possiamo esprimere questo come:

i, j = i + (i == j), (j + 1) * (i != j) 

, ma si scopre, a circa un milione di iterazioni o meno, sulla mia macchina, il seguente, più lungo senza fiato, il codice di valutazione pigra è di circa il 20% veloce:

from itertools import count, repeat 

def gen_i(): 
    """ A003056 """ 
    for x in count(0): # infinitely counts up 
     yield from repeat(x, x + 1) # replication 

def gen_j(): 
    """ A002262 """ 
    for x in count(0): # infinitely counts up 
     yield from range(x + 1) # count up to (including) x 

sequence = zip(gen_i(), gen_j()) 

for _ in range(1000000): 
    i, j = next(sequence) 

Nel codice precedente, gen_i(), gen_j(), count(), repeat(), e zip() sono tutti i generatori (e range() è un iteratore) così sequence continua t o chiamare il codice su richiesta quando sono richieste nuove coppie (i, j). Presumo che l'implementazione di range() e repeat() termini con un errore di lettura.

Semplice non è necessariamente anche veloce (cioè considerare tutte le aggiunte non necessarie di zero e le moltiplicazioni di una in forma compatta.)

Quindi, cosa è più importante, generare rapidamente la sequenza o evitare errori di previsione?