นิมรอด (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
ด้วย)
การติดตั้งใช้สิ่งอำนวยความสะดวก metaprogramming ของ Nimrod ในการเปิด / ปิดอินไลน์ระดับแรก ๆ ของการค้นหาแบบเรียกซ้ำ สิ่งนี้ช่วยประหยัดเวลาเล็กน้อยเมื่อใช้ gcc 4.8 และ 4.9 เป็นแบ็กเอนด์ของนิมโรดและจำนวนที่เหมาะสมสำหรับเสียงดังกราว
พื้นที่การค้นหาอาจถูกตัดทิ้งเพิ่มเติมโดยการสังเกตว่าเราเพียงแค่ต้องพิจารณาค่าของ S ที่แตกต่างกันในตำแหน่ง N แรกจำนวนเท่ากันจากทางเลือกของ F อย่างไรก็ตามความซับซ้อนหรือความต้องการหน่วยความจำของที่ไม่ขยายสำหรับค่าขนาดใหญ่ ของ 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 วินาทีเมื่อใช้สี่คอร์
N = 22 ใช้เวลาประมาณ 15 นาทีบนเครื่อง 64-core กับ GCC 4.4.6 เป็นแบ็กเอนด์นิมและล้นจำนวนเต็ม 64 บิตในleadingZeros[0]
(อาจจะไม่ได้คนที่ไม่ได้ลงชื่อไม่ได้มองไปที่มัน)
อัปเดต: ฉันพบที่ว่างสำหรับการปรับปรุงเพิ่มเติมอีกสองสามข้อ อันดับแรกสำหรับค่าที่กำหนดF
เราสามารถระบุ 16 รายการแรกของS
เวกเตอร์ที่เกี่ยวข้องได้อย่างแม่นยำเพราะพวกมันต้องแตกต่างกันในN/2
สถานที่ ดังนั้นเราจึง precompute รายการเวกเตอร์บิตของขนาดN
ที่มีN/2
บิตชุดและใช้เหล่านี้จะได้รับส่วนเริ่มต้นของจากS
F
ประการที่สองเราสามารถปรับปรุงการค้นหาแบบเรียกซ้ำโดยการสังเกตว่าเรารู้คุณค่าของเสมอF[N]
(เนื่องจาก MSB เป็นศูนย์ในการแทนค่าบิต) สิ่งนี้ช่วยให้เราสามารถทำนายได้อย่างแม่นยำว่าสาขาใดที่เรารับเงินคืนจากผลิตภัณฑ์ภายใน ในขณะที่จริงจะช่วยให้เราเปลี่ยนการค้นหาทั้งหมดเป็นวนซ้ำแบบซ้ำ ๆ ที่เกิดขึ้นจริงจะทำให้การคาดคะเนสาขาแตกหักเล็กน้อยดังนั้นเราจึงรักษาระดับสูงสุดไว้ในรูปแบบดั้งเดิม เรายังคงประหยัดเวลาส่วนใหญ่โดยการลดจำนวนสาขาที่เราทำ
สำหรับการล้างข้อมูลรหัสตอนนี้ใช้จำนวนเต็มไม่ได้ลงนามและแก้ไขได้ที่ 64- บิต (ในกรณีที่มีคนต้องการเรียกใช้บนสถาปัตยกรรม 32 บิต)
การเร่งความเร็วโดยรวมอยู่ระหว่างปัจจัย x3 และ x4 N = 22 ยังคงต้องการมากกว่าแปดแกนที่จะทำงานในอายุต่ำกว่า 10 นาที แต่บนเครื่อง 64-core ก็ตอนนี้ลงไปประมาณสี่นาที (ด้วยการ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]
อัปเดตอีกครั้งใช้ประโยชน์จากการลดพื้นที่การค้นหาที่เป็นไปได้เพิ่มเติม ทำงานในเวลาประมาณ 9:49 นาทีสำหรับ N = 22 บนเครื่อง quadcore ของฉัน
การปรับปรุงครั้งสุดท้าย (ฉันคิดว่า) คลาสความเท่าเทียมที่ดีกว่าสำหรับตัวเลือกของ F ตัด runtime สำหรับ 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)