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)
Kompilieren mit
nimrod cc --threads:on -d:release count.nim
(Nimrod kann hier heruntergeladen werden .)
Dies läuft in der zugewiesenen Zeit für n = 20 (und für n = 18, wenn nur ein einziger Thread verwendet wird, was im letzteren Fall ungefähr 2 Minuten dauert).
Der Algorithmus verwendet eine rekursive Suche, die den Suchbaum beschneidet, wenn ein inneres Produkt angetroffen wird, das nicht Null ist. Wir halbieren auch den Suchraum, indem (F, -F)
wir beobachten, dass wir für jedes Paar von Vektoren nur einen berücksichtigen müssen, weil der andere genau die gleichen Mengen innerer Produkte erzeugt (indem wir S
auch negieren ).
Die Implementierung verwendet die Metaprogrammierungsfunktionen von Nimrod, um die ersten Ebenen der rekursiven Suche zu entrollen / inline zu schalten. Dies spart ein wenig Zeit, wenn Sie gcc 4.8 und 4.9 als Backend von Nimrod verwenden, und eine angemessene Menge für das Klingen.
Der Suchraum könnte weiter eingeschränkt werden, indem beobachtet wird, dass wir nur Werte von S berücksichtigen müssen, die sich in einer geraden Anzahl der ersten N Positionen von unserer Wahl von F unterscheiden. Die Komplexität oder der Speicherbedarf davon skalieren jedoch nicht für große Werte von N, vorausgesetzt, der Schleifenkörper wird in diesen Fällen vollständig übersprungen.
Die Tabellierung, bei der das innere Produkt Null ist, scheint schneller zu sein, als die Verwendung einer Bitzählfunktion in der Schleife. Offensichtlich hat der Zugang zum Tisch eine ziemlich gute Lokalität.
Angesichts der Funktionsweise der rekursiven Suche scheint das Problem für die dynamische Programmierung zugänglich zu sein, aber es gibt keinen offensichtlichen Weg, dies mit einer angemessenen Speicherkapazität zu tun.
Beispielausgaben:
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]
Um den Algorithmus mit anderen Implementierungen zu vergleichen, dauert N = 16 auf meinem Computer bei Verwendung eines einzelnen Threads etwa 7,9 Sekunden und bei Verwendung von vier Kernen 2,3 Sekunden.
N = 22 dauert ungefähr 15 Minuten auf einem 64-Core-Rechner mit gcc 4.4.6 als Nimrods Backend und überläuft 64-Bit-Ganzzahlen leadingZeros[0]
(möglicherweise nicht vorzeichenlose, habe es nicht angeschaut).
Update: Ich habe Raum für ein paar Verbesserungen gefunden. Erstens F
können wir für einen gegebenen Wert von die ersten 16 Einträge der entsprechenden S
Vektoren genau aufzählen , da sie sich genau an den N/2
Stellen unterscheiden müssen . Wir berechnen also eine Liste von Bitvektoren der Größe N
, für die N/2
Bits gesetzt sind, und leiten daraus den Anfangsteil von S
ab F
.
Zweitens können wir die rekursive Suche verbessern, indem wir beobachten, dass wir immer den Wert von kennen F[N]
(da das MSB in der Bitdarstellung Null ist). Auf diese Weise können wir genau vorhersagen, in welchen Zweig wir vom inneren Produkt zurückkehren. Während dies uns tatsächlich erlauben würde, die gesamte Suche in eine rekursive Schleife umzuwandeln, führt dies tatsächlich dazu, dass die Verzweigungsvorhersage ziemlich durcheinander gerät, sodass wir die obersten Ebenen in ihrer ursprünglichen Form beibehalten. Wir sparen immer noch Zeit, vor allem durch die Reduzierung der Verzweigungen.
Für einige Aufräumarbeiten verwendet der Code jetzt vorzeichenlose Ganzzahlen und korrigiert diese auf 64-Bit (nur für den Fall, dass jemand dies auf einer 32-Bit-Architektur ausführen möchte).
Die Gesamtbeschleunigung liegt zwischen einem Faktor von x3 und x4. N = 22 benötigt immer noch mehr als acht Kerne, um in weniger als 10 Minuten ausgeführt zu werden, aber auf einem 64-Kern-Computer sind es jetzt nur noch etwa vier Minuten (mit entsprechend numThreads
erhöhten Werten). Ich glaube nicht, dass es ohne einen anderen Algorithmus viel mehr Raum für Verbesserungen gibt.
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]
Erneut aktualisiert, um weitere mögliche Reduzierungen des Suchraums zu nutzen. Läuft in ca. 9:49 Minuten für N = 22 auf meinem Quadcore-Rechner.
Endgültiges Update (glaube ich). Bessere Äquivalenzklassen für die Auswahl von F, Verkürzung der Laufzeit für N = 22 auf 3:19 Minuten 57 Sekunden (Bearbeiten: Ich hatte das versehentlich mit nur einem Thread ausgeführt) auf meinem Computer.
Diese Änderung nutzt die Tatsache, dass ein Vektorpaar die gleichen führenden Nullen erzeugt, wenn eine durch Drehen in die andere transformiert werden kann. Leider erfordert eine ziemlich kritische Low-Level-Optimierung, dass das oberste Bit von F in der Bitdarstellung immer das gleiche ist, und während diese Äquivalenz verwendet wird, wird der Suchraum ziemlich stark gekürzt und die Laufzeit um etwa ein Viertel gegenüber einem anderen Zustandsraum verringert Reduzierung von F, der Overhead durch das Eliminieren der Low-Level-Optimierung mehr als kompensiert. Es stellt sich jedoch heraus, dass dieses Problem behoben werden kann, indem auch die Tatsache berücksichtigt wird, dass F, die Inverse voneinander sind, ebenfalls äquivalent sind. Dies trug zwar etwas zur Komplexität der Berechnung der Äquivalenzklassen bei, erlaubte mir jedoch auch, die oben erwähnte Optimierung auf niedriger Ebene beizubehalten, was zu einer Beschleunigung von etwa x3 führte.
Ein weiteres Update zur Unterstützung von 128-Bit-Ganzzahlen für die akkumulierten Daten. Um mit 128-Bit-Ganzzahlen zu kompilieren, müssen Sie longint.nim
von hier aus und mit kompilieren -d:use128bit
. N = 24 dauert immer noch mehr als 10 Minuten, aber ich habe das Ergebnis für die Interessenten unten angegeben.
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)