Trovare il più grande dei cinque piccoli numeri interi il più rapidamente possibile


9

Uso una variazione di un filtro mediano a 5 incroci sui dati di immagine su un piccolo sistema incorporato, ad es

    x
  x x x
    x

L'algoritmo è davvero semplice: leggi 5 valori interi senza segno, ottieni i 2 più alti, fai dei calcoli su questi e riscrivi il risultato intero senza segno.

La cosa bella è che i 5 valori di input interi sono tutti nell'intervallo 0-20. Anche il valore intero calcolato è compreso nell'intervallo 0-20!

Attraverso la profilazione, ho capito che ottenere i due numeri più grandi è il collo di bottiglia, quindi voglio accelerare questa parte. Qual è il modo più veloce per eseguire questa selezione?

L'attuale algoritmo utilizza una maschera a 32 bit con 1 nella posizione indicata dai 5 numeri e una funzione CLZ supportata da HW.
Dovrei dire che la CPU è proprietaria, non disponibile al di fuori della mia azienda. Il mio compilatore è GCC ma creato su misura per questa CPU.

Ho provato a capire se posso usare una tabella di ricerca ma non sono riuscito a generare una chiave che posso usare.

Ho combinazioni per l'input ma l'ordine non è importante, cioè è lo stesso di .215[5,0,0,0,5][5,5,0,0,0]

Succede che la funzione hash di seguito produce un hash perfetto senza collisioni!

def hash(x):
    h = 0
    for i in x:
        h = 33*h+i
    return h

Ma l'hash è enorme e semplicemente non c'è abbastanza memoria per usarlo.

Esiste un algoritmo migliore che posso usare? È possibile risolvere il mio problema usando una tabella di ricerca e generando una chiave?


1
Quale algoritmo usi attualmente? Sono sufficienti sette confronti tra numeri interi, è troppo lento? Il tuo hashgià esegue più operazioni. Le chiamate successive al metodo sono correlate, ad esempio la centrale si xsposta attraverso la matrice riga per riga?
Raffaello

Il filtro è contorto nell'immagine riga per riga. Vale a dire ottenere 5 valori e fare i calcoli quindi spostare tutto di un passo verso destra e ripetere. L'hash era solo un esempio. Ho confrontato diverse soluzioni con finestre scorrevoli per ridurre al minimo la lettura dei dati, ma tutto si riduce alla ricerca dei 2 valori più alti.
Fredrik Pihl,

3
Molto probabilmente il tuo algoritmo, se implementato correttamente, sarebbe limitato dall'accesso alla memoria e non dal calcolo. L'uso di una tabella hash aumenterebbe solo la quantità di accessi alla memoria e rallenterebbe le cose. Pubblica il tuo codice attuale in modo che possiamo vedere come può essere migliorato - credo che sia possibile solo la micro-ottimizzazione. Il massimo che mi viene in mente è: forse possiamo trarre vantaggio dal fatto che 2 valori sono in comune tra le finestre vicine?
jkff,

@jkff A seconda della matrice, delle dimensioni della cache e della funzione di mappatura (cache), ogni valore potrebbe dover essere caricato una sola volta; la maggior parte delle operazioni dovrebbe quindi essere eseguita su registri o cache L1. Il pipelining è un altro problema, però.
Raffaello

1
A proposito, lo fai già in parallelo? Ciò sembra particolarmente adatto per la parallelizzazione vettoriale o SIMD (ad esempio su una GPU). Quella rotta aiuterebbe molto di più che risparmiare qualche percento per cella.
Raffaello

Risposte:


11

Nella mia altra risposta suggerisco che i salti condizionati potrebbero essere il principale ostacolo all'efficienza. Di conseguenza, vengono in mente le reti di smistamento : sono dati agnostici, ovvero la stessa sequenza di confronti viene eseguita indipendentemente dall'input, con solo gli swap condizionati.

U^2(5)=6

La rete che fornisce nelle soluzioni (riscritta in array a base zero) è

[0:4][1:4][0:3][1:3][0:2][1:2]

che implementa - dopo aver regolato la direzione dei confronti - in pseudocodice come

def selMax2(a : int[])
  a.swap(0,4) if a[0] < a[4]
  a.swap(1,4) if a[1] < a[4]
  a.swap(0,3) if a[0] < a[3]
  a.swap(1,3) if a[1] < a[3]
  a.swap(0,2) if a[0] < a[2]
  a.swap(1,2) if a[1] < a[2]
  return (a[0], a[1])
end

Ora, le implementazioni ingenue hanno ancora salti condizionati (attraverso il codice di scambio). Tuttavia, a seconda della macchina, puoi aggirarli con istruzioni condizionali. x86 sembra essere il solito sé mudpit; ARM sembra più promettente poiché apparentemente la maggior parte delle operazioni sono condizionate da sole. Se capisco correttamente le istruzioni , il primo scambio si traduce in questo, supponendo che i nostri valori di array siano stati caricati nei registri R0attraverso R4:

CMP     R0,R4
MOVLT   R5 = R0
MOVLT   R0 = R4
MOVLT   R4 = R6

Sì, sì, ovviamente puoi usare lo scambio XOR con EOR .

Spero solo che il tuo processore abbia questo o qualcosa di simile. Certo, se costruisci la cosa per questo scopo, forse puoi avere la rete cablata lì?

Questo è probabilmente (dimostrabilmente?) Il meglio che puoi fare nel regno classico, cioè senza usare il dominio limitato ed eseguire magie intra-parole malvagie.


  1. Ordinamento e ricerca di Donald E. Knuth; L'arte della programmazione per computer Vol. 3 (2a edizione, 1998)
  2. W^2(5)=7

Lo sto accettando. Ho ricevuto molte nuove idee che ho bisogno di confrontare prima di andare avanti. Fare riferimento a Knuth funziona sempre per me :-) Grazie per il tuo impegno e tempo!
Fredrik Pihl,

@FredrikPihl Fantastico, facci sapere come va alla fine!
Raffaello

Lo farò! Leggendo il capitolo 5.3.3 in questo momento. Adoro l'inizio di esso con riferimenti a Lewis Carroll e al torneo di tennis :-)
Fredrik Pihl,

2
A seconda del set di istruzioni, può essere utile usare 2 * max (a, b) = a + b + abs (ab) insieme alla rete di selezione; potrebbe essere meno costoso dei salti condizionati imprevedibili (anche senza una mossa intrinseca o condizionale per abs: gcc, almeno per x86, genera una sequenza jumpless che non sembra dipendere da x86). Avere una sequenza jumpless è utile anche in combinazione con SIMD o GPU.
Programmatore

1
Si noti che le reti di selezione (come le reti di ordinamento) sono suscettibili di operazioni parallele; in particolare nella rete di selezione specificata, i confronti 1: 4 e 0: 3 possono essere eseguiti in parallelo (se il processore, il compilatore, ecc. lo supportano in modo efficiente), e i confronti 1: 3 e 0: 2 possono anche essere eseguiti in parallelo.
Bruce Lilly,

4

Solo per essere sul tavolo, ecco un algoritmo diretto:

// Sort x1, x2
if x1 < x2
  M1 = x2
  m1 = x1
else
  M1 = x1
  m1 = x2
end

// Sort x3, x4
if x3 < x4
  M2 = x4
  m2 = x3
else
  M2 = x3
  m2 = x4
end

// Pick largest two
if M1 > M2
  M3 = M1
  if m1 > M2
    m3 = m1
  else
    m3 = M2
  end
else
  M3 = M2
  if m2 > M1
    m3 = m2
  else
    m3 = M1
  end
end

// Insert x4
if x4 > M3
  m3 = M3
  M3 = x4
else if x4 > m3
  m3 = x4
end

Con un'attuazione intelligente di if ... else, si può eliminare alcuni salti incondizionati che una traduzione diretta avrebbe.

Questo è brutto ma richiede solo

  • cinque o sei confronti (ovvero salti condizionati),
  • da nove a dieci assegnazioni (con 11 variabili, tutte nei registri) e
  • nessun ulteriore accesso alla memoria.

W2(5)

Tuttavia, non ci si può aspettare che questo sia veloce su macchine con tubazioni; data la loro alta percentuale di salti condizionati, la maggior parte del tempo sarebbe probabilmente trascorsa in stallo.

Si noti che una variante più semplice - ordina x1e x2, quindi, inserisce successivamente gli altri valori - accetta da quattro a sette confronti e solo da cinque a sei assegnazioni. Dato che mi aspetto che i salti abbiano un costo maggiore qui, mi sono bloccato con questo.


  1. Ordinamento e ricerca di Donald E. Knuth; L'arte della programmazione per computer Vol. 3 (2a edizione, 1998)

Mi chiedo cosa possa fare un compilatore ottimizzante con questi.
Raffaello

Lo implementerò e lo confronterò con l'attuale soluzione basata su CLZ. Grazie per il tuo tempo!
Fredrik Pihl,

1
@FredrikPihl Qual è stato il risultato dei tuoi benchmark?
Raffaello

1
L'approccio basato su SWAP batte CLZ! Adesso sul cellulare. Puoi pubblicare più dati un'altra volta, sul cellulare ora
Fredrik Pihl,

@FredrikPihl Cool! Sono felice che il buon vecchio approccio teorico possa (ancora) essere di utilità pratica. :)
Raffaello

4

Questa potrebbe essere un'ottima applicazione e un test case per il progetto Souper . Souper è un superottimizzatore - uno strumento che accetta una breve sequenza di codice come input e cerca di ottimizzarlo il più possibile (cerca di trovare una sequenza equivalente di codice che sarà più veloce).

Souper è open source. Potresti provare a eseguire Souper sul tuo snippet di codice per vedere se può fare di meglio.

Vedi anche il contest di John Regehr sulla scrittura di codice veloce per ordinare 16 valori a 4 bit ; è possibile che alcune delle tecniche potrebbero essere utili.


Sarei interessato a cosa può fare questo sui programmi che l'OP sta provando.
Raffaello

3

213

T[T[T[441*a+21*b+c]*21+d]*21+e]

214

212

212

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.