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)
Ajuntar com
nimrod cc --threads:on -d:release count.nim
(O Nimrod pode ser baixado aqui .)
Isso é executado no tempo alocado para n = 20 (e para n = 18 ao usar apenas um único encadeamento, levando cerca de 2 minutos no último caso).
O algoritmo usa uma pesquisa recursiva, removendo a árvore de pesquisa sempre que um produto interno diferente de zero é encontrado. Também cortamos o espaço de pesquisa pela metade, observando que, para qualquer par de vetores (F, -F)
, precisamos considerar apenas um porque o outro produz exatamente os mesmos conjuntos de produtos internos (negando S
também).
A implementação usa os recursos de metaprogramação do Nimrod para desenrolar / alinhar os primeiros níveis da pesquisa recursiva. Isso economiza um pouco de tempo ao usar o gcc 4.8 e 4.9 como o back-end do Nimrod e uma quantidade razoável de clang.
O espaço de pesquisa poderia ser ainda mais podado, observando que só precisamos considerar valores de S que diferem em um número par das primeiras posições N da escolha de F. de N, dado que o corpo do loop é completamente ignorado nesses casos.
Tabular onde o produto interno é zero parece ser mais rápido do que usar qualquer funcionalidade de contagem de bits no loop. Aparentemente, acessar a tabela tem uma boa localidade.
Parece que o problema deve ser passível de programação dinâmica, considerando como a pesquisa recursiva funciona, mas não há uma maneira aparente de fazer isso com uma quantidade razoável de memória.
Exemplo de saídas:
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 fins de comparação do algoritmo com outras implementações, N = 16 leva cerca de 7,9 segundos na minha máquina ao usar um único encadeamento e 2,3 segundos ao usar quatro núcleos.
N = 22 leva cerca de 15 minutos em uma máquina de 64 núcleos com o gcc 4.4.6, como o back-end do Nimrod e transborda números inteiros de 64 bits leadingZeros[0]
(possivelmente os não assinados, que ainda não viram).
Atualização: Encontrei espaço para mais algumas melhorias. Primeiro, para um determinado valor de F
, podemos enumerar as 16 primeiras entradas dos S
vetores correspondentes com precisão, porque elas devem diferir exatamente em N/2
locais. Portanto, pré-calculamos uma lista de vetores de bits de tamanho N
com N/2
bits definidos e os usamos para derivar a parte inicial de S
from F
.
Segundo, podemos melhorar a pesquisa recursiva, observando que sempre sabemos o valor de F[N]
(como o MSB é zero na representação de bits). Isso nos permite prever com precisão em qual ramo recorreremos do produto interno. Embora isso nos permita transformar toda a pesquisa em um loop recursivo, isso acaba atrapalhando bastante a previsão do ramo, por isso mantemos os níveis mais altos em sua forma original. Ainda economizamos algum tempo, principalmente reduzindo a quantidade de ramificações que estamos realizando.
Para alguma limpeza, o código agora está usando números inteiros não assinados e os corrige em 64 bits (caso alguém queira executar isso em uma arquitetura de 32 bits).
A aceleração geral está entre um fator de x3 e x4. N = 22 ainda precisa de mais de oito núcleos para rodar em menos de 10 minutos, mas em uma máquina de 64 núcleos agora é de cerca de quatro minutos (com numThreads
aumento de acordo). Não acho que haja muito mais espaço para melhorias sem um 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]
Atualizado novamente, fazendo uso de outras reduções possíveis no espaço de pesquisa. É executado em cerca de 9:49 minutos para N = 22 na minha máquina quadcore.
Atualização final (eu acho). Melhores classes de equivalência para opções de F, reduzindo o tempo de execução de N = 22 para 3:19 minutos e 57 segundos (editar: eu acidentalmente executei isso com apenas um thread) na minha máquina.
Essa alteração utiliza o fato de que um par de vetores produz os mesmos zeros à esquerda se um puder ser transformado no outro girando-o. Infelizmente, uma otimização de baixo nível bastante crítica exige que o bit superior de F na representação de bits seja sempre o mesmo e, ao usar essa equivalência, reduziu bastante o espaço de pesquisa e reduziu o tempo de execução em cerca de um quarto, usando um espaço de estado diferente redução em F, a sobrecarga de eliminar mais a otimização de baixo nível do que compensá-la. No entanto, verifica-se que esse problema pode ser eliminado considerando-se também o fato de que F que são inversos um do outro também são equivalentes. Embora isso tenha aumentado um pouco a complexidade do cálculo das classes de equivalência, ele também me permitiu manter a otimização de baixo nível mencionada, levando a uma aceleração de cerca de x3.
Mais uma atualização para oferecer suporte a números inteiros de 128 bits para os dados acumulados. Para compilar com 128 bit inteiros, você vai precisar longint.nim
de aqui e para compilar com -d:use128bit
. N = 24 ainda leva mais de 10 minutos, mas incluí o resultado abaixo para os interessados.
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)