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)
Kompilasi dengan
nimrod cc --threads:on -d:release count.nim
(Nimrod dapat diunduh di sini .)
Ini berjalan dalam waktu yang ditentukan untuk n = 20 (dan untuk n = 18 ketika hanya menggunakan utas tunggal, membutuhkan waktu sekitar 2 menit dalam kasus terakhir).
Algoritme menggunakan pencarian rekursif, memangkas pohon pencarian setiap kali produk dalam yang tidak nol ditemukan. Kami juga memotong ruang pencarian menjadi dua dengan mengamati bahwa untuk setiap pasangan vektor, (F, -F)
kita hanya perlu mempertimbangkan satu karena yang lainnya menghasilkan set produk dalam yang sama (dengan meniadakan S
juga).
Implementasinya menggunakan fasilitas metaprogramming Nimrod untuk membuka gulungan / sebaris beberapa level pertama dari pencarian rekursif. Ini menghemat sedikit waktu ketika menggunakan gcc 4.8 dan 4.9 sebagai backend dari Nimrod dan jumlah yang wajar untuk dentang.
Ruang pencarian dapat dipangkas lebih lanjut dengan mengamati bahwa kita hanya perlu mempertimbangkan nilai-nilai S yang berbeda dalam jumlah genap dari posisi N pertama dari pilihan kita F. Namun, kompleksitas atau kebutuhan memori yang tidak skala untuk nilai-nilai besar dari N, mengingat bahwa tubuh loop benar-benar dilewati dalam kasus-kasus itu.
Tabulasi di mana produk dalam adalah nol tampaknya lebih cepat daripada menggunakan fungsi penghitungan bit dalam loop. Rupanya mengakses tabel memiliki lokasi yang cukup bagus.
Tampaknya seolah-olah masalahnya harus sesuai dengan pemrograman dinamis, mengingat cara kerja pencarian rekursif, tetapi tidak ada cara yang jelas untuk melakukan itu dengan jumlah memori yang masuk akal.
Output contoh:
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]
Untuk tujuan membandingkan algoritme dengan implementasi lainnya, N = 16 membutuhkan waktu sekitar 7,9 detik pada mesin saya ketika menggunakan utas tunggal dan 2,3 detik saat menggunakan empat inti.
N = 22 membutuhkan waktu sekitar 15 menit pada mesin 64-inti dengan gcc 4.4.6 sebagai backend Nimrod dan meluap bilangan bulat 64-bit leadingZeros[0]
(mungkin bukan yang tidak ditandatangani, belum melihatnya).
Pembaruan: Saya telah menemukan ruang untuk beberapa perbaikan lagi. Pertama, untuk nilai tertentu F
, kita dapat menghitung 16 entri pertama dari S
vektor terkait secara tepat, karena mereka harus berbeda di N/2
tempat yang tepat . Jadi kita melakukan precompute daftar vektor bit dari ukuran N
yang memiliki N/2
bit diatur dan menggunakannya untuk mendapatkan bagian awal S
dari F
.
Kedua, kita dapat meningkatkan pencarian rekursif dengan mengamati bahwa kita selalu tahu nilai F[N]
(karena MSB adalah nol dalam representasi bit). Ini memungkinkan kami untuk memprediksi dengan tepat cabang mana yang kami rekur masuk dari produk dalam. Meskipun itu benar-benar memungkinkan kita mengubah seluruh pencarian menjadi loop rekursif, itu sebenarnya cukup mengacaukan prediksi cabang, jadi kami menjaga level teratas dalam bentuk aslinya. Kami masih menghemat waktu, terutama dengan mengurangi jumlah cabang yang kami lakukan.
Untuk beberapa pembersihan, kode sekarang menggunakan bilangan bulat yang tidak ditandatangani dan memperbaikinya pada 64-bit (kalau-kalau ada seseorang yang ingin menjalankan ini pada arsitektur 32-bit).
Speedup keseluruhan adalah antara faktor x3 dan x4. N = 22 masih membutuhkan lebih dari delapan core untuk berjalan dalam waktu kurang dari 10 menit, tetapi pada mesin 64-core sekarang turun menjadi sekitar empat menit (dengan numThreads
menambahkan sesuai). Saya tidak berpikir ada banyak ruang untuk perbaikan tanpa algoritma yang berbeda.
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]
Diperbarui lagi, memanfaatkan kemungkinan pengurangan lebih lanjut di ruang pencarian. Berjalan sekitar 9:49 menit untuk N = 22 pada mesin quadcore saya.
Pembaruan akhir (saya pikir). Kelas kesetaraan yang lebih baik untuk pilihan F, memotong runtime untuk N = 22 hingga 3:19 menit 57 detik (sunting: Saya tidak sengaja menjalankannya dengan hanya satu utas) pada mesin saya.
Perubahan ini memanfaatkan fakta bahwa sepasang vektor menghasilkan nol terkemuka yang sama jika satu dapat diubah menjadi yang lain dengan memutarnya. Sayangnya, optimasi tingkat rendah yang cukup kritis mensyaratkan bahwa bit atas F dalam representasi bit selalu sama, dan saat menggunakan kesetaraan ini memangkas ruang pencarian sedikit dan mengurangi runtime sekitar seperempat lebih dari menggunakan ruang keadaan yang berbeda pengurangan pada F, overhead dari menghilangkan optimasi level rendah lebih dari mengimbanginya. Namun, ternyata masalah ini dapat dihilangkan dengan juga mempertimbangkan fakta bahwa F yang merupakan invers satu sama lain juga setara. Sementara ini menambah kompleksitas perhitungan kelas ekivalensi sedikit, itu juga memungkinkan saya untuk mempertahankan optimasi tingkat rendah yang disebutkan di atas, yang mengarah ke peningkatan sekitar x3.
Satu lagi pembaruan untuk mendukung bilangan bulat 128-bit untuk akumulasi data. Untuk mengkompilasi dengan integer 128 bit, Anda harus longint.nim
dari sini dan mengkompilasinya -d:use128bit
. N = 24 masih membutuhkan waktu lebih dari 10 menit, tetapi saya sudah memasukkan hasil di bawah ini untuk mereka yang tertarik.
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)