猎人(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的后端时,这可以节省一些时间,而对于clang来说可以节省很多时间。
通过观察我们只需要考虑与我们选择的F的前N个偶数个偶数个数不同的S值,就可以进一步缩小搜索空间。但是,对于较大的值,S的复杂性或存储需求不会扩展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秒。
在具有gcc 4.4.6作为Nimrod后端的64核计算机上,N = 22大约需要15分钟,并且会溢出64位整数leadingZeros[0]
(可能不是未签名的整数,没有看过)。
更新:我发现了一些进一步改进的空间。首先,对于给定的值F
,我们可以S
精确地枚举对应向量的前16个条目,因为它们的位置必须完全不同N/2
。所以我们预先计算大小的位向量的名单N
有N/2
位设置和使用这些推导的初始部分S
的F
。
其次,我们可以通过观察我们始终知道的值F[N]
(因为在位表示中MSB为零)来改进递归搜索。这使我们可以准确地预测从内部乘积递归到哪个分支。虽然这实际上使我们可以将整个搜索变成一个递归循环,但实际上恰好使分支预测搞砸了很多,因此我们将顶层保留为原始形式。我们仍然节省了一些时间,主要是通过减少正在执行的分支数量。
为了进行一些清理,代码现在使用无符号整数并将其修复为64位(以防万一有人希望在32位体系结构上运行此整数)。
总体提速介于x3和x4之间。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秒(编辑:我不小心只用一个线程运行了它)。
这种改变利用了这样的事实:如果一对矢量可以通过旋转将其转换为另一矢量,则它们会产生相同的前导零。不幸的是,一个相当关键的低级优化要求位表示中的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)