Nimrod (N = 22)
import math, locks
const
N = 20
M = N + 1
FSize = (1 shl N)
FMax = FSize - 1
SStep = 1 shl (N-1)
numThreads = 16
type
ZeroCounter = array[0..M-1, int]
ComputeThread = TThread[int]
var
leadingZeros: ZeroCounter
lock: TLock
innerProductTable: array[0..FMax, int8]
proc initInnerProductTable =
for i in 0..FMax:
innerProductTable[i] = int8(countBits32(int32(i)) - N div 2)
initInnerProductTable()
proc zeroInnerProduct(i: int): bool =
innerProductTable[i] == 0
proc search2(lz: var ZeroCounter, s, f, i: int) =
if zeroInnerProduct(s xor f) and i < M:
lz[i] += 1 shl (M - i - 1)
search2(lz, (s shr 1) + 0, f, i+1)
search2(lz, (s shr 1) + SStep, f, i+1)
when defined(gcc):
const
unrollDepth = 1
else:
const
unrollDepth = 4
template search(lz: var ZeroCounter, s, f, i: int) =
when i < unrollDepth:
if zeroInnerProduct(s xor f) and i < M:
lz[i] += 1 shl (M - i - 1)
search(lz, (s shr 1) + 0, f, i+1)
search(lz, (s shr 1) + SStep, f, i+1)
else:
search2(lz, s, f, i)
proc worker(base: int) {.thread.} =
var lz: ZeroCounter
for f in countup(base, FMax div 2, numThreads):
for s in 0..FMax:
search(lz, s, f, 0)
acquire(lock)
for i in 0..M-1:
leadingZeros[i] += lz[i]*2
release(lock)
proc main =
var threads: array[numThreads, ComputeThread]
for i in 0 .. numThreads-1:
createThread(threads[i], worker, i)
for i in 0 .. numThreads-1:
joinThread(threads[i])
initLock(lock)
main()
echo(@leadingZeros)
Compila con
nimrod cc --threads:on -d:release count.nim
(Nimrod può essere scaricato qui .)
Questo viene eseguito nel tempo assegnato per n = 20 (e per n = 18 quando si utilizza solo un singolo thread, impiegando circa 2 minuti in quest'ultimo caso).
L'algoritmo utilizza una ricerca ricorsiva, potando l'albero di ricerca ogni volta che si incontra un prodotto interno diverso da zero. Abbiamo anche dimezzato lo spazio di ricerca osservando che per ogni coppia di vettori (F, -F)
dobbiamo solo considerare uno perché l'altro produce gli stessi identici insiemi di prodotti interni (negando S
anche).
L'implementazione utilizza le strutture di metaprogrammazione di Nimrod per srotolare / allineare i primi livelli della ricerca ricorsiva. Ciò consente di risparmiare un po 'di tempo quando si utilizzano gcc 4.8 e 4.9 come backend di Nimrod e una discreta quantità di clang.
Lo spazio di ricerca potrebbe essere ulteriormente potato osservando che dobbiamo solo considerare i valori di S che differiscono in un numero pari delle prime posizioni N dalla nostra scelta di F. Tuttavia, le esigenze di complessità o memoria di ciò non si adattano per valori di grandi dimensioni di N, dato che il corpo del loop viene completamente ignorato in questi casi.
Tabulare in cui il prodotto interno è zero sembra essere più veloce rispetto all'utilizzo di qualsiasi funzionalità di conteggio dei bit nel loop. A quanto pare l'accesso al tavolo ha una località abbastanza buona.
Sembra che il problema dovrebbe essere suscettibile alla programmazione dinamica, considerando come funziona la ricerca ricorsiva, ma non esiste un modo apparente per farlo con una quantità ragionevole di memoria.
Esempi di output:
N = 16:
@[55276229099520, 10855179878400, 2137070108672, 420578918400, 83074121728, 16540581888, 3394347008, 739659776, 183838720, 57447424, 23398912, 10749184, 5223040, 2584896, 1291424, 645200, 322600]
N = 18:
@[3341140958904320, 619683355033600, 115151552380928, 21392898654208, 3982886961152, 744128512000, 141108051968, 27588886528, 5800263680, 1408761856, 438001664, 174358528, 78848000, 38050816, 18762752, 9346816, 4666496, 2333248, 1166624]
N = 20:
@[203141370301382656, 35792910586740736, 6316057966936064, 1114358247587840, 196906665902080, 34848574013440, 6211866460160, 1125329141760, 213330821120, 44175523840, 11014471680, 3520839680, 1431592960, 655872000, 317675520, 156820480, 78077440, 39005440, 19501440, 9750080, 4875040]
Ai fini del confronto dell'algoritmo con altre implementazioni, N = 16 impiega circa 7,9 secondi sulla mia macchina quando si utilizza un singolo thread e 2,3 secondi quando si utilizzano quattro core.
N = 22 impiega circa 15 minuti su una macchina a 64 core con gcc 4.4.6 come backend di Nimrod e trabocca in interi a 64 bit leadingZeros[0]
(probabilmente non quelli senza segno, non l'ho mai visto).
Aggiornamento: ho trovato spazio per un paio di miglioramenti in più. Innanzitutto, per un dato valore di F
, possiamo enumerare con S
precisione le prime 16 voci dei corrispondenti vettori, perché devono differire esattamente in N/2
punti. Così abbiamo Precompute una lista di vettori di bit di dimensioni N
che hanno N/2
bit set e utilizzare questi per ricavare la parte iniziale di S
da F
.
In secondo luogo, possiamo migliorare la ricerca ricorsiva osservando che conosciamo sempre il valore di F[N]
(poiché MSB è zero nella rappresentazione dei bit). Questo ci consente di prevedere con precisione in quale ramo ricerchiamo dal prodotto interno. Sebbene ciò ci consentirebbe effettivamente di trasformare l'intera ricerca in un ciclo ricorsivo, ciò in realtà capita di rovinare un po 'la previsione del ramo, quindi manteniamo i livelli più alti nella sua forma originale. Risparmiamo ancora un po 'di tempo, principalmente riducendo la quantità di ramificazioni che stiamo facendo.
Per un po 'di pulizia, il codice ora utilizza numeri interi senza segno e li corregge a 64 bit (nel caso in cui qualcuno desideri eseguirlo su un'architettura a 32 bit).
Lo speedup complessivo è compreso tra un fattore di x3 e x4. N = 22 richiede ancora più di otto core per funzionare in meno di 10 minuti, ma su una macchina a 64 core è ora sceso a circa quattro minuti (con un numThreads
aumento di conseguenza). Tuttavia, non credo che ci sia molto più margine di miglioramento senza un algoritmo diverso.
N = 22:
@[12410090985684467712, 2087229562810269696, 351473149499408384, 59178309967151104, 9975110458933248, 1682628717576192, 284866824372224, 48558946385920, 8416739196928, 1518499004416, 301448822784, 71620493312, 22100246528, 8676573184, 3897278464, 1860960256, 911646720, 451520512, 224785920, 112198656, 56062720, 28031360, 14015680]
Aggiornato di nuovo, facendo uso di ulteriori riduzioni possibili nello spazio di ricerca. Funziona in circa 9:49 minuti per N = 22 sulla mia macchina quadcore.
Aggiornamento finale (penso). Migliori classi di equivalenza per le scelte di F, riducendo il tempo di esecuzione per N = 22 fino a 3:19 minuti 57 secondi (modifica: l'avevo accidentalmente eseguito con un solo thread) sulla mia macchina.
Questa modifica sfrutta il fatto che una coppia di vettori produce gli stessi zeri iniziali se uno può essere trasformato nell'altro ruotandolo. Sfortunatamente, un'ottimizzazione di basso livello abbastanza critica richiede che il bit superiore di F nella rappresentazione dei bit sia sempre lo stesso, e mentre si utilizza questa equivalenza si è ridotto di molto lo spazio di ricerca e si è ridotto il tempo di esecuzione di circa un quarto rispetto a uno spazio di stato diverso riduzione su F, il sovraccarico dall'eliminazione dell'ottimizzazione di basso livello più che compensato. Tuttavia, si scopre che questo problema può essere eliminato considerando anche il fatto che anche F che sono inverse l'una dall'altra sono equivalenti. Mentre questo ha aggiunto un po 'alla complessità del calcolo delle classi di equivalenza, mi ha anche permesso di conservare la suddetta ottimizzazione di basso livello, portando a una velocità di circa x3.
Un altro aggiornamento per supportare numeri interi a 128 bit per i dati accumulati. Per compilare con numeri interi a 128 bit, è necessario longint.nim
da qui e compilare -d:use128bit
. N = 24 richiede ancora più di 10 minuti, ma ho incluso il risultato di seguito per gli interessati.
N = 24:
@[761152247121980686336, 122682715414070296576, 19793870419291799552, 3193295704340561920, 515628872377565184, 83289931274780672, 13484616786640896, 2191103969198080, 359662314586112, 60521536552960, 10893677035520, 2293940617216, 631498735616, 230983794688, 102068682752, 48748969984, 23993655296, 11932487680, 5955725312, 2975736832, 1487591936, 743737600, 371864192, 185931328, 92965664]
import math, locks, unsigned
when defined(use128bit):
import longint
else:
type int128 = uint64 # Fallback on unsupported architectures
template toInt128(x: expr): expr = uint64(x)
const
N = 22
M = N + 1
FSize = (1 shl N)
FMax = FSize - 1
SStep = 1 shl (N-1)
numThreads = 16
type
ZeroCounter = array[0..M-1, uint64]
ZeroCounterLong = array[0..M-1, int128]
ComputeThread = TThread[int]
Pair = tuple[value, weight: int32]
var
leadingZeros: ZeroCounterLong
lock: TLock
innerProductTable: array[0..FMax, int8]
zeroInnerProductList = newSeq[int32]()
equiv: array[0..FMax, int32]
fTable = newSeq[Pair]()
proc initInnerProductTables =
for i in 0..FMax:
innerProductTable[i] = int8(countBits32(int32(i)) - N div 2)
if innerProductTable[i] == 0:
if (i and 1) == 0:
add(zeroInnerProductList, int32(i))
initInnerProductTables()
proc ror1(x: int): int {.inline.} =
((x shr 1) or (x shl (N-1))) and FMax
proc initEquivClasses =
add(fTable, (0'i32, 1'i32))
for i in 1..FMax:
var r = i
var found = false
block loop:
for j in 0..N-1:
for m in [0, FMax]:
if equiv[r xor m] != 0:
fTable[equiv[r xor m]-1].weight += 1
found = true
break loop
r = ror1(r)
if not found:
equiv[i] = int32(len(fTable)+1)
add(fTable, (int32(i), 1'i32))
initEquivClasses()
when defined(gcc):
const unrollDepth = 4
else:
const unrollDepth = 4
proc search2(lz: var ZeroCounter, s0, f, w: int) =
var s = s0
for i in unrollDepth..M-1:
lz[i] = lz[i] + uint64(w)
s = s shr 1
case innerProductTable[s xor f]
of 0:
# s = s + 0
of -1:
s = s + SStep
else:
return
template search(lz: var ZeroCounter, s, f, w, i: int) =
when i < unrollDepth:
lz[i] = lz[i] + uint64(w)
if i < M-1:
let s2 = s shr 1
case innerProductTable[s2 xor f]
of 0:
search(lz, s2 + 0, f, w, i+1)
of -1:
search(lz, s2 + SStep, f, w, i+1)
else:
discard
else:
search2(lz, s, f, w)
proc worker(base: int) {.thread.} =
var lz: ZeroCounter
for fi in countup(base, len(fTable)-1, numThreads):
let (fp, w) = fTable[fi]
let f = if (fp and (FSize div 2)) == 0: fp else: fp xor FMax
for sp in zeroInnerProductList:
let s = f xor sp
search(lz, s, f, w, 0)
acquire(lock)
for i in 0..M-1:
let t = lz[i].toInt128 shl (M-i).toInt128
leadingZeros[i] = leadingZeros[i] + t
release(lock)
proc main =
var threads: array[numThreads, ComputeThread]
for i in 0 .. numThreads-1:
createThread(threads[i], worker, i)
for i in 0 .. numThreads-1:
joinThread(threads[i])
initLock(lock)
main()
echo(@leadingZeros)