randซึ่งเป็น crate สุ่มหลักของ Rust มีการกระจายโอเปอเรชันที่ใช้บ่อยไว้ตาม trait หลายตัว จึงพัฒนา urandom ขึ้นมาให้มี public/implementation surface ที่เล็กกว่าและประสบการณ์ใช้งานที่สม่ำเสมอกว่า- รวบรวมโอเปอเรชันระดับสูงไว้ในโครงสร้าง
Randomตัวเดียว และซีล traitRngเพื่อให้ความสำคัญกับ การค้นหา API ได้ง่าย และการปรับแต่งภายใน มากกว่าการรองรับตัวสร้างสุ่มแบบใดก็ได้ - โดยไม่ได้นำอัลกอริทึมสุ่มใหม่มาใช้ แต่เลือกฟังก์ชันเอาต์พุตของ Xoshiro256 ตามการใช้งาน ทำให้ในการทดสอบสร้าง
f64จำนวน 1,000 ค่า มี throughput สูงกว่าrand0.10.2 ราว 31% - การสุ่มจำนวนเต็มแบบสม่ำเสมอถูกรวมเส้นทาง reuse/one-shot ด้วย implementation แบบไม่ลำเอียง เพียงชุดเดียวที่คำนวณ threshold แบบ lazy และในการทดสอบช่วง
500..20_000ก็เร็วกว่าสองเส้นทางของrand - เอาต์พุตดิบจาก seed แบบชัดเจนรองรับการทำซ้ำได้ข้ามสถาปัตยกรรมที่รองรับและรีลีสที่เข้ากันได้ตาม SemVer แต่ต้องแลกกับการ เสียความสามารถเชื่อมต่อกับตัวสร้างสุ่มใดก็ได้ และ ecosystem ด้าน distribution/third-party integration ขนาดใหญ่ของ
rand
API Random ที่รวมไว้เป็นหนึ่งเดียว
- โอเปอเรชันที่มีประโยชน์ของ
randกระจายอยู่ตาม trait หลายตัว- การสุ่มค่าในช่วงต้องใช้
RngExt, การเลือกจากลำดับต้องใช้IndexedRandom, การสับต้องใช้SliceRandom rand0.10 มี helper ระดับรากอย่างrand::random_rangeสำหรับการเรียกแบบครั้งเดียว- แต่ถ้าต้องการเก็บ RNG handle หรือใช้โอเปอเรชันกับลำดับอย่างการเลือก/สับ ก็ยังต้องหาเมธอดตาม trait หลายตัวอยู่ดี
- การสุ่มค่าในช่วงต้องใช้
- แม้จะลด import ด้วย prelude ก็ยังต้องรู้ว่า extension method ใช้กับ type ไหนระหว่าง RNG, slice หรือ iterator จึงหาได้ไม่ง่ายจาก IDE autocomplete อย่างเดียว
- urandom วาง consumer API ระดับสูงไว้ใน โครงสร้าง wrapper
Randomตัวเดียว- สร้าง
Random<urandom::rng::Xoshiro256Rng>ได้ด้วยurandom::new() - เรียก
uniform,choose,shuffleได้จากออบเจ็กต์เดียวกัน - ดู
random,uniform,chance,choose,shuffle,sampleและอื่น ๆ ได้จาก autocomplete - ทั้งหมดเป็นเมธอดเฉพาะตัว จึงไม่ต้องหาหรือ import high-level extension trait
- สร้าง
Rng แบบซีลที่เลือกการปรับแต่งแทน extensibility
randมอง low-level RNG trait เป็น public extension point แต่ในurandomนั้น traitRngถูกซีล เพื่อให้เลือกและ implement ตัวสร้างที่รองรับจากภายใน crate- ไม่สามารถนำตัวสร้างสุ่มใด ๆ มาต่อเข้ากับ
Randomได้ - หากจะเพิ่มตัวสร้างใหม่ ต้องแก้
urandomเอง
- ไม่สามารถนำตัวสร้างสุ่มใด ๆ มาต่อเข้ากับ
- หากเป้าหมายคืออัลกอริทึมที่ดีกว่า ตอนนี้ Xoshiro256 และ ChaCha ก็เป็นตัวเลือกตั้งต้นที่ใช้กันอยู่แล้วตามบทบาท และคำแนะนำก็เปลี่ยนช้า
- หากมีตัวเลือกที่ดีกว่า ก็สามารถนำมาใช้ใน major release ถัดไปได้
- หากต้องการให้เข้ากันได้กับโปรเจ็กต์อื่น ภาษาโปรแกรมอื่น อัลกอริทึมแบบ legacy ฮาร์ดแวร์เฉพาะทาง หรือตัวสร้างสำหรับงาน simulation โดยเฉพาะ การใช้ตัวสร้างเดียวกันอย่างเดียวไม่พอ
- ต้องใช้อัลกอริทึมที่เกี่ยวข้องอย่างการสุ่มแบบสม่ำเสมอและการสับให้เหมือนกันด้วย จึงเหมาะกว่าที่จะมี implementation เฉพาะซึ่งทำสัญญาทั้งชุดนั้น
- ด้วย trait แบบซีล จึงสามารถเพิ่มโอเปอเรชันดิบที่
urandomต้องใช้ได้โดยไม่ต้องออกแบบหรือเขียนเอกสารสัญญาการ implement สำหรับตัวสร้างที่ไม่รู้จักและกรณียกเว้นต่าง ๆ- สามารถจับคู่ตัวสร้างกับอัลกอริทึมให้เหมาะกันเพื่อทำ specialization บางอย่างได้ ซึ่ง
randทำไม่ได้
- สามารถจับคู่ตัวสร้างกับอัลกอริทึมให้เหมาะกันเพื่อทำ specialization บางอย่างได้ ซึ่ง
- สำหรับแอปส่วนใหญ่ การเลือก entropy มีประโยชน์กว่าการเพิ่ม implementation PRNG ใหม่
- ตัวสร้างแต่ละตัวเปิดคอนสตรักเตอร์
from_seedแบบเนทีฟไว้ - สร้าง
Randomที่ใช้ seed แบบชัดเจนได้ เช่นChaCha12Rng::from_seed(seed) - แม้จะไม่รับ implementation RNG ใดก็ได้ แต่ก็ยังคง extension point ที่คาดว่าผู้ใช้ขั้นสูงจะต้องการไว้
- ตัวสร้างแต่ละตัวเปิดคอนสตรักเตอร์
ประสิทธิภาพที่ดีขึ้นจากอัลกอริทึมชุดเดิม
urandomไม่ได้ใช้อัลกอริทึมสร้างเลขสุ่มแบบใหม่- สำหรับงานที่ไม่เกี่ยวกับคริปโตบนระบบ 64 บิต
urandom::new()และrand::rngs::SmallRngใช้ตระกูล Xoshiro256 เดียวกัน - สำหรับงานด้านคริปโต
urandom::csprng()และrand::rngs::StdRngใช้ ChaCha12 - ตัวสร้างภายในของ convenience function
rand::rng()ก็เป็น ChaCha12 เช่นกัน
- สำหรับงานที่ไม่เกี่ยวกับคริปโตบนระบบ 64 บิต
- อินเทอร์เฟซของตัวสร้างใน
randให้ทั้ง integer word และการเติมไบต์ ดังนั้น distribution ที่ต้องการf64ก็ยังต้องขอจากu64เต็มคำก่อน urandom::Rngไม่ได้มีแค่next_u32,next_u64แต่ยังมีnext_f32และnext_f64ด้วย- เลขสุ่มแบบ floating-point ต้องใช้บิตสุ่มน้อยกว่า word เต็ม
- ตัวสร้างสามารถ override เมธอดเหล่านี้ด้วยเส้นทางเอาต์พุตที่ต้นทุนต่ำกว่าได้
- implementation ของ Xoshiro แยกเส้นทางเอาต์พุตโดยใช้ state transition ร่วมกัน
- สำหรับ
u64ยังคงใช้ Xoshiro256++ - สำหรับ
u32และ floating-point ใช้ Xoshiro256+ ที่เร็วกว่า โดยบิตบนถูกออกแบบมาให้เหมาะกับงานประเภทนี้
- สำหรับ
- ผลไมโครบेंช์มาร์กที่สร้างเลขสุ่ม 1,000 ค่า ด้วย
urandom1.0 และrand0.10.2 มีดังนี้- Xoshiro
u64: ทั้งสองฝั่ง 814ns - Xoshiro
u32:rand836ns,urandom788ns - Xoshiro
f64:rand1,033ns,urandom788ns - ChaCha12
f64:rand2,199ns,urandom2,011ns
- Xoshiro
- throughput แบบ end-to-end ของ Xoshiro
f64สูงขึ้นราว 31% และใช้เวลารันสั้นลง 24% แต่เส้นทางu64ที่ทำงานเดียวกันแทบไม่ต่างกัน - ChaCha12 ไม่ได้ override
next_f64จึงมีประสิทธิภาพใกล้เคียงกันโดยรวม - เวลาแน่นอนอาจต่างกันตามเครื่องและคอมไพเลอร์ และดูเงื่อนไขเพิ่มเติมได้ใน บันทึกเบนช์มาร์กฉบับเต็ม
เส้นทางการสุ่มแบบสม่ำเสมอที่รวมเป็นหนึ่งเดียว
- หากสุ่มจำนวนเต็มด้วยการหาเศษจากความยาวช่วงตรง ๆ จะเกิด อคติ ดังนั้นการสุ่มจำนวนเต็มแบบสม่ำเสมอที่ถูกต้องต้องปฏิเสธผลลัพธ์บางส่วนจากเอาต์พุตของตัวสร้าง
- การคำนวณ rejection threshold ที่ถูกต้องต้องใช้การหาเศษซึ่งมีต้นทุนสูง
- ถ้าใช้ sampler ซ้ำหลายครั้ง ก็ยอมรับต้นทุนตอนตั้งค่าเริ่มต้นได้
- แต่ถ้าสร้างค่าเดียว ต้นทุนนี้จะสูงเมื่อเทียบกัน
randเปิดเผยความต่างนี้ผ่าน traitUniformSamplerUniformIntที่สร้างไว้จะคำนวณ threshold ล่วงหน้าเพื่อสุ่มแบบไม่ลำเอียงRng::random_rangeใช้ hooksample_singleหรือsample_single_inclusiveแยกต่างหากเพื่อเลี่ยงค่าใช้จ่ายตอนตั้งค่า- ในฟีเจอร์พื้นฐาน เส้นทางลัดแบบครั้งเดียวใช้อัลกอริทึมตัวที่สองที่มีอคติเล็กน้อย
- ฟีเจอร์เสริม
unbiasedจะเปลี่ยนเป็นเวอร์ชันวนซ้ำที่ซับซ้อนกว่า
urandomคำนวณ threshold แบบ lazy และใช้ implementation คูณ-ปฏิเสธแบบไม่ลำเอียงเพียงชุดเดียว ทั้งสำหรับช่วงที่ใช้ซ้ำและช่วงแบบครั้งเดียว- ใช้วิธีที่อธิบายไว้ในงานปี 2018 ของ Daniel Lemire Fast Random Integer Generation in an Interval
- สำหรับช่วงที่ใช้จริงส่วนใหญ่ candidate แรกจะถูกส่งคืนก่อนถึงขั้นหาร
- หาก candidate แรกใช้ไม่ได้ จึงค่อยคำนวณ threshold ที่ถูกต้องแล้ววนซ้ำแบบไม่ลำเอียง
- รองรับกรณียกเว้น
range == 0ที่หมายถึงขอทั้งช่วงเต็มด้วย
- จัดการทั้ง distribution แบบใช้ซ้ำและช่วงแบบครั้งเดียวด้วย implementation เดียวกัน โดยไม่ต้องมีเมธอดแยก อัลกอริทึมตัวที่สอง ค่าใช้จ่ายตั้งต้น หรือ fast path ที่มีอคติ
- ผลเบนช์มาร์กการสุ่ม 1,000 ค่าในช่วง
500..20_000มีดังนี้UniformIntแบบใช้ซ้ำ:rand1,098ns,urandom950ns- ช่วงแบบครั้งเดียว:
rand1,079ns,urandom942ns
- ผลของ
randอ้างอิงจากฟีเจอร์พื้นฐาน ดังนั้นแถว one-shot ที่เร็วกว่าเล็กน้อยยังเป็นเส้นทางที่มีอคติเล็กน้อย ขณะที่urandomเร็วกว่าทั้งสองเส้นทางในสภาพไม่ลำเอียง
การทำซ้ำได้ข้ามรีลีสและสถาปัตยกรรม
urandomถือว่า การทำซ้ำได้ เป็นส่วนหนึ่งของสัญญาสาธารณะ- หากใช้ seed แบบชัดเจนเดียวกันและลำดับการเรียก low-level RNG เหมือนกัน เอาต์พุตดิบของตัวสร้างแบบกำหนดได้จะคงเดิม
- รับประกันความเสถียรข้ามสถาปัตยกรรมที่รองรับและรีลีสที่เข้ากันได้ตาม SemVer
- เซิร์ฟเวอร์ 64 บิตและไคลเอนต์ WebAssembly 32 บิตสามารถใช้ฐานตัวสร้างเดียวกันเพื่อการ replay ได้
- เพื่อรักษาความเข้ากันได้นี้ จึงยอมแลกประสิทธิภาพบนสถาปัตยกรรม 32 บิต
- เป็นการรับประกันที่แรงกว่า นโยบายการทำซ้ำได้ของ
rand- ตัวสร้างและอัลกอริทึมการสุ่มแบบพกพาได้ของ
randอาจให้เอาต์พุตต่างกันใน minor release SmallRngและStdRngก็ไม่ได้พกพาได้โดยชัดแจ้ง และอาจเปลี่ยนไปตามแพลตฟอร์มหรือรีลีสของไลบรารีด้วย
- ตัวสร้างและอัลกอริทึมการสุ่มแบบพกพาได้ของ
ต้นทุนของทางเลือกและเกณฑ์การนำไปใช้
urandomรวบรวมโอเปอเรชันทั่วไปไว้ในRandomเพื่อให้ค้นหาได้ง่ายโดยไม่ต้องพึ่ง extension trait- ออกแบบตัวสร้างและ distribution ร่วมกันเพื่อทำเส้นทางเอาต์พุต Xoshiro ที่เบากว่า และเส้นทางสุ่มแบบสม่ำเสมอที่ไม่ลำเอียงเพียงชุดเดียว
- สตรีมดิบที่เสถียรจากตัวสร้างที่กำหนด seed แบบชัดเจนสามารถใช้กับ เกมและการจำลองแบบกำหนดได้
- แต่ก็ไม่สามารถนำตัวสร้างสุ่มใดก็ได้มาใช้ และไม่มีทั้งรายการ distribution ที่ใหญ่กว่าและ ecosystem การเชื่อมต่อ third-party แบบที่
randมี - หากต้องการ ecosystem ที่กว้าง
randเหมาะกว่า แต่หากชอบ API surface ที่เล็กกว่า การค้นหาที่ง่าย การปรับแต่งแบบบูรณาการ และนโยบายการทำซ้ำได้ที่เข้มแข็งกว่า ก็สามารถเลือกurandomได้ - ดูแพ็กเกจได้ที่ crates.io, เอกสาร API, และ ซอร์สบน GitHub
1 ความคิดเห็น
ความคิดเห็นบน Lobste.rs
มีเหตุผลมากพอที่จะฟอร์ก
randแต่ ชื่อurandomฟังดูเหมือนเป็นไลบรารีที่เกี่ยวข้องกับ/dev/urandomเห็นด้วยกับประเด็นปัญหา แต่ไม่ค่อยชอบ
pub fn new() -> Random<impl Rng + Clone>ถ้าทำให้ทั้งแอปพลิเคชันเป็นพารามิเตอร์ด้วย
Random<T> where T: Rngจะเพิ่มงานจุกจิก และทำให้ปัญหาเรื่อง เวลา compile กับdynหนักขึ้นมาก สู้ให้struct Randomมีชนิดข้อมูลแบบเจาะจงไปเลย หรืออย่างรองก็เลือกstruct Random<T = rng::Xoshiro256Rng>ดีกว่าเพราะความอึดอัดคล้าย ๆ กัน เคยลองทำเองแล้วเหมือนกัน แต่ไม่ใช่ฟอร์ก และ มีฟีเจอร์น้อยกว่า
randมากดีใจที่มีคนที่รู้สึกถึงปัญหาแบบเดียวกับฉันลงมือแก้จริง ๆ ดูเหมือนว่า Rust จะมีแนวโน้มแปลก ๆ ที่ทำให้คนสร้างไลบรารีแบบ trait soup
ชนิดข้อมูลหลักของฐานข้อมูลที่ใช้ในงานต้อง implement trait อย่างน้อย 15 ตัว ทำให้ autocomplete เละเทะและเอกสารก็สับสน แม้จะลดจำนวน trait ลงไปบ้างแล้ว แต่ก็มักติดปัญหา dependency แบบวนรอบ หรือทำให้เขียนเทสต์หลักไม่ได้
ไลบรารีนี้ทำให้นึกถึงทั้ง deep interface ของ APOSD และงานด้าน cryptography ของ Filippo ที่ออกแบบให้ทำพลาดได้ยาก ซึ่งทั้งสองอย่างเป็นคำชมอย่างมาก
urandom::new()ไม่ได้คืนค่า ตัวสร้างเลขสุ่มที่ปลอดภัยเชิง cryptography ดังนั้นจึงยังไม่ใช่การออกแบบที่พลาดไม่ได้จริง ๆ โดยเฉพาะอย่างยิ่ง/dev/urandomบน Linux นั้นปลอดภัย จึงยิ่งทำให้สับสนอีกทางเลือกหนึ่งแทน
randคือfastrandซึ่งเป็นตัวสร้างเลขสุ่มที่เรียบง่ายและเร็ว เรียบง่ายกว่าrandและurandomแต่ก็มีฟีเจอร์น้อยกว่าด้วย