Score:2

วิธีที่มีประสิทธิภาพในการเลือกดัชนีอาร์เรย์โดยใช้ a พูด ตัวเลขสุ่ม 64 บิต

ธง in

พูดว่าฉันมี uint64_t rand = <ตัวเลขสุ่ม>, และ อาร์เรย์ถ่าน [20] = .... เป้าหมายของฉันคือการเลือกองค์ประกอบใน อาร์เรย์ โดยอิงจากเนื้อหาของ แรนด์.

  1. วิธีหนึ่งที่ช้าคือใช้ส่วนที่เหลือ: size_t i = แรนด์ % 20 จากนั้นเลือกองค์ประกอบตาม อาร์เรย์[i].
  2. อีกทางหนึ่งซึ่ง ฉันคิดว่า เร็วกว่าคือ ฉัน = แรนด์/UINT64_MAX * 20. หรือเพื่อหลีกเลี่ยงความจำเป็นในการดำเนินการแบบลอยตัว ส่วนที่ผกผัน 20/(UINT64_MAX/แรนด์).
  3. วิธีที่ 3 คือการใช้บิตสุ่มเพื่อแยกย่อยไปยังดัชนีเหมือนต้นไม้ (แต่พลาดทุกหมายเลขที่ 5):
size_t total_bytes = 20;
size_t มาสก์ = 1;
size_t ฉัน = 0;
ในขณะที่ (total_bytes) {
  ถ้า (แรนด์ & มาสก์) ฉัน += total_bytes / 2; // สาขาขวา
  อื่น ฉัน += 0; //สาขาซ้าย
  หน้ากาก <<= 1;
  total_bytes /= 2;
}

มีวิธีที่เร็วกว่าสำหรับฮาร์ดแวร์ทั่วไปหรือไม่? เช่น. แล็ปท็อป / เดสก์ท็อปพีซี?

เหตุผลที่ฉันสนใจ: ฉันกำลังใช้ฟังก์ชันการสืบทอดคีย์หน่วยความจำ และในบางจุด ฉันต้องเลือกองค์ประกอบอาร์เรย์ตามเนื้อหาของไซเฟอร์เท็กซ์ที่คำนวณได้ จำนวนสุ่มคือ 64 บิต

ภาษาเป้าหมายคือ C

Meir Maor avatar
in flag
คุณได้ตรวจสอบจริง ๆ แล้ว %20 ช้าเกินไปหรือไม่? บนพีซีสมัยใหม่? ฉันคงจะตกใจมาก
Maarten Bodewes avatar
in flag
@มนุษย์ถ้ำ ไม่เป็นไร คำถามแตกต่างจากที่คาดไว้เล็กน้อย ความเห็นยามดึก....
in flag
โพสต์ข้าม: https://stackoverflow.com/questions/68809491/whats-the-fastest-method-in-c-for-converting-a-64bit-random-number-into-a-smallพร้อมรายละเอียดเพิ่มเติมในความคิดเห็น รวมถึงว่า "20" ไม่ใช่ค่าคงที่
Score:4
ธง ng

แรนด์ % 20 สร้างผลลัพธ์ใน $\{0,1,\ldots,18,19\}$ นั่นคือ เกือบ ชุดยูนิฟอร์ม(สมมุติ แรนด์ เป็น): $\Pr(19)/\Pr(0)=1-1/922337203685477581$. นั่นเป็นอคติที่ยอมรับได้

บน "แล็ปท็อป/เดสก์ท็อปพีซี" ที่มี CPU 64 บิตที่ทันสมัย แรนด์ % 20 มีความรวดเร็วพอสมควร และมีจุดเด่นที่สำคัญ คือ ถูกต้อง เรียบง่าย และปรับเปลี่ยนได้ง่าย อย่างไรก็ตาม อย่างน้อยก็บ่อยครั้ง (ดู ความคิดเห็น) เป็นไปได้ที่จะใช้เร็วขึ้น

(แรนด์-((แรนด์-(แรนด์>>2))>>1))>>59

ซึ่งมีอัตราส่วน (ที่เหมาะสมที่สุด) เท่ากันระหว่างผลลัพธ์ที่น้อยที่สุดและเป็นไปได้มากที่สุด ในขณะที่ใช้การดำเนินการกะและเพิ่มเท่านั้น ฉันมั่นใจมากขึ้นว่ารหัสที่สร้างขึ้นนั้นเป็นเวลาคงที่ ซึ่งอาจมีความสำคัญในแอปพลิเคชันการเข้ารหัสลับ และค่าเฉลี่ยจะใกล้เคียงกับ $19/2$.

สำหรับสัญชาตญาณว่าสูตรนั้นทำงานอย่างไร: สำหรับใดๆ $x\in\mathbb R$ มันถือ $(x-(x-x\,2^{-2})\,2^{-1})\,2^{-59}=20\,x\,2^{-64}$ดังนั้นเราจึงจำเป็นต้องประเมินสิ่งที่แสดงออก (uint64_t)ชั้น(แรนด์*(20/(UINT64_MAX+1.))) หรือ (uint64_t)((แรนด์*(uint128_t)20)>>64) พยายามประเมิน โปรดสังเกตว่าสำหรับบางค่ารวมถึง แรนด์=0xCCCCCCCCCCCCCCCC สูตรต่อมาไม่ตรงกับสูตรที่ผมเสนอ แต่การกระจายที่ทำได้โดยทั้งสองนั้นมีความสม่ำเสมออย่างเหมาะสมที่สุด

วิธีการไม่จำกัดเฉพาะค่าคงที่ $m=20$ สำหรับขนาดอาร์เรย์ มันทำให้ทุกคน คงที่ $m$ มีน้ำหนักแฮมมิ่งปานกลาง การคำนวณกะที่เหมาะสมจากค่าคงที่นั้นไม่สำคัญ ฉันอ้างถึงสิ่งนี้ คำตอบที่น่าอัศจรรย์ (หมายเหตุ: จำนวนกะสุดท้ายที่กำหนดจะต้องเพิ่มขึ้น 32 ในกรณีที่มีอยู่) สำหรับบางสิ่งที่ใช้งานได้ แต่ก็ไม่ได้ดีที่สุดเสมอไป ฉันไม่มีข้อมูลอ้างอิงอื่นใดสำหรับวิธีการที่ฉันคิดค้นขึ้นใหม่สำหรับ ARM Cortex-M0 ซึ่งพิสูจน์แล้วว่ามีประโยชน์ จริงๆ แล้วฉันพบสูตรเชิงประจักษ์สำหรับค่าคงที่ไม่กี่ค่าที่เหมาะกับความต้องการของฉันเท่านั้น และ Anders Kaseorg ให้ความสำคัญกับวิธีสร้างสูตรอย่างเป็นระบบ


หากเราเต็มใจที่จะสูญเสียความสม่ำเสมอเล็กน้อยและรับประกันว่ารหัสนั้นเป็นเวลาคงที่ เราสามารถใช้

((แรนด์>>3)*5)>>59

ซึ่งง่ายกว่า เร็วกว่า และง่ายกว่าที่จะปรับให้เข้ากับค่าคงที่อื่นๆ $m$ ค่อนข้างมากกว่า $20$: พวกเราเขียน $m$ เช่น $r\,2^i$ กับ $i$ จำนวนเต็มและ $r$ เลขคี่ดีกว่า แล้วหาจำนวนเต็ม $เจ$ กับ $2^{j-1}\le r<2^j$. เราใช้ ((แรนด์>>j)*r)>>(64+i-j). ปัญหาคือด้านล่าง $เจ$ บิตของ แรนด์ ไม่ได้ใช้ และความสม่ำเสมอของผลลัพธ์จะลดลงตามลำดับ (ยกเว้นในกรณีที่ $m$ เป็นกำลังสอง)

เมื่อไร $m$ เป็น $2^j$ สำหรับจำนวนเต็ม $เจ$, เราสามารถใช้ แรนด์>>(64-j) หรือ แรนด์&(m-1). ภายหลังจะสังเกตเห็นใน คำตอบอื่น ๆ. วิธีการเหล่านี้ไม่สูญเสียความสม่ำเสมอ หากบิตทั้งหมด แรนด์ มีความสม่ำเสมอและเป็นอิสระ

ถ้า $m$ การเปลี่ยนแปลงที่รันไทม์ด้วย $m<2^j$ สำหรับค่าคงที่ที่รู้จัก $เจ$, เราสามารถใช้

((แรนด์>>ญ)*ม)>>(64-ญ)

อย่างไรก็ตาม $เจ$ บิตที่ต่ำกว่าของ แรนด์ จะหายไปและทำให้ความสม่ำเสมอของผลลัพธ์ลดลง (ยกเว้นในกรณีที่ $m$ เป็นกำลังสอง)


นอกหัวข้อ:

  • (uint64_t)(ชั้น(แรนด์*(20/(UINT64_MAX+1.)))) จะไม่เป็นไรหากไม่มีข้อผิดพลาดในการปัดเศษ แต่เนื่องจากสิ่งเหล่านี้มีอยู่จริง จึงยากที่จะบอกได้ว่าสามารถให้ผลได้หรือไม่ 20 สำหรับการป้อนข้อมูลบางอย่าง นอกจากนี้ในคอมไพเลอร์หลายตัวก็ไม่เหมือนกันอย่างเหมาะสม
  • (uint64_t)((แรนด์*(uint128_t)20)>>64) มีความถูกต้องทางคณิตศาสตร์และใกล้เคียงกับที่เราประเมินมาก แต่ uint128_t เป็นคุณลักษณะ C ที่เป็นทางเลือกและยังคงรองรับเล็กน้อย
  • คำถาม แรนด์/UINT64_MAX * 20 เอาท์พุทใน $\{0,20\}$ จึงไม่เหมาะ ปัญหาคือการหารปัดเศษลงเป็นจำนวนเต็ม และ (ไม่ขึ้นกับ) สิ่งนั้น แรนด์ เป็นไปได้ UINT64_MAX.
  • คำถาม 20/(UINT64_MAX/แรนด์) เอาท์พุทใน $\{0,1,2,3,4,5,6,10,20\}$ และทำให้เกิดการหารด้วยศูนย์ได้ จึงไม่เหมาะ ปัญหาคือการหารปัดเศษลงเป็นจำนวนเต็ม และ (ไม่ขึ้นกับ) สิ่งนั้น แรนด์ เป็นไปได้ 0.
  • ส่วนของรหัสคำถาม 3 มีเสมอ ฉัน%5 != 4 บนเอาต์พุตจึงไม่เหมาะ ปัญหาคือผลลัพธ์ ผม ถูกสร้างขึ้นเป็น 10+5+2+1 โดยลบคำศัพท์บางคำออกไป
Gilles 'SO- stop being evil' avatar
เมื่อปรับความเร็วให้เหมาะสมสำหรับ CPU 64 บิตทั่วไป ส่วนที่เหลือหรือการหารด้วยค่าคงที่จะถูกคอมไพล์ด้วยการคูณด้วยค่าคงที่บวกกับการเลื่อนบางส่วนและการบวก/ลบ การแบ่งฮาร์ดแวร์ช้าและคอมไพเลอร์รู้ (แม้ว่าส่วนใหญ่จะไม่ทำการคำนวณเวลาคอมไพล์สำหรับการแบ่ง 64 บิตบน CPU 32 บิต)กะที่คุณเสนอมีจำนวนคำสั่งเท่ากัน แต่ไม่มีการคูณและจำนวนการเข้าถึงหน่วยความจำเท่ากัน ดังนั้นวิธีการเปลี่ยนของคุณจึงน่าจะเร็วกว่าใน CPU ใดๆ ยกเว้นบางอันที่ออกแบบมาสำหรับเรียลไทม์โดยมีจำนวนรอบต่ำ /แผนก https://godbolt.org/z/z4PverffY
fgrieu avatar
ng flag
@Gilles'SO-stopbbingevil' : ฉันไม่พบข้อมูลที่เหมาะสมใน -vol-1-2abcd-3abcd.pdf) เพื่อยืนยันว่าการเพิ่มประสิทธิภาพที่คุณพูดถึงยังคงคุ้มค่ากับซีพียู x64 ล่าสุด อัปเดต: ฉันชี้ไปที่ [เหล่านี้](https://www.agner.org/optimize/#manuals) แหล่งข้อมูลที่มีประโยชน์
Gilles 'SO- stop being evil' avatar
ฉันคิดว่าคุณต้องค้นหาคู่มือเฉพาะรุ่นสำหรับสิ่งนั้น คุณเชื่อมโยงกับการอ้างอิงสถาปัตยกรรมทั่วไป การอ้างอิงชุดคำสั่ง (เล่มที่ 2) จะมีความเกี่ยวข้องมากกว่า แต่ถึงแม้จะเป็นเพียงคำอธิบายการทำงาน แต่ก็ไม่รวมถึงจำนวนรอบ (ซึ่งไม่ได้บอกเรื่องราวประสิทธิภาพทั้งหมด แต่สำหรับกรณีง่ายๆ นี้ ไม่มีการแตกแขนงหรือขนานกัน ดังนั้นฉันคิดว่าการเพิ่มจำนวนรอบจะทำให้เกิดการเปรียบเทียบที่มีความหมาย)
caveman avatar
in flag
จะคุ้มหรือไม่หากจะสรุปว่าการเปลี่ยนโซลูชันเป็นหมายเลขอื่นที่ไม่ใช่ 20 เพื่อให้ได้รอบน้อยกว่าการใช้แนวทาง `%` เพราะ 20 ไม่ใช่ค่าคงที่ แต่เป็นเพียงตัวอย่างที่ผมเลือกมา
fgrieu avatar
ng flag
@caveman: คำตอบชี้แจงว่าใช่ เราสามารถขยายไปยังค่าคงที่อื่นๆ [สิ่งนี้](https://tinyurl.com/unicst) ให้สูตรสำหรับค่าคงที่ทั้งหมดที่มีทศนิยมสูงสุด 3 หลัก (แต่อย่าลืมบวก 32 ให้กับจำนวนกะสุดท้าย) อีกครั้ง การเพิ่มประสิทธิภาพนั้นเหมาะสมก็ต่อเมื่อตัวดำเนินการ `%` ทำงานช้า และจะไม่ใช้กับแล็ปท็อป/เดสก์ท็อปพีซีสมัยใหม่
Gilles 'SO- stop being evil' avatar
@caveman ฉันไม่ใช่ผู้เชี่ยวชาญ แต่ฉันคิดว่าในแง่ของประสิทธิภาพ การคำนวณที่จำเป็นในการคำนวณกะที่จำเป็นจะมีค่าใช้จ่ายมากกว่าหนึ่งคำสั่งหาร อย่างไรก็ตาม แนวทางการเปลี่ยนแปลงมีประโยชน์นอกเหนือจากประสิทธิภาพ โดยส่วนใหญ่รับประกันว่าจะไม่มีกำหนดเวลาที่ขึ้นอยู่กับข้อมูลที่เป็นความลับ
pe flag
ดูเหมือนว่าจะเป็นเวอร์ชันที่ซับซ้อนกว่าของ [Lemire](https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/) `(rand() * 20) >> 64` เข้าใกล้
fgrieu avatar
ng flag
@SamuelNeves: มีความแตกต่าง (A) นิพจน์ `(rand() * 20) >> 64` ต้องการการประเมินผลิตภัณฑ์ใน 69 บิต และนั่นเป็นไปไม่ได้ที่จะพกพาได้ เคล็ดลับของ Lemire ที่เชื่อมโยงคือ `rand()` แบบ 32 บิตขยายเป็น 64 บิต และชนกำแพงนั้นด้วย `rand()` แบบ 64 บิต (B) สำหรับค่าบางค่าของ `rand()` รวมถึง 0xCCCCCCCCCCCCCCCC สิ่งที่ฉันเสนอจะแตกต่างกันไปตามค่าหนึ่ง แต่ก็ยังมีการแจกแจงที่สม่ำเสมอในอุดมคติ
Score:3
ธง in

เพียงแค่ทำ % 20

ตาม http://ithare.com/infographics-operation-costs-in-cpu-clock-cycles/ การแบ่งจำนวนเต็มไม่มีค่าใช้จ่าย 12-44 รอบ cpu บน CPU สมัยใหม่ (และในบางกรณีอาจน้อยกว่าเนื่องจากโครงสร้างไปป์ไลน์หาก ALU ไม่ได้ทำอย่างอื่น) เมื่อพิจารณาถึงสิ่งต่อไปที่คุณต้องการทำคือการเข้าถึงหน่วยความจำที่ดีที่สุดคือการอ่าน L1 จะมีค่าใช้จ่าย 3-4 รอบในตัวเอง และคุณอาจต้องการทำบางสิ่งด้วยค่านี้

ฉันไม่สามารถจินตนาการถึงสถานการณ์ที่สิ่งนี้ควรค่าแก่การปรับให้เหมาะสมแม้ว่าจะเป็นไปได้ที่จะลดสัญญาณนาฬิกาหนึ่งหรือสองขีด

มองหาคอขวดก่อนที่จะปรับให้เหมาะสม

fgrieu avatar
ng flag
[image](http://ithare.com/wp-content/uploads/part101_infographics_v08.png) ในแหล่งข้อมูลที่เป็นประโยชน์ของคุณระบุว่าการหารจำนวนเต็มมีค่าใช้จ่าย 15-40 รอบ ข้อความอ้างถึงการอ้างอิงว่าให้ "ต้นทุนของการแบ่ง 32/64 บิต (รู้จักในชื่อ DIV/IDIV บน x86/64) â ที่ระหว่าง 12-44 รอบ" จากประสบการณ์ของฉันที่ขึ้นอยู่กับแพลตฟอร์มและความกว้างของข้อโต้แย้งอย่างมาก และสัญชาตญาณของฉันคือ 15 หรือ 12 ไม่ได้สะท้อนถึงขอบตกเลือดในปี 2021 สัญชาตญาณเริ่มต้น (ที่ใช้ร่วมกัน) ของเราที่ว่าบน x64 CPU `i%20` นั้นเร็วพอและอาจจะเร็วที่สุดก็ยังสมเหตุสมผล
Meir Maor avatar
in flag
@fgrieu อันที่จริงฉันคัดลอกหมายเลขผิด ฉันแก้ไขหมายเลขแล้ว มันไม่ได้เปลี่ยนบรรทัดล่าง นี้เป็นไปอย่างรวดเร็ว
Gilles 'SO- stop being evil' avatar
ถ้า 20 เป็นค่าคงที่และตัวเลขไม่เกินหนึ่งคำเครื่อง โดยทั่วไป `% 20` จะถูกปรับให้เหมาะสมกับการคูณ ซึ่งใช้เวลาน้อยกว่าการหาร ซึ่งช่วยลดความแตกต่างลงไปอีก ไม่ว่าในกรณีใด ฉันยอมรับว่าแม้การแบ่งจะเล็กน้อยเมื่อเทียบกับการเข้าถึงหน่วยความจำบนแพลตฟอร์มใดๆ ที่มีแคชหน่วยความจำ (โดยเฉพาะอย่างยิ่งหากเป็นการค้นหาตารางเวลาคงที่ซึ่งต้องมีการโหลดจำนวนมาก) อย่างไรก็ตาม สำหรับแอปพลิเคชันเข้ารหัส อาจไม่พึงปรารถนาที่จะใช้การหารหรือการคูณ เนื่องจากเป็นเรื่องปกติที่จะมีเวลาที่ขึ้นกับข้อมูล
Meir Maor avatar
in flag
ตอนแรกฉันให้จำนวนรอบสำหรับการคูณแล้วแก้ไขตามความคิดเห็น การเพิ่มประสิทธิภาพไมโครจริง ๆ เช่นนี้เป็นเรื่องยุ่งยากและขึ้นอยู่กับสิ่งอื่นเพื่อดูว่าซีพียูบรรจุคำสั่งได้ดีเพียงใด แม้ว่าฉันคิดว่าฉันจะไม่ตอบให้ยาวกว่านี้
Score:1
ธง sk

โดยปกติเราจะพยายามทำให้ขนาดอาร์เรย์เป็นกำลัง 2 จากนั้นดัชนีสามารถคำนวณได้โดยใช้บิตและ:

อาร์เรย์ถ่าน [0x40];
uint64_t แรนด์;
...
ถ่าน c = อาร์เรย์ [แรนด์ & 0x3f];
id flag
นั่นเป็นคำตอบ "ฉันสามารถแก้ปัญหาอื่นได้อย่างรวดเร็วจริงๆ" แน่นอน แต่นั่นไม่ใช่คำถามที่ถูกถาม และในคริปโต เมื่ออัลกอริทึมบอกว่าให้ใช้ 20 คุณไม่ต้องแทนที่ 32 เพียงเพราะมันเร็วกว่า การเขียนโปรแกรมแบบนั้นคือวิธีที่คุณทำลาย crypto
ThomasM avatar
sk flag
ตามที่ฉันเข้าใจคำถาม อัลกอริทึมไม่ได้รับ แต่อยู่ระหว่างการปรับปรุง มิฉะนั้นอาจมีวิธีกำหนดวิธีการคำนวณดัชนีจากตัวเลขสุ่ม และไม่สามารถลองวิธีอื่นเพื่อหาวิธีที่เร็วที่สุดได้

โพสต์คำตอบ

คนส่วนใหญ่ไม่เข้าใจว่าการถามคำถามมากมายจะปลดล็อกการเรียนรู้และปรับปรุงความสัมพันธ์ระหว่างบุคคล ตัวอย่างเช่น ในการศึกษาของ Alison แม้ว่าผู้คนจะจำได้อย่างแม่นยำว่ามีคำถามกี่ข้อที่ถูกถามในการสนทนา แต่พวกเขาไม่เข้าใจความเชื่อมโยงระหว่างคำถามและความชอบ จากการศึกษาทั้ง 4 เรื่องที่ผู้เข้าร่วมมีส่วนร่วมในการสนทนาด้วยตนเองหรืออ่านบันทึกการสนทนาของผู้อื่น ผู้คนมักไม่ตระหนักว่าการถามคำถามจะมีอิทธิพลหรือมีอิทธิพลต่อระดับมิตรภาพระหว่างผู้สนทนา