님로드 (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 분 소요).
이 알고리즘은 재귀 검색을 사용하여 0이 아닌 내부 제품이 발견 될 때마다 검색 트리를 제거합니다. 또한 한 쌍의 벡터에 (F, -F)
대해 하나만 고려 하면된다는 사실을 관찰함으로써 검색 공간을 반으로 줄였습니다 S
.
이 구현은 Nimrod의 메타 프로그래밍 기능을 사용하여 재귀 검색의 처음 몇 단계를 풀거나 인라인합니다. 이렇게하면 gcc 4.8 및 4.9를 Nimrod의 백엔드로 사용하고 상당한 양의 clang을 사용할 때 시간이 절약됩니다.
F의 선택과 짝수의 첫 번째 N 위치가 다른 S의 값만 고려하면된다는 사실을 관찰함으로써 검색 공간을 추가로 정리할 수있었습니다. 이 경우 루프 바디를 완전히 건너 뛴 경우 N
내부 제품이 0 인 곳을 표로 작성하는 것이 루프에서 비트 카운팅 기능을 사용하는 것보다 빠릅니다. 분명히 테이블에 액세스하면 꽤 좋은 지역성이 있습니다.
재귀 검색의 작동 방식을 고려할 때 문제는 동적 프로그래밍에 적합 해야하는 것처럼 보이지만 합리적인 양의 메모리로이를 수행하는 확실한 방법은 없습니다.
출력 예 :
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
에서을 F
.
둘째, F[N]
비트 값에서 MSB가 0이므로 항상 값을 알고 있음을 관찰하여 재귀 검색을 개선 할 수 있습니다 . 이를 통해 우리는 내부 제품에서 어떤 분기로 재귀하는지 정확하게 예측할 수 있습니다. 실제로 전체 검색을 재귀 루프로 바꿀 수는 있지만 실제로 분기 예측을 약간 어렵게 만들므로 최상위 수준을 원래 형태로 유지합니다. 우리는 주로 우리가하고있는 분기의 양을 줄임으로써 여전히 시간을 절약합니다.
일부 정리를 위해 코드는 이제 부호없는 정수를 사용하고 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 초로 줄입니다 (편집 : 실수로 하나의 스레드로 실행했습니다).
이 변경은 벡터가 회전하여 다른 벡터로 변환 될 수있는 경우 한 쌍의 벡터가 동일한 선행 0을 생성한다는 사실을 이용합니다. 불행히도, 매우 중요한 저수준 최적화는 비트 표현에서 F의 최상위 비트가 항상 동일해야하며,이 동등성을 사용하면 검색 공간이 상당히 줄어들고 다른 상태 공간을 사용하여 런타임이 약 1/4 감소합니다. 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)