ニムロッド(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のメタプログラミング機能を使用して、再帰検索の最初のいくつかのレベルを展開/インライン化します。これにより、gim 4.8および4.9をNimrodのバックエンドとして使用するときの時間を節約し、clangをかなり節約できます。
探索空間は、Fの選択と最初のN個の偶数個の位置で異なるSの値のみを考慮する必要があることを観察することにより、さらに刈り取ることができます。これらの場合、ループ本体が完全にスキップされるため、
内積がゼロである場所を集計することは、ループでビットカウント機能を使用するよりも高速に見えます。どうやらテーブルにアクセスすると、かなり良い局所性があります。
再帰的な検索がどのように機能するかを考えると、問題は動的プログラミングに適しているように見えますが、合理的な量のメモリでそれを行う明らかな方法はありません。
出力例:
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秒かかり、4つのコアを使用している場合は2.3秒かかります。
N = 22は、Nimrodのバックエンドとしてgcc 4.4.6を搭載した64コアマシンで約15分かかり、64ビット整数をオーバーフローさせますleadingZeros[0]
(おそらく符号なしのものではなく、まだ見ていない)。
更新:さらにいくつかの改善の余地を見つけました。まず、与えられたの値に対してF
、対応するS
ベクトルの最初の16エントリを正確に列挙できますN/2
。それらは正確な場所で異なる必要があるためです。したがって、ビットが設定さN
れているサイズのビットベクトルのリストを事前計算し、N/2
これらを使用してS
fromの初期部分を導出しF
ます。
第二に、F[N]
(MSBはビット表現でゼロであるため)の値を常に知っていることを観察することにより、再帰検索を改善できます。これにより、内積から再帰するブランチを正確に予測できます。これにより実際に検索全体を再帰ループに変えることができますが、実際には分岐予測がかなり台無しになるため、トップレベルを元の形式に保ちます。主に実行する分岐の量を減らすことにより、時間を節約できます。
いくつかのクリーンアップのために、コードは符号なし整数を使用し、64ビットで修正します(誰かが32ビットアーキテクチャでこれを実行したい場合に備えて)。
全体的なスピードアップは、3倍から4倍です。N = 22では10分未満で実行するには8個以上のコアが必要ですが、64コアのマシンでは約4分になりました(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]
再び更新され、検索スペースのさらなる削減を活用しました。クアッドコアマシンでN = 22の場合、約9:49分で実行されます。
最終更新(私は思う)。Fの選択に対する同等のクラスが改善され、N = 22のランタイムが3:19分 57秒に短縮されました(編集:誤って1つのスレッドで実行していました)。
この変更では、一方を回転させて他方に変換できる場合、ベクトルのペアが同じ先行ゼロを生成するという事実を利用します。残念ながら、かなり重要な低レベルの最適化では、ビット表現のFの最上位ビットが常に同じである必要がありますが、この等価性を使用すると、検索スペースが大幅に削減され、異なる状態スペースを使用するよりも約4分の1だけ実行時間が短縮されますFの削減、低レベルの最適化を相殺する以上の排除によるオーバーヘッド。ただし、この問題は、互いに逆のFも等価であるという事実も考慮することで解消できることがわかります。これにより、等価クラスの計算が少し複雑になりましたが、前述の低レベルの最適化を維持することもでき、約3倍の高速化につながりました。
累積データの128ビット整数をサポートするためのもう1つの更新。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)