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)
İle derleyin
nimrod cc --threads:on -d:release count.nim
(Nimrod indirilebilir burada .)
Bu, n = 20 için ayrılan süre içinde çalışır (ve yalnızca tek bir iplik kullanıldığında, ikinci durumda yaklaşık 2 dakika süren n = 18 için).
Algoritma, sıfır olmayan bir iç ürünle karşılaşıldığında arama ağacını budayan, yinelemeli bir arama kullanır. Ayrıca, herhangi bir vektör çifti için, (F, -F)
sadece bir tane düşünmemiz gerektiğini, diğerinin de aynı iç ürün setlerini ürettiğini göz ardı ederek, arama alanını yarı yarıya azalttık.S
aynı zamanda ) .
Uygulama, özyinelemeli aramanın ilk birkaç seviyesini açmak / sıralamak için Nimrod'un metaprogramlama tesislerini kullanıyor. Bu, Nimrod'un arka ucu olarak gcc 4.8 ve 4.9 kullanırken ve clang için makul miktarda kullanıldığında biraz zaman kazandırır.
Sadece F seçimimizden ilk N konumunda eşit sayıdaki S değerlerini göz önünde bulundurmamız gerektiğini gözlemleyerek araştırma alanı daha da budanabilir. Ancak bunun karmaşıklığı veya hafızasının ihtiyaçları büyük değerler için ölçeklendirilmez N, bu durumlarda döngü gövdesinin tamamen atlandığı göz önüne alındığında.
İç ürünün sıfır olduğu yerin yeniden düzenlenmesi, döngüdeki herhangi bir bit sayma işlevini kullanmaktan daha hızlı görünmektedir. Görünüşe göre masaya erişim oldukça iyi bir konuma sahiptir.
Özyinelemeli aramanın nasıl yürüdüğünü göz önünde bulundurarak, problemin dinamik programlamaya uygun olması gerektiği gibi görünüyor, ancak bunu makul miktarda bellekle yapmanın belirgin bir yolu yok.
Örnek çıktılar:
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]
Algoritmayı diğer uygulamalarla karşılaştırmak amacıyla, N = 16 makinemde tek bir iplik kullanırken yaklaşık 7.9 saniye ve dört çekirdek kullanırken 2.3 saniye sürer.
N = 22, Nimrod'un arka ucu olarak gcc 4.4.6 olan 64 çekirdekli bir makinede yaklaşık 15 dakika sürer ve 64-bit tam sayıların içine taşar leadingZeros[0]
(muhtemelen işaretsiz olanlar da bakmaz).
Güncelleme: Birkaç iyileştirme için yer buldum. İlk olarak, belirli bir değer için F
, karşılık gelen S
vektörlerin ilk 16 girişini tam olarak numaralandırabiliriz , çünkü tam olarak farklı N/2
yerlerde olmaları gerekir . Biz boyutta bit vektörlerin listesi precompute Yani N
sahip N/2
bit seti ve baştarafını türetmek için bunları kullanabilir S
dan F
.
İkincisi, özyinelemeli aramada değerini her zaman bildiğimizi gözlemleyerek iyileştirebiliriz F[N]
(çünkü MSB bit gösteriminde sıfırdır). Bu, iç üründen hangi dalda tutunduğumuzu kesin olarak tahmin etmemizi sağlar. Bu aslında tüm araştırmayı özyinelemeli bir döngüye dönüştürmemize izin verirken, bu aslında şube tahminini biraz parçaladı, bu yüzden en üst seviyeyi orijinal biçiminde tutuyoruz. Öncelikle yaptığımız dallanma miktarını azaltarak hala zaman kazanıyoruz.
Bazı temizleme işlemleri için kod şimdi işaretsiz tamsayılar kullanıyor ve bunları 64 bit'te düzeltiyor (sadece birinin bunu 32 bit mimaride çalıştırmak istemesi durumunda).
Genel hızlanma, x3 ve x4 faktörü arasındadır. N = 22'nin 10 dakikadan daha az bir sürede çalışması için sekizden fazla çekirdeğe ihtiyacı var, ancak 64 çekirdekli bir makinede şu anda yaklaşık dört dakikaya düştü ( numThreads
buna göre çarpıldı). Yine de, farklı bir algoritma olmadan iyileştirme için daha fazla yer olduğunu sanmıyorum.
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]
Yeniden düzenlendi, arama alanındaki daha fazla azaltma kullanıldı. Quadcore makinemde N = 22 için 9:49 dakika içinde çalışır.
Son güncelleme (sanırım). F seçimleri için daha iyi denklik sınıfları, çalışma zamanını N = 22 için 3:19 dakikaya kadar kesme süresi 57 saniye (düzenleme: Yanlışlıkla sadece bir iplik ile çalıştırdım) makinemde.
Bu değişiklik, bir vektörün döndürülerek diğerine dönüştürülebilmesi durumunda aynı baştaki sıfırları üretmesi gerçeğinden yararlanır. Ne yazık ki, oldukça kritik bir düşük seviye optimizasyonu, bit gösteriminde F'nin üst bitinin her zaman aynı olmasını gerektirir ve bu denklik kullanılırken, arama alanını bir miktar kısalttı ve çalışma süresini farklı bir durum alanı kullanarak yaklaşık dörtte bir oranında azaltın F üzerinde azalma, ek yükün düşük seviyeli optimizasyonu ortadan kaldırmaktan daha fazla mahsup etmesi. Bununla birlikte, bu sorunun, birbirinin tersi olan F'nin de eşdeğer olduğu gerçeği göz önüne alınarak giderilebileceği ortaya çıkmıştır. Bu, denklik sınıflarının hesaplanmasının karmaşıklığına biraz eklenirken, aynı zamanda yukarıda belirtilen düşük seviye optimizasyonunu sürdürmeme izin verdi, bu da yaklaşık x3'lük bir hızlanmaya yol açtı.
Birikmiş veriler için 128 bit tamsayıları destekleyen bir güncelleme daha. 128 bitlik tamsayılar ile derlemek için, gerekir longint.nim
dan burada ve birlikte derlemek için -d:use128bit
. N = 24 hala 10 dakikadan fazla sürüyor, ancak ilgilenenler için aşağıdaki sonucu ekledim.
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)