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)
Compilar con
nimrod cc --threads:on -d:release count.nim
(Nimrod se puede descargar aquí ).
Esto se ejecuta en el tiempo asignado para n = 20 (y para n = 18 cuando solo se usa un solo subproceso, lo que demora aproximadamente 2 minutos en el último caso).
El algoritmo utiliza una búsqueda recursiva, podando el árbol de búsqueda cada vez que se encuentra un producto interno distinto de cero. También reducimos el espacio de búsqueda a la mitad al observar que para cualquier par de vectores (F, -F)
solo necesitamos considerar uno porque el otro produce exactamente los mismos conjuntos de productos internos (al negar S
también).
La implementación utiliza las funciones de metaprogramación de Nimrod para desenrollar / en línea los primeros niveles de la búsqueda recursiva. Esto ahorra un poco de tiempo al usar gcc 4.8 y 4.9 como el backend de Nimrod y una buena cantidad para el sonido metálico.
El espacio de búsqueda podría reducirse aún más al observar que solo necesitamos considerar valores de S que difieran en un número par de las primeras N posiciones de nuestra elección de F. Sin embargo, la complejidad o las necesidades de memoria de eso no escalan para valores grandes de N, dado que el cuerpo del bucle se omite por completo en esos casos.
Tabular donde el producto interno es cero parece ser más rápido que usar cualquier funcionalidad de conteo de bits en el ciclo. Aparentemente acceder a la mesa tiene bastante buena localidad.
Parece que el problema debería ser susceptible a la programación dinámica, considerando cómo funciona la búsqueda recursiva, pero no hay una forma aparente de hacerlo con una cantidad razonable de memoria.
Salidas de ejemplo:
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]
Para comparar el algoritmo con otras implementaciones, N = 16 toma aproximadamente 7.9 segundos en mi máquina cuando se usa un solo hilo y 2.3 segundos cuando se usan cuatro núcleos.
N = 22 tarda unos 15 minutos en una máquina de 64 núcleos con gcc 4.4.6 como backend de Nimrod y desborda enteros de 64 bits leadingZeros[0]
(posiblemente no sin firmar, no lo he mirado).
Actualización: he encontrado espacio para un par de mejoras más. Primero, para un valor dado de F
, podemos enumerar las primeras 16 entradas de los S
vectores correspondientes con precisión, porque deben diferir en N/2
lugares exactos . Así precomputamos una lista de vectores de bits de tamaño N
que tienen N/2
determinados bits y utilizar estos para derivar la parte inicial de S
partir F
.
En segundo lugar, podemos mejorar la búsqueda recursiva al observar que siempre conocemos el valor de F[N]
(ya que el MSB es cero en la representación de bits). Esto nos permite predecir con precisión a qué rama recurrimos desde el producto interno. Si bien eso realmente nos permitiría convertir toda la búsqueda en un bucle recursivo, eso realmente arruina bastante la predicción de rama, por lo que mantenemos los niveles superiores en su forma original. Todavía ahorramos algo de tiempo, principalmente al reducir la cantidad de ramificaciones que estamos haciendo.
Para alguna limpieza, el código ahora usa enteros sin signo y los repara a 64 bits (en caso de que alguien quiera ejecutar esto en una arquitectura de 32 bits).
La aceleración general está entre un factor de x3 y x4. N = 22 todavía necesita más de ocho núcleos para ejecutarse en menos de 10 minutos, pero en una máquina de 64 núcleos ahora se reduce a unos cuatro minutos (con un numThreads
aumento correspondiente en consecuencia). Sin embargo, no creo que haya mucho más margen de mejora sin un algoritmo diferente.
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]
Actualizada nuevamente, haciendo uso de otras posibles reducciones en el espacio de búsqueda. Se ejecuta en aproximadamente 9:49 minutos para N = 22 en mi máquina quadcore.
Actualización final (creo). Mejores clases de equivalencia para las opciones de F, reduciendo el tiempo de ejecución de N = 22 a 3:19 minutos y 57 segundos (editar: accidentalmente lo ejecuté con solo un hilo) en mi máquina.
Este cambio hace uso del hecho de que un par de vectores produce los mismos ceros iniciales si uno puede transformarse en el otro al rotarlo. Desafortunadamente, una optimización de bajo nivel bastante crítica requiere que el bit superior de F en la representación de bits sea siempre el mismo, y mientras usa esta equivalencia recorta un poco el espacio de búsqueda y reduce el tiempo de ejecución en aproximadamente un cuarto sobre el uso de un espacio de estado diferente reducción en F, la sobrecarga de eliminar la optimización de bajo nivel más que compensarla. Sin embargo, resulta que este problema puede eliminarse considerando también el hecho de que F que son inversas entre sí también son equivalentes. Si bien esto aumentó un poco la complejidad del cálculo de las clases de equivalencia, también me permitió retener la optimización de bajo nivel mencionada anteriormente, lo que condujo a una aceleración de aproximadamente x3.
Una actualización más para admitir enteros de 128 bits para los datos acumulados. Para compilar con enteros de 128 bits, necesitará longint.nim
desde aquí y compilar con -d:use128bit
. N = 24 todavía lleva más de 10 minutos, pero he incluido el resultado a continuación para los interesados.
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)