Німрод (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
(Nimrod можна завантажити тут .)
Це запускається протягом відведеного часу для n = 20 (і для n = 18, коли використовується лише одна нитка, в останньому випадку йде приблизно 2 хвилини).
Алгоритм використовує рекурсивний пошук, обрізаючи дерево пошуку, коли виникає ненульовий внутрішній продукт. Ми також скорочуємо наполовину пошуковий простір, спостерігаючи, що для будь-якої пари векторів (F, -F)
нам потрібно враховувати лише один, оскільки інший виробляє такі самі набори внутрішніх продуктів (відкидаючи S
також).
Реалізація використовує засоби метапрограмування Nimrod для розгортання / вставки перших кількох рівнів рекурсивного пошуку. Це економить небагато часу при використанні gcc 4.8 та 4.9 як доповнення Nimrod та неабияку суму за кланг.
Простір пошуку можна додатково зменшити, зауваживши, що нам потрібно враховувати лише значення 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-бітній архітектурі).
Загальна швидкість - між коефіцієнтом x3 та x4. 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, які є оберненими одна від одної, також є рівнозначними. Хоча це трохи додало складності обчислення класів еквівалентності, це також дозволило мені зберегти вищезгадану оптимізацію низького рівня, що призвело до прискорення приблизно x3.
Ще одне оновлення для підтримки 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)