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)
Biên dịch với
nimrod cc --threads:on -d:release count.nim
(Nimrod có thể được tải xuống ở đây .)
Điều này chạy trong thời gian quy định cho n = 20 (và cho n = 18 khi chỉ sử dụng một luồng duy nhất, mất khoảng 2 phút trong trường hợp sau).
Thuật toán sử dụng tìm kiếm đệ quy, cắt tỉa cây tìm kiếm bất cứ khi nào gặp sản phẩm bên trong khác không. Chúng tôi cũng cắt giảm một nửa không gian tìm kiếm bằng cách quan sát rằng đối với bất kỳ cặp vectơ nào, (F, -F)
chúng tôi chỉ cần xem xét một vì các cái khác tạo ra cùng một bộ sản phẩm bên trong chính xác (bằng cách phủ địnhS
cũng).
Việc triển khai sử dụng các phương tiện siêu lập trình của Nimrod để hủy đăng ký / nội tuyến trong một vài cấp độ đầu tiên của tìm kiếm đệ quy. Điều này giúp tiết kiệm một ít thời gian khi sử dụng gcc 4.8 và 4.9 làm phần phụ trợ của Nimrod và một số tiền hợp lý cho tiếng kêu.
Không gian tìm kiếm có thể được cắt tỉa thêm bằng cách quan sát rằng chúng ta chỉ cần xem xét các giá trị của S khác nhau về số lượng vị trí N đầu tiên so với lựa chọn F. Tuy nhiên, sự phức tạp hoặc nhu cầu bộ nhớ của nó không mở rộng cho các giá trị lớn của N, cho rằng thân vòng lặp hoàn toàn bị bỏ qua trong những trường hợp đó.
Việc lập bảng trong đó sản phẩm bên trong bằng 0 dường như nhanh hơn việc sử dụng bất kỳ chức năng đếm bit nào trong vòng lặp. Rõ ràng truy cập vào bảng có địa phương khá tốt.
Có vẻ như vấn đề cần phải tuân theo đối với lập trình động, xem xét cách tìm kiếm đệ quy hoạt động, nhưng không có cách nào rõ ràng để làm điều đó với một lượng bộ nhớ hợp lý.
Kết quả ví dụ:
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]
Đối với mục đích so sánh thuật toán với các triển khai khác, N = 16 mất khoảng 7,9 giây trên máy của tôi khi sử dụng một luồng và 2,3 giây khi sử dụng bốn lõi.
N = 22 mất khoảng 15 phút trên máy 64 lõi với gcc 4.4.6 làm phụ trợ của Nimrod và tràn số nguyên 64 bit vào leadingZeros[0]
(có thể không phải là không dấu, không nhìn vào nó).
Cập nhật: Tôi đã tìm thấy phòng cho một vài cải tiến. Đầu tiên, với một giá trị nhất định F
, chúng ta có thể liệt kê S
chính xác 16 mục nhập của các vectơ tương ứng , vì chúng phải khác nhau ở N/2
những vị trí chính xác . Vì vậy, chúng tôi tính toán trước một danh sách các vectơ bit có kích thước N
có N/2
các bit được đặt và sử dụng chúng để lấy phần ban đầu S
từ F
.
Thứ hai, chúng ta có thể cải thiện tìm kiếm đệ quy bằng cách quan sát rằng chúng ta luôn biết giá trị của F[N]
(vì MSB bằng 0 trong biểu diễn bit). Điều này cho phép chúng tôi dự đoán chính xác nhánh nào chúng tôi thu hồi từ sản phẩm bên trong. Mặc dù điều đó thực sự sẽ cho phép chúng ta biến toàn bộ tìm kiếm thành một vòng lặp đệ quy, nhưng điều đó thực sự xảy ra để làm hỏng dự đoán nhánh khá nhiều, vì vậy chúng tôi giữ các mức cao nhất ở dạng ban đầu. Chúng tôi vẫn tiết kiệm thời gian, chủ yếu bằng cách giảm số lượng phân nhánh chúng tôi đang làm.
Đối với một số dọn dẹp, mã hiện đang sử dụng các số nguyên không dấu và sửa chúng ở 64 bit (chỉ trong trường hợp ai đó muốn chạy mã này trên kiến trúc 32 bit).
Tăng tốc tổng thể là giữa một yếu tố của x3 và x4. N = 22 vẫn cần nhiều hơn tám lõi để chạy trong vòng dưới 10 phút, nhưng trên máy 64 lõi, giờ chỉ còn khoảng bốn phút (vớinumThreads
va chạm tương ứng). Tuy nhiên, tôi không nghĩ có nhiều chỗ để cải tiến hơn nếu không có thuật toán khác.
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]
Cập nhật lại, sử dụng các mức giảm có thể hơn nữa trong không gian tìm kiếm. Chạy trong khoảng 9:49 phút cho N = 22 trên máy quadcore của tôi.
Cập nhật cuối cùng (tôi nghĩ). Các lớp tương đương tốt hơn cho các lựa chọn F, cắt thời gian chạy cho N = 22 xuống còn 3:19 phút 57 giây (chỉnh sửa: Tôi đã vô tình chạy nó chỉ với một luồng) trên máy của mình.
Sự thay đổi này sử dụng thực tế là một cặp vectơ tạo ra các số 0 đứng đầu giống nhau nếu có thể biến đổi thành một số khác bằng cách xoay nó. Thật không may, tối ưu hóa mức độ khá quan trọng đòi hỏi bit F trên cùng trong biểu diễn bit luôn giống nhau và trong khi sử dụng tính tương đương này đã làm giảm không gian tìm kiếm khá nhiều và giảm khoảng một phần tư thời gian sử dụng một không gian trạng thái khác giảm trên F, chi phí từ việc loại bỏ tối ưu hóa mức thấp hơn là bù lại. Tuy nhiên, hóa ra vấn đề này có thể được loại bỏ bằng cách xem xét thực tế rằng F là nghịch đảo của nhau cũng tương đương. Mặc dù điều này làm tăng thêm tính phức tạp của việc tính toán các lớp tương đương một chút, nhưng nó cũng cho phép tôi giữ lại tối ưu hóa mức thấp đã nói ở trên, dẫn đến tăng tốc khoảng x3.
Thêm một bản cập nhật để hỗ trợ số nguyên 128 bit cho dữ liệu tích lũy. Để biên dịch với số nguyên 128 bit, bạn sẽ cần longint.nim
từ đây và biên dịch với -d:use128bit
. N = 24 vẫn mất hơn 10 phút, nhưng tôi đã bao gồm kết quả bên dưới cho những người quan tâm.
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)