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)
Compiler avec
nimrod cc --threads:on -d:release count.nim
(Nimrod peut être téléchargé ici .)
Cela s'exécute dans le temps imparti pour n = 20 (et pour n = 18 si vous n'utilisez qu'un seul thread, ce qui prend environ 2 minutes dans ce dernier cas).
L'algorithme utilise une recherche récursive, élaguant l'arbre de recherche chaque fois qu'un produit interne différent de zéro est rencontré. Nous avons également réduit l’espace de recherche de moitié en observant que, pour toute paire de vecteurs, (F, -F)
il suffit de prendre en compte l’un car l’autre produit les mêmes ensembles de produits internes (en inversant S
également).
L'implémentation utilise les installations de métaprogrammation de Nimrod pour dérouler / intégrer les premiers niveaux de la recherche récursive. Cela fait gagner un peu de temps lorsque vous utilisez gcc 4.8 et 4.9 comme back-end de Nimrod et une bonne somme pour clang.
On pourrait encore élaguer l’espace de recherche en observant que nous n'avons besoin que de considérer les valeurs de S qui diffèrent par un nombre pair des N premières positions de notre choix de F. Cependant, la complexité ou les besoins en mémoire de celui-ci ne sont pas proportionnels aux grandes valeurs. de N, étant donné que le corps de la boucle est complètement ignoré dans ces cas.
La tabulation où le produit intérieur est égal à zéro semble être plus rapide que d'utiliser une fonctionnalité de comptage de bits dans la boucle. Apparemment, accéder à la table a une très bonne localité.
Il semble que le problème doive être réglé par une programmation dynamique, tenant compte du fonctionnement de la recherche récursive, mais il n’existe aucun moyen apparent de le faire avec une quantité de mémoire raisonnable.
Exemple de sorties:
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]
Pour comparer l'algorithme avec d'autres implémentations, N = 16 prend environ 7,9 secondes sur ma machine avec un seul thread et 2,3 secondes avec quatre cœurs.
N = 22 prend environ 15 minutes sur une machine à 64 cœurs avec 4.4.6 gcc, car le backend de Nimrod et déborde d'entiers 64 bits leadingZeros[0]
(éventuellement non signés, ils ne l'ont pas examiné).
Mise à jour: j'ai trouvé de la place pour quelques améliorations supplémentaires. Premièrement, pour une valeur donnée de F
, nous pouvons énumérer les 16 premières entrées des S
vecteurs correspondants avec précision, car elles doivent différer exactement à des N/2
endroits. Nous avons donc Précalculer une liste de vecteurs de bits de taille N
qui ont des N/2
bits ensemble et les utiliser pour tirer la partie initiale de S
partir F
.
Deuxièmement, nous pouvons améliorer la recherche récursive en observant que nous connaissons toujours la valeur de F[N]
(car le MSB a la valeur zéro dans la représentation binaire). Cela nous permet de prédire avec précision dans quelle branche nous recurse à partir du produit intérieur. Cela nous permettrait en fait de transformer toute la recherche en une boucle récursive, mais il arrive en fait que la prédiction de branche soit bousillée un peu, nous avons donc gardé les niveaux supérieurs dans leur forme originale. Nous gagnons encore du temps, principalement en réduisant le nombre de branches que nous effectuons.
Pour certains nettoyages, le code utilise maintenant des entiers non signés et les corrige en 64 bits (juste au cas où quelqu'un voudrait l'exécuter sur une architecture 32 bits).
L'accélération globale est comprise entre un facteur x3 et x4. N = 22 a encore besoin de plus de huit cœurs pour fonctionner en moins de 10 minutes, mais sur une machine à 64 cœurs, le temps passe maintenant à environ quatre minutes (avec une numThreads
montée en puissance correspondante). Je ne pense pas qu'il y ait beaucoup plus de place à l'amélioration sans un algorithme différent, cependant.
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]
Mis à jour à nouveau, en utilisant d'autres réductions possibles de l'espace de recherche. Fonctionne dans environ 9:49 minutes pour N = 22 sur ma machine quadcore.
Dernière mise à jour (je pense). De meilleures classes d'équivalence pour les choix de F, réduisant le temps d'exécution de N = 22 à 3:19 minutes 57 secondes (edit: j'avais accidentellement exécuté cela avec un seul thread) sur ma machine.
Ce changement tient compte du fait qu’une paire de vecteurs produit les mêmes zéros d’avant si l’un peut être transformé en un autre en le faisant pivoter. Malheureusement, une optimisation de bas niveau assez critique nécessite que le bit le plus haut de F dans la représentation de bit soit toujours identique, et tout en utilisant cette équivalence, a considérablement réduit l'espace de recherche et réduit le temps d'exécution d'environ un quart par rapport à l'utilisation d'un espace d'état différent. réduction sur F, les frais généraux liés à l'élimination de l'optimisation de bas niveau l'ont plus que compensée. Cependant, il s'avère que ce problème peut être éliminé en considérant également le fait que F inverses les uns des autres sont également équivalents. Bien que cela ait ajouté un peu à la complexité du calcul des classes d'équivalence, cela m'a également permis de conserver l'optimisation de bas niveau susmentionnée, conduisant à une accélération d'environ x3.
Une mise à jour supplémentaire pour prendre en charge les entiers 128 bits pour les données accumulées. Pour compiler avec des entiers 128 bits, vous aurez besoin longint.nim
d' ici et avec -d:use128bit
. N = 24 prend toujours plus de 10 minutes, mais j'ai inclus le résultat ci-dessous pour les personnes intéressées.
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)