หากคุณไม่จำเป็นต้องมีการสุ่มที่มีคุณภาพสูงมากและใกล้ชิดกับเครื่องแบบกระจายเป็นสิ่งที่ดีพอที่คุณสามารถไปจริงๆอย่างรวดเร็วโดยเฉพาะใน CPU ที่ทันสมัยด้วยประสิทธิภาพ SIMD เวกเตอร์จำนวนเต็มเช่น 86 x กับ SSE2 หรือ AVX2
นี่เป็นเหมือนคำตอบของ @ NominalAnimalเนื่องจากเราทั้งคู่มีความคิดเดียวกัน แต่ vectorized ด้วยตนเองสำหรับ x86 (และด้วยตัวเลขสุ่มที่มีคุณภาพแย่กว่านั้น แต่ก็ยังดีพอสำหรับกรณีใช้งานจำนวนมาก) สิ่งนี้ทำงานได้เร็วกว่าโค้ดของ @ Nominal ประมาณ 15 หรือ 30 เท่าที่เอาต์พุต ASCII ~ 13GB / s บน 2.5GHz Intel Haswell CPU ที่มี AVX2 นั่นยังคงน้อยกว่าแบนด์วิดท์หน่วยความจำหลักสูงสุดทางทฤษฎี (ดูอัลแชนแนล DDR3-1600 ประมาณ 25.6GB / s) แต่ฉันเขียนเวลาไปที่ / dev / null ดังนั้นจริงๆแล้วมันแค่เขียนบัฟเฟอร์ที่ร้อนในแคช Skylake ควรเรียกใช้รหัสเดียวกันนี้เร็วกว่า Haswell (ดูที่ด้านล่างของคำตอบนี้)
สมมติว่าคุณมีปัญหาคอขวดใน I / O กับดิสก์หรือการวางท่อนี้ที่ใดที่หนึ่งการใช้งานที่รวดเร็วหมายความว่า CPU ของคุณไม่จำเป็นต้องตั้งนาฬิกาให้สูงกว่าที่ไม่ได้ใช้งาน ใช้พลังงานทั้งหมดน้อยกว่ามากในการสร้างผลลัพธ์ (อายุการใช้งานแบตเตอรี่ / ความร้อน / ภาวะโลกร้อน)
เร็วมากจนคุณไม่อยากเขียนมันลงดิสก์ เพียงสร้างใหม่ตามต้องการ (จากเมล็ดเดียวกันหากคุณต้องการข้อมูลเดิมอีกครั้ง) แม้ว่าคุณต้องการป้อนเข้าสู่กระบวนการแบบมัลติเธรดที่สามารถใช้ CPU ทั้งหมดการเรียกใช้งานนี้เพื่อไพพ์ข้อมูลจะทำให้ร้อนในแคช L3 (และแคช L2 บนแกนที่เขียน) และใช้มาก เวลา CPU เล็กน้อย (แต่โปรดทราบว่า piping เพิ่มค่าใช้จ่ายจำนวนมากเมื่อเทียบกับการเขียน/dev/null
บน Skylake i7-6700k การไพพ์ไปยังwc -c
หรือโปรแกรมอื่นที่เพิ่งอ่าน + ทิ้งอินพุตมันประมาณ 8 เท่าช้ากว่าการเขียน/dev/null
และใช้ 70% ของ a เท่านั้น CPU แต่ก็ยังคง 4.0GB / s ในซีพียู 3.9GHz
การสร้างใหม่นั้นเร็วกว่าการอ่านอีกครั้งแม้จะมาจาก SSD ที่เชื่อมต่อ PCIe อย่างรวดเร็ว แต่ IDK ถ้ามันมีประสิทธิภาพการใช้พลังงานมากกว่า (ตัวคูณทวีคูณแบบเวกเตอร์จำนวนมากยังคงยุ่งอยู่และอาจหิวมากพร้อมกับ AVX2 อื่น ๆ 256b ALU เวกเตอร์) OTOH ฉันไม่รู้ว่าเวลาในการอ่าน CPU ของดิสก์จะถูกนำออกไปจากสิ่งที่เพิ่มจำนวนคอร์ทั้งหมดที่ประมวลผลอินพุตนี้ ฉันเดาว่าการสลับบริบทเพื่อสร้างชิ้นส่วนอีกครั้งใน 128k อาจแข่งขันกับการเรียกใช้รหัสระบบไฟล์ / pagecache และจัดสรรหน้าเพื่ออ่านข้อมูลจากดิสก์ แน่นอนถ้ามันร้อนใน pagecache มันก็แค่ memcpy OTOH เราเขียนเร็วพอ ๆ กับ memcpy! (ซึ่งต้องแยกแบนด์วิธหน่วยความจำหลักระหว่างการอ่านและการเขียน) (โปรดทราบว่าการเขียนไปยังหน่วยความจำที่ 'rep movsb
(เพิ่มประสิทธิภาพ memcpy และ memset ในไมโครโค้ดซึ่งหลีกเลี่ยง RFO เนื่องจากการดำเนินการของ Andy Glew ใน P6 (Pentium Pro ))
จนถึงตอนนี้เป็นเพียงการพิสูจน์แนวคิดและการจัดการขึ้นบรรทัดใหม่มีความถูกต้องโดยประมาณเท่านั้น มันผิดที่ส่วนท้ายของบัฟเฟอร์ power-of-2 ด้วยเวลาในการพัฒนาที่มากขึ้น ฉันมั่นใจว่าฉันสามารถหาวิธีที่มีประสิทธิภาพมากขึ้นในการแทรกบรรทัดใหม่ที่ถูกต้องอย่างแน่นอนด้วยค่าใช้จ่ายอย่างน้อยที่สุดเท่าที่จะทำได้ (เทียบกับการแสดงผลเฉพาะที่ว่าง) ฉันคิดว่านี่เป็นสิ่งที่ชอบ 10 ถึง 20% ฉันแค่สนใจที่จะรู้ว่าเราสามารถวิ่งได้เร็วแค่ไหนไม่ได้มีเวอร์ชั่นที่ขัดมันจริง ๆ ดังนั้นฉันจะปล่อยให้ส่วนนั้นเป็นแบบฝึกหัดสำหรับผู้อ่านพร้อมความคิดเห็นที่อธิบายแนวคิดบางอย่าง
บน Haswell i5 ที่เทอร์โบสูงสุด 2.5GHz พร้อม DDR3-1600MHz RAMหมดเวลาผลิต 100GiB แต่ลดขนาดลง (หมดเวลาใน cygwin64 บน Win10 ด้วย gcc5.4 -O3 -march=native
ถูกละเว้น-funroll-loops
เนื่องจากฉันมีเวลามากพอที่จะได้รับเวลาที่เหมาะสมบนแล็ปท็อปที่ยืมมานี้ควรเพิ่งบูต Linux บน USB)
เขียนถึง / dev / null เว้นแต่จะระบุไว้เป็นอย่างอื่น
- James Hollis's: (ไม่ผ่านการทดสอบ)
- รุ่น fwrite ที่กำหนด: ~ 2.21s
- สิ่งนี้ (SSE2): ~ 0.142 วินาที (ไม่ จำกัด ขนาด = จริง = 14.232 วินาที, ผู้ใช้ = 13.999s, sys = 0.187s)
- สิ่งนี้ (AVX-128): ~ 0.140 วินาที
- สิ่งนี้ (AVX2): ~ 0.073 วินาที (ไม่ระบุมาตราส่วน : จริง = 0m7.291s, ผู้ใช้ = 0m7.125s, sys = 0m0.155s)
- (AVX2) cygwin ไปป์
wc -c
ไลน์ด้วยขนาดบัฟเฟอร์ 128kiB: 0.32 วินาทีพร้อม CPU ที่ 2.38GHz (เทอร์โบ dual-core สูงสุด) (เวลาที่ไม่ถูกปรับสัดส่วน: จริง = 32.466s ผู้ใช้ = 11.468s sys = 41.092s รวมทั้งสิ่งนี้และwc
) แต่มีเพียงครึ่งหนึ่งที่คัดลอกข้อมูลจริง ๆ เพราะโปรแกรมโง่ ๆ ของฉันสมมติว่าการเขียนทำบัฟเฟอร์เต็มแม้ว่าไม่ใช่กรณีและ cygwin write () ทำได้ 64k ต่อการโทรหนึ่งครั้งเท่านั้น
ดังนั้นด้วย SSE2 นี่จะเร็วกว่าโค้ดสเกลาร์ของ @Nominal Animal ประมาณ 15 เท่า ด้วย AVX2 จะเร็วกว่าประมาณ 30 เท่า ฉันไม่ได้ลองใช้รหัสของ Nominal รุ่นที่เพิ่งใช้write()
แทนfwrite()
แต่น่าจะเป็นเพราะบัฟเฟอร์ขนาดใหญ่ stdio ส่วนใหญ่ยังคงอยู่นอกเส้นทาง หากเป็นการคัดลอกข้อมูลนั่นจะทำให้เกิดการชะลอตัวเป็นจำนวนมาก
เวลาในการผลิตข้อมูล 1GB บน Core2Duo E6600 (Merom 2.4GHz, 32kiB ส่วนตัว L1, 4MiB แคช L2 ที่ใช้ร่วมกัน 4MiB), DDR2-533MHzใน 64-bit Linux 4.2 (Ubuntu 15.10) ยังคงใช้ขนาดบัฟเฟอร์ 128kiB สำหรับการเขียน () ไม่ได้สำรวจมิตินั้น
เขียนถึง / dev / null เว้นแต่จะระบุไว้เป็นอย่างอื่น
- (SSE2) สิ่งนี้พร้อมกับการจัดการบรรทัดใหม่และ 4 เวกเตอร์ของตัวเลขจากแต่ละเวกเตอร์ของการสุ่มไบต์: 0.183s (หมดเวลาทำ 100GiB ใน 18.3s แต่ผลลัพธ์ที่คล้ายกันสำหรับการวิ่ง 1GiB) 1.85 คำแนะนำต่อรอบ
- (SSE2) สิ่งนี้ส่งไปที่
wc -c
: 0.593 วินาที (ไม่ถูกปรับสัดส่วน : จริง = 59.266s ผู้ใช้ = 20.148s sys = 1m6.548s รวมถึงเวลา CPU ของ wc) จำนวนการเขียน () ระบบที่เรียกเช่นเดียวกับ cygwin แต่จริง ๆ แล้วการไพพ์ข้อมูลทั้งหมดเนื่องจาก Linux จัดการ 128k ทั้งหมดของการเขียน () ไปยังไพพ์
fwrite()
เวอร์ชันของ NominalAnimal (gcc5.2 -O3 -march=native
) ทำงานด้วย./decdig 100 $((1024*1024*1024/200)) > /dev/null
: 3.19s +/- 0.1% พร้อมคำแนะนำ 1.40 ต่อรอบ -Funroll-loops อาจจะแตกต่างกันเล็กน้อย clang-3.8 -O3 -march=native
: 3.42 วินาที +/- 0.1%
- Nomine-
fwrite
piping wc -c
: real = 3.980s user = 3.176s sys = 2.080s
- เวอร์ชั่น line-at-a-time ของ James Hollis (
clang++-3.8 -O3 -march=native
): 22.885s +/- 0.07% โดยมี 0.84 คำสั่งต่อรอบ (g ++ 5.2 ช้ากว่าเล็กน้อย: 22.98 วินาที) การเขียนเพียงครั้งละหนึ่งบรรทัดอาจเจ็บอย่างมาก
- Stéphane Chazelas's
tr < /dev/urandom | ...
: real = 41.430s ผู้ใช้ = 26.832s sys = 40.120s tr
กำลังทำให้แกน CPU ทั้งหมดใช้เวลาส่วนใหญ่ใช้เวลาเกือบทั้งหมดในเคอร์เนลไดรเวอร์ที่สร้างไบต์แบบสุ่มและคัดลอกไปยังไพพ์ แกนอื่น ๆ ของเครื่องแกนคู่นี้ใช้ส่วนที่เหลือของไปป์ไลน์
time LC_ALL=C head -c512M </dev/urandom >/dev/null
: นั่นเป็นเพียงการอ่านที่สุ่มมากที่ไม่มีท่อ: จริง = 35.018s ผู้ใช้ = 0.036s sys = 34.940s
- โปรแกรม perl ของLưuVĩnhPhúc (perl v5.20.2 จาก Ubuntu15.10)
LANG=en_CA.UTF-8
:: จริง = 4m32.634s ผู้ใช้ = 4m3.288s sys = 0m29.364
LC_ALL=C LANG=C
: จริง = 4m18.637s ผู้ใช้ = 3m50.324s sys = 0m29.356s ยังช้ามาก
- (SSE2) สิ่งนี้โดยไม่มีการจัดการบรรทัดใหม่และ 3 หรือ 4 เวกเตอร์ของตัวเลขจากแต่ละเวกเตอร์ของการสุ่มไบต์ (เกือบความเร็วเดียวกัน:
dig3 = v%10
ขั้นตอนเกี่ยวกับการแบ่งเท่า ๆ กันบน HW นี้): 0.166s (1.82 คำแนะนำต่อรอบ) . นี่เป็นข้อ จำกัด ขั้นต่ำสำหรับสิ่งที่เราสามารถเข้าใกล้ได้ด้วยการจัดการขึ้นบรรทัดใหม่ที่มีประสิทธิภาพอย่างสมบูรณ์แบบ
- (SSE2) รุ่นเก่านี้กับการจัดการการขึ้นบรรทัดใหม่ไม่ แต่เพียงการหนึ่งหลักต่อองค์ประกอบ uint16_t ใช้
v%10
, 0.222 วินาที +/- 0.4%, 2.12 คำแนะนำต่อวงจร (คอมไพล์ด้วย gcc5.2,. -march=native -O3 -funroll-loops
เกิดลูปการคลี่คลายเพื่อช่วยให้โค้ดนี้กับฮาร์ดแวร์นี้อย่าใช้อย่างสุ่มสี่สุ่มห้าโดยเฉพาะอย่างยิ่งสำหรับโปรแกรมขนาดใหญ่)
- (SSE2) เวอร์ชันเก่านี้เขียนลงไฟล์ (บน RAID10f2 ของฮาร์ดไดรฟ์แม่เหล็กเร็ว 3 ตัวซึ่งไม่เหมาะสำหรับการเขียน): ~ 4 วินาที สามารถทำงานได้เร็วขึ้นโดยปรับแต่งการตั้งค่าบัฟเฟอร์เคอร์เนล I / O เพื่อให้ข้อมูลสกปรกมากขึ้นก่อนการเขียนบล็อก () เวลา "ระบบ" ยังคงอยู่ ~ 1.0 วินาทีสูงกว่าเวลา "ผู้ใช้" มาก ในระบบเก่านี้ที่มี DDR2-533 RAM ช้ามันจะใช้เวลานานขึ้น ~ 4x สำหรับเคอร์เนลในการบันทึกข้อมูลลงใน pagecache และเรียกใช้ฟังก์ชั่น XFS มากกว่าที่จะให้ลูปของฉันทำการเขียนใหม่ในบัฟเฟอร์ที่ยังร้อนอยู่ ขุมทรัพย์
มันเป็นอย่างไร
PRNG ที่รวดเร็วนั้นสำคัญมาก xorshift128 +สามารถแปลงเป็นเวกเตอร์ได้ดังนั้นคุณจึงมีเครื่องกำเนิด 64 บิตสองหรือสี่ตัวขนานในองค์ประกอบของเวกเตอร์ SIMD แต่ละขั้นตอนสร้างเวกเตอร์เต็มจำนวนแบบสุ่มไบต์ ( การใช้งาน 256b AVX2 ที่นี่พร้อมกับ Intel ที่แท้จริง ) ฉันเลือกมันมากกว่าทางเลือกที่กำหนดของ xorshift * เพราะ 64 บิตเวกเตอร์จำนวนเต็มคูณเป็นไปได้เฉพาะใน SSE2 / AVX2 กับเทคนิคการขยายความแม่นยำ
ด้วยเวกเตอร์สุ่มจำนวนไบต์เราสามารถสับองค์ประกอบ 16 บิตแต่ละรายการเป็นเลขทศนิยมหลายหลัก เราผลิตหลายเวกเตอร์ขององค์ประกอบ 16 บิตที่มีแต่ละคน ASCII พื้นที่หลัก เราเก็บสิ่งนั้นไว้ในบัฟเฟอร์ผลลัพธ์โดยตรง
เวอร์ชันเดิมของฉันเพิ่งใช้x / 6554
เพื่อรับหนึ่งตัวเลขแบบสุ่มจากทุกองค์ประกอบ uint16_t ของเวกเตอร์ มันมักจะอยู่ระหว่าง 0 และ 9 รวม มันเอนเอียงไป9
เพราะ(2^16 -1 ) / 6554
เพียง 9.99923 (6554 = ceil ((2 ^ 16-1) / 10) ซึ่งรับรองว่าผลหารเสมอ <10)
x/6554
สามารถคำนวณด้วยการคูณด้วยค่าคงที่ "เวทย์มนตร์" ( การกำหนดจุดตายตัว ) และการเลื่อนขวาของผลลัพธ์ครึ่งสูง นี่เป็นกรณีที่ดีที่สุดสำหรับการหารโดยค่าคงที่; ตัวหารบางตัวใช้เวลาในการดำเนินการมากขึ้น x % 10
มีอคติที่คล้ายกันและไม่ถูกคำนวณ (gm's asm output เทียบเท่ากับx - 10*(x/10)
คือการเพิ่มทวีคูณและการลบที่ด้านบนของส่วนโดยใช้การคูณแบบแยกส่วนแบบแยกส่วน) นอกจากนี้บิตต่ำสุดของ xorshift128 + นั้นไม่ได้คุณภาพสูงดังนั้นการหารเพื่อนำเอนโทรปีจากบิตสูงนั้นดีกว่า ( สำหรับคุณภาพและความเร็ว) กว่าโมดูโลที่ใช้เอนโทรปีจากบิตต่ำ
อย่างไรก็ตามเราสามารถใช้เอนโทรปีได้มากขึ้นในแต่ละ uint16_t โดยดูที่ตัวเลขทศนิยมต่ำเช่นdigit()
ฟังก์ชันของ @ Nominal เพื่อประสิทธิภาพสูงสุดฉันตัดสินใจที่จะใช้ทศนิยม 3 หลักต่ำและx/6554
เพื่อบันทึกหนึ่ง PMULLW และ PSUBW (และอาจเป็น MOVDQA บางส่วน) เทียบกับตัวเลือกคุณภาพที่สูงขึ้นของการใช้ตัวเลขทศนิยมต่ำ 4 หลัก x / 6554 ได้รับผลกระทบเล็กน้อยจากตัวเลขทศนิยม 3 หลักที่ต่ำดังนั้นจึงมีความสัมพันธ์ระหว่างตัวเลขจากองค์ประกอบเดียวกัน (8 หรือ 16 หลักในการแยกเอาต์พุต ASCII ขึ้นอยู่กับความกว้างของเวกเตอร์)
ฉันคิดว่า gcc จะหารด้วย 100 และ 1,000 แทนที่จะเป็นโซ่ที่ยาวกว่าซึ่งหารด้วย 10 อย่างต่อเนื่องดังนั้นมันอาจไม่สั้นลงอย่างมีนัยสำคัญถึงความยาวของห่วงโซ่อ้างอิงที่ไม่ใช่แบบพกพาซึ่งผลิต 4 ผลลัพธ์จากแต่ละ PRNG port0 (vector ทวีคูณและกะ) เป็นคอขวดเนื่องจากการคูณแบบแยกส่วนโมดุลและการเลื่อนใน xorshift + ดังนั้นมันจึงมีประโยชน์อย่างมากในการบันทึกเวกเตอร์ทวีคูณ
xorshift + นั้นเร็วมากแม้การใช้สุ่มเพียง 3.3 บิตจากทุก ๆ 16 (เช่นประสิทธิภาพ 20%) นั้นไม่ช้ากว่าการสับเป็นทศนิยมหลายหลัก เราประมาณการกระจายตัวแบบสม่ำเสมอเท่านั้นเพราะคำตอบนี้เน้นที่ความเร็วตราบใดที่คุณภาพไม่เลวร้ายนัก
พฤติกรรมตามเงื่อนไขใด ๆ ที่เก็บหมายเลของค์ประกอบของตัวแปรจะทำงานได้มากขึ้น (แต่อาจจะทำได้ค่อนข้างมีประสิทธิภาพโดยใช้เทคนิคการบรรจุหีบห่อ SIMDอย่างไรก็ตามประสิทธิภาพที่ลดลงสำหรับองค์ประกอบขนาดเล็กตารางการค้นหาหน้ากากแบบสุ่มขนาดใหญ่ไม่สามารถใช้งานได้และไม่มีการสับเปลี่ยนเลน AVX2 ที่มีขนาดเล็กกว่า 32- องค์ประกอบบิต. รุ่น 128b PSHUFB ยังอาจจะสามารถที่จะสร้างหน้ากากในการบินด้วย BMI2 PEXT / PDEP เช่นคุณสามารถทำได้สำหรับ AVX2 มีองค์ประกอบที่มีขนาดใหญ่แต่ก็ยุ่งยากเพราะ 64 บิตจำนวนเต็มเท่านั้นถือ 8 ไบต์. การเชื่อมโยง godbolt ในคำตอบนั้นมีรหัสบางอย่างที่อาจใช้กับการนับองค์ประกอบที่สูงกว่า)
หากเวลาแฝงของ RNG เป็นปัญหาคอขวดเราสามารถทำงานได้เร็วขึ้นด้วยการเรียกใช้เครื่องกำเนิดไฟฟ้าสองเวกเตอร์ในแบบขนานสลับกันเป็นแบบที่เราใช้ คอมไพเลอร์ยังคงสามารถเก็บทุกอย่างได้อย่างง่ายดายในการลงทะเบียนในการวนซ้ำที่ไม่ได้ควบคุมและนั่นทำให้ทั้งสองเครือข่ายสามารถพึ่งพากันในแบบคู่ขนาน
ในเวอร์ชันปัจจุบันการตัดการส่งออกของ PRNG เราจริง ๆ แล้วคอขวดบนพอร์ต 0 ทรูพอร์ตไม่ใช่ PRNG latency ดังนั้นจึงไม่จำเป็นต้องทำเช่นนั้น
รหัส: รุ่น AVX2
เวอร์ชันเต็มที่มีความคิดเห็นเพิ่มเติมเกี่ยวกับคอมไพเลอร์สำรวจ Godbolt
ไม่เป็นระเบียบมากขอโทษฉันต้องนอนและต้องการโพสต์สิ่งนี้
รับรุ่น SSE2, s/_mm256/_mm
, s/256/128/
, s/v16u/v8u/
และการเปลี่ยนแปลงvector_size(32)
ถึง 16 นอกจากนี้ยังมีการเปลี่ยนแปลงเพิ่มขึ้นบรรทัดใหม่จาก 4 * 16-4 * 8 (อย่างที่ฉันบอกว่ารหัสยุ่งและไม่ดีสำหรับการคอมไพล์สองเวอร์ชัน แต่เดิมไม่ได้วางแผนที่จะสร้างเวอร์ชั่น AVX2 แต่จริงๆแล้วฉันต้องการทดสอบบน Haswell CPU ที่ฉันเข้าถึง)
#include <immintrin.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
//#include <string.h>
// This would work equally fast 128b or 256b at a time (AVX2):
// https://stackoverflow.com/questions/24001930/avx-sse-version-of-xorshift128
struct rngstate256 {
__m256i state0;
__m256i state1;
};
static inline __m256i xorshift128plus_avx2(struct rngstate256 *sp)
{
__m256i s1 = sp->state0;
const __m256i s0 = sp->state1;
sp->state0 = s0;
s1 = _mm256_xor_si256(s1, _mm256_slli_epi64(s1, 23));
__m256i state1new = _mm256_xor_si256(_mm256_xor_si256(_mm256_xor_si256(s1, s0),
_mm256_srli_epi64(s1, 18)),
_mm256_srli_epi64(s0, 5));
sp->state1 = state1new;
return _mm256_add_epi64(state1new, s0);
}
// GNU C native vectors let us get the compiler to do stuff like %10 each element
typedef unsigned short v16u __attribute__((vector_size(32)));
__m256i* vec_store_digit_and_space(__m256i vec, __m256i *restrict p)
{
v16u v = (v16u)vec;
v16u ten = (v16u)_mm256_set1_epi16(10);
v16u divisor = (v16u)_mm256_set1_epi16(6554); // ceil((2^16-1) / 10.0)
v16u div6554 = v / divisor; // Basically the entropy from the upper two decimal digits: 0..65.
// Probably some correlation with the modulo-based values, especially dig3, but we do this instead of
// dig4 for more ILP and fewer instructions total.
v16u dig1 = v % ten;
v /= ten;
v16u dig2 = v % ten;
v /= ten;
v16u dig3 = v % ten;
// dig4 would overlap much of the randomness that div6554 gets
const v16u ascii_digitspace = (v16u)_mm256_set1_epi16( (' '<<8) | '0');
v16u *vecbuf = (v16u*)p;
vecbuf[0] = div6554 | ascii_digitspace;
vecbuf[1] = dig1 | ascii_digitspace;
vecbuf[2] = dig2 | ascii_digitspace;
vecbuf[3] = dig3 | ascii_digitspace;
return p + 4; // always a constant number of full vectors
}
void random_decimal_fill_buffer(char *restrict buf, size_t len, struct rngstate256 *restrict rngstate)
{
buf = __builtin_assume_aligned(buf, 32);
// copy to a local so clang can keep state in register, even in the non-inline version
// restrict works for gcc, but apparently clang still thinks that *buf might alias *rngstate
struct rngstate256 rng_local = *rngstate;
__m256i *restrict p = (__m256i*restrict)buf;
__m256i *restrict endbuf = (__m256i*)(buf+len);
static unsigned newline_pos = 0;
do {
__m256i rvec = xorshift128plus_avx2(&rng_local);
p = vec_store_digit_and_space(rvec, p); // stores multiple ASCII vectors from the entropy in rvec
#if 1
// this is buggy at the end or start of a power-of-2 buffer:
// usually there's a too-short line, sometimes a too-long line
const unsigned ncols = 100;
newline_pos += 4*16;
if (newline_pos >= ncols) {
newline_pos -= ncols;
char *cur_pos = (char*)p;
*(cur_pos - newline_pos*2 - 1) = '\n';
}
#endif
// Turning every 100th space into a newline.
// 1) With an overlapping 1B store to a location selected by a counter. A down-counter would be more efficient
// 2) Or by using a different constant for ascii_digitspace to put a newline in one element
// lcm(200, 16) is 400 bytes, so unrolling the loop enough to produce two full lines makes a pattern of full vectors repeat
// lcm(200, 32) is 800 bytes
// a power-of-2 buffer size doesn't hold a whole number of lines :/
// I'm pretty sure this can be solved with low overhead, like maybe 10% at worst.
} while(p <= endbuf-3);
*rngstate = rng_local;
}
#define BUFFER_SIZE (128 * 1024)
const static size_t bufsz = BUFFER_SIZE;
__attribute__((aligned(64))) static char static_buf[BUFFER_SIZE];
int main(int argc, char *argv[])
{
// TODO: choose a seed properly. (Doesn't affect the speed)
struct rngstate256 xorshift_state = {
_mm256_set_epi64x(123, 456, 0x123, 0x456),
_mm256_set_epi64x(789, 101112, 0x789, 0x101112)
};
for (int i=0; i < 1024ULL*1024*1024 / bufsz * 100; i++) {
random_decimal_fill_buffer(static_buf, bufsz, &xorshift_state);
size_t written = write(1, static_buf, bufsz);
(void)written;
//fprintf(stderr, "wrote %#lx of %#lx\n", written, bufsz);
}
}
คอมไพล์ด้วย gcc, clang หรือ ICC (หรือหวังว่าคอมไพเลอร์อื่น ๆ ที่เข้าใจภาษา GNU C ของ C99 และ Intrinsics ของ Intel) ส่วนขยายเวกเตอร์ของ GNU C นั้นสะดวกในการรวบรวมคอมไพเลอร์เพื่อสร้างหมายเลขเวทย์มนตร์สำหรับการหาร / โมดูโลโดยใช้ตัวคูณแบบแยกส่วนแบบแยกส่วนและบางครั้ง__attribute__
มีประโยชน์
สิ่งนี้สามารถเขียนได้แบบพกพา แต่จะใช้รหัสมากกว่านี้
หมายเหตุประสิทธิภาพ:
ร้านค้าที่ทับซ้อนกันเพื่อแทรกบรรทัดใหม่มีค่าใช้จ่ายจำนวนมากในการตัดสินใจว่าจะวางที่ใด (การคาดคะเนสาขาและคอขวดส่วนหน้าบน Core2) แต่ตัวร้านค้าเองไม่มีผลกระทบต่อประสิทธิภาพการทำงาน แสดงความคิดเห็นเพียงคำสั่งที่เก็บไว้ใน asm ของคอมไพเลอร์ (ปล่อยให้สาขาทั้งหมดเหมือนเดิม) ออกจากประสิทธิภาพการทำงานใน Core2 ไม่เปลี่ยนแปลงอย่างสมบูรณ์กับการทำงานซ้ำให้เวลาเดียวกันถึง +/- น้อยกว่า 1% ดังนั้นฉันสรุปได้ว่าบัฟเฟอร์การจัดเก็บ / แคชจัดการได้ดี
ถึงกระนั้นการใช้หน้าต่างหมุนได้บางส่วนascii_digitspace
กับองค์ประกอบหนึ่งที่มีการขึ้นบรรทัดใหม่อาจจะเร็วกว่านี้ถ้าเราเปิดใช้งานมากพอที่เคาน์เตอร์ / สาขาใด ๆ หายไป
การเขียนไปยัง / dev / null นั้นเป็นแบบไม่ใช้ดังนั้นบัฟเฟอร์อาจยังคงร้อนอยู่ในแคช L2 (256kiB ต่อคอร์ใน Haswell) คาดว่าความเร็วเวกเตอร์ที่สมบูรณ์แบบจาก 128b ไปเป็น 256b นั้นไม่มีคำแนะนำเพิ่มเติมและทุกอย่าง (รวมถึงร้านค้า) ก็เกิดขึ้นด้วยความกว้างสองเท่า แม้ว่าสาขาที่แทรกขึ้นบรรทัดใหม่จะได้รับสองครั้งบ่อยครั้ง ฉันโชคไม่ดีที่ฉันไม่ได้มีเวลาในการตั้งค่า Haswell cygwin กับส่วน#ifdef
นั้น
2.5GHz * 32B / 13.7GB / s = 5.84 รอบต่อ AVX2- ร้านค้าบน Haswell ค่อนข้างดี แต่อาจเร็วกว่า อาจจะมีค่าใช้จ่ายในการเรียกระบบ cygwin มากกว่าที่ฉันคิด ฉันไม่ได้ลองแสดงความคิดเห็นเหล่านั้นในเอาต์พุต asm ของคอมไพเลอร์ (ซึ่งจะทำให้แน่ใจได้ว่าไม่มีสิ่งใดที่ดีที่สุดออกมา)
L1 cache สามารถเก็บ 32B หนึ่งที่เก็บต่อนาฬิกาและ L2 ไม่ได้มีแบนด์วิดท์ที่ต่ำกว่ามาก (เวลาแฝงที่สูงขึ้น)
เมื่อฉันดู IACA ไม่กี่รุ่นที่ผ่านมา (ไม่มีการแยกสาขาสำหรับการขึ้นบรรทัดใหม่ แต่รับเพียงหนึ่งเวกเตอร์ ASCII ต่อเวกเตอร์ RNG) มันเป็นการคาดการณ์บางอย่างเช่นร้านขายเวกเตอร์ 32B ต่อ 4 หรือ 5 นาฬิกา
ฉันหวังว่าจะได้รับข้อมูลเพิ่มขึ้นอย่างรวดเร็วจากการดึงข้อมูลเพิ่มเติมจากผลลัพธ์ RNG แต่ละรายการโดยพิจารณาจากตัวฉันเองพิจารณาคำแนะนำของ Agner Fogและแหล่งข้อมูลการเพิ่มประสิทธิภาพอื่น ๆ ซึ่งฉันได้เพิ่มลิงก์ไว้ในแท็ก SO x86 )
ดูเหมือนว่ามันจะเร็วขึ้นอย่างมากใน Skylakeที่จำนวนเต็มเวกเตอร์คูณและการเลื่อนสามารถทำงานบนพอร์ตได้มากเป็นสองเท่า (p0 / p1) เมื่อเทียบกับ Haswell (p0 เท่านั้น) xorshift และตัวแยกหลักใช้ทั้งกะและทวีคูณ ( อัปเดต: Skylake รันที่ 3.02 IPC, ให้เรา 3.77 รอบต่อ 32- ไบต์ AVX2 store , เวลา 0.030 วินาทีต่อการทำซ้ำ 1GB, เขียนลง/dev/null
บน Linux 4.15 บน i7-6700k ที่ 3.9GHz
มันไม่จำเป็นต้องโหมด 64 บิตจะทำงานได้ดี เวอร์ชั่น SSE2 นั้นเร็วมากเมื่อทำการคอมไพล์ด้วย-m32
เนื่องจากมันไม่จำเป็นต้องมีการลงทะเบียนเวกเตอร์จำนวนมากและคณิตศาสตร์ 64 บิตทั้งหมดนั้นทำในเวกเตอร์ไม่ใช่การลงทะเบียนที่ใช้งานทั่วไป
จริง ๆ แล้วมันเร็วขึ้นเล็กน้อยในโหมด 32 บิตบน Core2 เนื่องจากการเปรียบเทียบ / มาโครฟิวชั่นสาขาใช้งานได้ในโหมด 32 บิตเท่านั้นดังนั้นจึงมี uops น้อยลงสำหรับคอร์ที่ล้าสมัย (18.3 วินาที (1.85 คำแนะนำต่อนาฬิกา) vs . 16.9s (2.0 IPC)) ขนาดโค้ดที่เล็กลงจากการไม่มีส่วนนำหน้า REX ช่วยตัวถอดรหัสของ Core2
นอกจากนี้การย้ายเวกเตอร์ reg-reg บางส่วนจะถูกแทนที่ด้วยโหลดเนื่องจากค่าคงที่ทั้งหมดไม่ได้แก้ไขในเวกเตอร์ regs อีกต่อไป เนื่องจากปริมาณงานที่โหลดจากแคช L1 ไม่ใช่ปัญหาคอขวดสิ่งนี้จึงช่วยได้จริง (เช่นการคูณด้วยเวกเตอร์คงที่ของset1(10)
: movdqa xmm0, xmm10
/ pmullw xmm0, xmm1
กลายเป็นmovdqa xmm0, [constant]
/ pmullw xmm0, xmm1
.) เนื่องจาก reg-reg MOVDQA ต้องใช้พอร์ต ALU จึงแข่งขันกับงานจริงที่กำลังทำอยู่ แต่โหลด MOVDQA จะแข่งขันเฉพาะแบนด์วิดท์ถอดรหัสด้านหน้า (การมีที่อยู่ขนาด 4 ไบต์ภายในคำแนะนำจำนวนมากจะยกเลิกการได้รับประโยชน์มากมายจากการบันทึกคำนำหน้า REX
ฉันจะไม่แปลกใจถ้าการบันทึก ALU MOVDQA uops เป็นที่ซึ่งกำไรที่แท้จริงมาจากส่วนหน้าควรจะรักษาด้วย 2.0 IPC เฉลี่ยค่อนข้างดี
ความแตกต่างเหล่านี้ทั้งหมดหายไปใน Haswell ซึ่งสิ่งทั้งหมดควรรันจากแคช decoded-uop หากไม่ใช่บัฟเฟอร์ loopback มาโครฟิวชั่นสาขา ALU + ทำงานได้ทั้งสองโหมดตั้งแต่ Nehalem