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)
Połącz z
nimrod cc --threads:on -d:release count.nim
(Nimrod można pobrać tutaj .)
Działa to w wyznaczonym czasie dla n = 20 (i dla n = 18, gdy używa się tylko jednego wątku, w tym drugim przypadku zajmuje to około 2 minut).
Algorytm wykorzystuje wyszukiwanie rekurencyjne, przycinając drzewo wyszukiwania za każdym razem, gdy napotkany zostanie niezerowy produkt wewnętrzny. Skróciliśmy również przestrzeń poszukiwań o połowę, obserwując to dla dowolnej pary wektorów(F, -F)
musimy wziąć pod uwagę tylko jeden, ponieważ drugi wytwarza dokładnie takie same zestawy produktów wewnętrznych ( S
również przez negację ).
Wdrożenie wykorzystuje funkcje metaprogramowania Nimrod do rozwijania / wstawiania pierwszych kilku poziomów wyszukiwania rekurencyjnego. To oszczędza trochę czasu, gdy używasz gcc 4.8 i 4.9 jako backendu Nimroda i sporej kwoty na clang.
Przestrzeń wyszukiwania można dodatkowo przyciąć, obserwując, że musimy wziąć pod uwagę tylko wartości S, które różnią się parzystą liczbą pierwszych N pozycji od naszego wyboru F. Jednak złożoność lub potrzeby pamięciowe tego nie są skalowane dla dużych wartości z N, biorąc pod uwagę, że w takich przypadkach ciało pętli jest całkowicie pomijane.
Tabulowanie, gdzie iloczyn wewnętrzny jest zerowy, wydaje się być szybsze niż używanie jakiejkolwiek funkcji liczenia bitów w pętli. Najwyraźniej dostęp do stołu ma całkiem niezłą lokalizację.
Wygląda na to, że problem powinien być podatny na programowanie dynamiczne, biorąc pod uwagę, jak działa wyszukiwanie rekurencyjne, ale nie ma widocznego sposobu, aby to zrobić przy rozsądnej ilości pamięci.
Przykładowe wyniki:
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]
Dla celów porównania algorytmu z innymi implementacjami, N = 16 zajmuje około 7,9 sekundy na mojej maszynie przy użyciu jednego wątku i 2,3 sekundy przy użyciu czterech rdzeni.
N = 22 zajmuje około 15 minut na 64-rdzeniowej maszynie z gcc 4.4.6, ponieważ backend Nimroda przepełnia 64-bitowe liczby całkowite leadingZeros[0]
(być może nie podpisane, nie oglądałem).
Aktualizacja: Znalazłem miejsce na kilka ulepszeń. Po pierwsze, dla danej wartości F
możemy dokładnie wyliczyć pierwsze 16 pozycji odpowiednich S
wektorów, ponieważ muszą się one różnić dokładnie w różnych N/2
miejscach. Więc wstępnego wyliczenia listy bitowych wektorów wielkości N
, które mają N/2
bitów zestawu i korzystają z nich czerpać początkową część S
z F
.
Po drugie, możemy ulepszyć wyszukiwanie rekurencyjne, obserwując, że zawsze znamy wartość F[N]
(ponieważ MSB ma zero w reprezentacji bitowej). To pozwala nam dokładnie przewidzieć, w której gałęzi wracamy z produktu wewnętrznego. Chociaż faktycznie pozwoliłoby nam to przekształcić całe wyszukiwanie w pętlę rekurencyjną, tak naprawdę zdarza się, że dość spieszy przewidywanie gałęzi, więc utrzymujemy najwyższe poziomy w oryginalnej formie. Wciąż oszczędzamy trochę czasu, przede wszystkim poprzez zmniejszenie liczby rozgałęzień, które wykonujemy.
W przypadku niektórych porządków kod używa teraz liczb całkowitych bez znaku i naprawia je w wersji 64-bitowej (na wypadek, gdyby ktoś chciał uruchomić to w architekturze 32-bitowej).
Ogólne przyspieszenie wynosi od współczynnika x3 do x4. N = 22 nadal potrzebuje więcej niż ośmiu rdzeni, aby uruchomić się w czasie krótszym niż 10 minut, ale na 64-rdzeniowej maszynie jest to teraz około czterech minut (znumThreads
odpowiednio zwiększone). Nie sądzę jednak, aby było dużo więcej miejsca na ulepszenia bez innego algorytmu.
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]
Zaktualizowano ponownie, wykorzystując dalsze możliwe ograniczenia przestrzeni wyszukiwania. Działa za około 9:49 minut dla N = 22 na mojej maszynie quadcore.
Ostatnia aktualizacja (myślę). Lepsze klasy równoważności dla wyborów F, skrócenie czasu działania dla N = 22 do 3:19 minut 57 sekund (edycja: przypadkowo uruchomiłem to z tylko jednym wątkiem) na mojej maszynie.
Ta zmiana wykorzystuje fakt, że para wektorów wytwarza te same zera wiodące, jeśli jeden można przekształcić w drugi, obracając go. Niestety, dość krytyczna optymalizacja niskiego poziomu wymaga, aby górny bit F w reprezentacji bitów był zawsze taki sam, a podczas korzystania z tej równoważności nieco zmniejszył przestrzeń wyszukiwania i skrócił czas działania o około jedną czwartą w porównaniu z użyciem innej przestrzeni stanów redukcja F, narzut związany z eliminacją optymalizacji niskiego poziomu bardziej niż ją zrównoważył. Okazuje się jednak, że problem ten można wyeliminować również biorąc pod uwagę fakt, że F, które są odwrotne względem siebie, są również równoważne. Wprawdzie nieco to skomplikowało obliczenia klas równoważności, ale pozwoliło mi również zachować wspomnianą optymalizację niskiego poziomu, co doprowadziło do przyspieszenia około x3.
Jeszcze jedna aktualizacja do obsługi 128-bitowych liczb całkowitych dla zgromadzonych danych. Aby skompilować z 128 bitowych liczb całkowitych, trzeba longint.nim
z tutaj i skompilować -d:use128bit
. N = 24 nadal zajmuje więcej niż 10 minut, ale dla zainteresowanych zainteresowałem się poniższym wynikiem.
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)