Нимрод (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)
Компилировать с
nimrod cc --threads:on -d:release count.nim
(Нимрод можно скачать здесь .)
Это выполняется за отведенное время для n = 20 (и для n = 18, когда используется только один поток, в последнем случае это занимает около 2 минут).
Алгоритм использует рекурсивный поиск, сокращая дерево поиска всякий раз, когда встречается ненулевой внутренний продукт. Мы также сократили пространство поиска пополам, наблюдая, что для любой пары векторов(F, -F)
нам нужно рассмотреть только один, потому что другой производит те же самые наборы внутренних произведений (отрицаяS
).
Реализация использует средства метапрограммирования Nimrod, чтобы развернуть / встроить первые несколько уровней рекурсивного поиска. Это экономит немного времени при использовании gcc 4.8 и 4.9 в качестве бэкэнда Nimrod и достаточное количество для clang.
Пространство поиска может быть дополнительно сокращено, если заметить, что нам нужно учитывать только значения S, которые отличаются четным числом первых N позиций от нашего выбора F. Однако сложность или потребности в памяти не масштабируются для больших значений N, учитывая, что тело цикла полностью пропускается в этих случаях.
Табулирование, где внутренний продукт равен нулю, оказывается быстрее, чем использование любых функций подсчета битов в цикле. Видимо доступ к таблице имеет довольно хорошую местность.
Похоже, что проблема должна быть поддающейся динамическому программированию, учитывая, как работает рекурсивный поиск, но нет очевидного способа сделать это с разумным объемом памяти.
Пример выходов:
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]
В целях сравнения алгоритма с другими реализациями, N = 16 занимает около 7,9 секунды на моем компьютере при использовании одного потока и 2,3 секунды при использовании четырех ядер.
N = 22 занимает около 15 минут на 64-ядерном компьютере с gcc 4.4.6 в качестве бэкэнда Nimrod и переполняет 64-битные целые числа leadingZeros[0]
(возможно, не беззнаковые, не смотрели на это).
Обновление: я нашел место для еще пары улучшений. Во-первых, для заданного значения F
мы можем S
точно перечислить первые 16 записей соответствующих векторов, потому что они должны точно отличаться в разных N/2
местах. Таким образом, мы предварительно вычисляем список битовых векторов размера N
, в которых N/2
установлены биты, и используем их для получения начальной части S
из F
.
Во-вторых, мы можем улучшить рекурсивный поиск, наблюдая, что мы всегда знаем значение F[N]
(поскольку MSB равен нулю в битовом представлении). Это позволяет нам точно предсказать, в какую ветвь мы перейдем из внутреннего продукта. Хотя это на самом деле позволило бы нам превратить весь поиск в рекурсивный цикл, на самом деле это совсем немного мешает предсказанию ветвлений, поэтому мы сохраняем верхние уровни в первоначальном виде. Мы все еще экономим некоторое время, прежде всего, за счет уменьшения количества ветвлений, которые мы делаем.
Для некоторой очистки код теперь использует целые числа без знака и фиксирует их на 64-битном (на тот случай, если кто-то захочет запустить это на 32-битной архитектуре).
Общее ускорение составляет от х3 до х4. Для N = 22 по-прежнему требуется более восьми ядер для работы менее чем за 10 минут, но на 64-ядерном компьютере это теперь сокращается примерно до четырех минут (сnumThreads
увеличением). Я не думаю, что есть гораздо больше возможностей для улучшения без другого алгоритма.
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]
Обновлен снова, используя дальнейшие возможные сокращения в пространстве поиска. Работает примерно 9:49 минут для N = 22 на моей четырехъядерной машине.
Окончательное обновление (я думаю). Классы лучшей эквивалентности для выбора F, сокращая время выполнения для N = 22 до 3:19 минут 57 секунд (правка: я случайно запустил это только с одним потоком) на моей машине.
Это изменение использует тот факт, что пара векторов производит одинаковые ведущие нули, если один можно преобразовать в другой, вращая его. К сожалению, довольно критичная низкоуровневая оптимизация требует, чтобы верхний бит F в битовом представлении всегда был одинаковым, и при использовании этой эквивалентности значительно сократил пространство поиска и сократил время выполнения примерно на четверть по сравнению с использованием другого пространства состояний сокращение на F, издержки от устранения низкоуровневой оптимизации более чем компенсируют ее. Однако оказывается, что эту проблему можно устранить, если учесть тот факт, что F, обратные друг другу, также эквивалентны. Хотя это немного усложнило вычисление классов эквивалентности, оно также позволило мне сохранить вышеупомянутую низкоуровневую оптимизацию, что привело к ускорению примерно в 3 раза.
Еще одно обновление для поддержки 128-битных целых чисел для накопленных данных. Для компиляции с 128 - битными целыми числами, вам нужно longint.nim
от сюда и компилировать с -d:use128bit
. N = 24 по-прежнему занимает более 10 минут, но я включил приведенный ниже результат для интересующихся.
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)