1P by GN⁺ | ★ favorite | 댓글 1개
  • Rust의 대표 난수 크레이트 rand는 일상적인 연산이 여러 특성에 흩어져 있어, 더 작은 공개·구현 표면과 일관된 사용 경험을 갖춘 urandom을 개발함
  • 고수준 연산을 하나의 Random 구조체에 모으고 Rng 특성을 봉인해, 임의 생성기 지원보다 API 발견성과 내부 최적화를 우선함
  • 새로운 난수 알고리듬을 도입하지 않고 Xoshiro256의 출력 함수를 용도별로 선택해, 1,000개 f64 생성 벤치마크에서 rand 0.10.2보다 약 31% 높은 처리량을 기록함
  • 균등 정수 표본 추출은 임계값을 지연 계산하는 하나의 비편향 구현으로 재사용·일회성 경로를 통합했으며, 500..20_000 범위 벤치마크에서 rand의 두 경로보다 빨랐음
  • 명시적 시드의 원시 출력은 지원 아키텍처와 SemVer 호환 릴리스에서 재현성을 보장하지만, 임의 생성기 연결rand의 방대한 분포·서드파티 통합 생태계는 포기함

하나로 모은 Random API

  • rand의 유용한 연산은 여러 특성에 흩어져 있음
    • 난수 범위 생성에는 RngExt, 시퀀스 선택에는 IndexedRandom, 섞기에는 SliceRandom이 필요함
    • rand 0.10은 일회성 호출을 위한 rand::random_range 같은 루트 수준 도우미를 제공함
    • RNG 핸들을 유지하거나 선택·섞기 같은 시퀀스 연산을 사용하려면 여전히 여러 특성의 메서드를 찾아야 함
  • prelude로 가져오기를 줄여도 확장 메서드가 RNG, 슬라이스, 반복자 중 어느 타입에 적용되는지 알아야 하므로 IDE 자동 완성만으로 찾기 어려움
  • urandom은 고수준 소비자 API를 하나의 Random 래퍼 구조체에 배치함
    • urandom::new()Random<urandom::rng::Xoshiro256Rng>를 생성함
    • uniform, choose, shuffle을 같은 객체에서 호출할 수 있음
    • 자동 완성으로 random, uniform, chance, choose, shuffle, sample 등을 확인할 수 있음
    • 모두 고유 메서드이므로 고수준 확장 특성을 찾거나 가져올 필요가 없음

확장성 대신 최적화를 택한 봉인된 Rng

  • rand는 저수준 RNG 특성을 공개 확장 지점으로 취급하지만, urandomRng 특성은 봉인되어 지원 생성기를 크레이트 내부에서 선택하고 구현함
    • 임의 생성기를 Random에 연결할 수 없음
    • 새 생성기를 추가하려면 urandom 자체를 변경해야 함
  • 더 나은 알고리듬이 목적이라면 현재 역할별 기본 선택으로 Xoshiro256과 ChaCha가 이미 자리 잡고 있으며, 권장 사항도 천천히 바뀜
    • 더 나은 선택지가 생기면 향후 메이저 릴리스에서 채택할 수 있음
  • 다른 프로젝트·프로그래밍 언어·레거시 알고리듬·특수 하드웨어·시뮬레이션 전용 생성기와 호환하려면 생성기만 같아서는 부족함
    • 균등 표본 추출과 섞기 등 관련 알고리듬까지 같아야 하므로 전체 계약을 구현한 전용 구현이 더 적합함
  • 봉인된 특성 덕분에 알 수 없는 생성기와 예외 상황을 위한 구현 계약을 설계·문서화하지 않고 urandom에 필요한 원시 연산만 추가할 수 있음
    • 생성기와 알고리듬을 서로 맞춰 특수화할 수 있어 rand에서는 사용할 수 없는 일부 최적화가 가능함
  • 대부분의 애플리케이션에서는 새 PRNG 구현보다 엔트로피 선택이 더 유용함
    • 구체적인 생성기는 네이티브 from_seed 생성자를 공개함
    • ChaCha12Rng::from_seed(seed)처럼 명시적 시드를 사용한 Random을 만들 수 있음
    • 임의 RNG 구현은 받지 않지만 고급 사용자가 필요로 할 것으로 예상되는 확장 지점은 유지함

같은 알고리듬에서 얻은 성능 향상

  • urandom은 새로운 난수 생성 알고리듬을 사용하지 않음
    • 64비트 시스템에서 비암호학적 용도의 urandom::new()rand::rngs::SmallRng은 같은 Xoshiro256 계열을 사용함
    • 암호학적 용도의 urandom::csprng()rand::rngs::StdRngChaCha12를 사용함
    • 편의 함수 rand::rng()의 내부 생성기도 ChaCha12임
  • rand의 생성기 인터페이스는 정수 워드와 바이트 채우기를 제공하므로 f64가 필요한 분포도 전체 u64부터 요청함
  • urandom::Rngnext_u32, next_u64뿐 아니라 next_f32next_f64 도 제공함
    • 부동소수점 난수는 전체 워드보다 적은 난수 비트가 필요함
    • 생성기는 이 메서드들을 더 저렴한 출력 함수로 재정의할 수 있음
  • Xoshiro 구현은 상태 전이를 공유하면서 출력 경로를 구분함
    • u64에는 Xoshiro256++를 유지함
    • u32와 부동소수점에는 상위 비트가 해당 용도에 맞게 설계된 더 빠른 Xoshiro256+를 사용함
  • urandom 1.0과 rand 0.10.2로 각각 1,000개의 난수를 생성한 마이크로벤치마크 결과는 다음과 같음
    • Xoshiro u64: 양쪽 모두 814ns
    • Xoshiro u32: rand 836ns, urandom 788ns
    • Xoshiro f64: rand 1,033ns, urandom 788ns
    • ChaCha12 f64: rand 2,199ns, urandom 2,011ns
  • Xoshiro f64의 종단 간 처리량은 약 31% 높고 실행 시간은 24% 짧았지만, 동일한 작업을 수행하는 u64 경로는 사실상 동률이었음
  • ChaCha12는 next_f64를 재정의하지 않아 성능이 대체로 비슷함
  • 정확한 시간은 머신과 컴파일러에 따라 달라지며, 자세한 조건은 전체 벤치마크 노트에서 확인할 수 있음

하나로 통합한 균등 표본 추출 경로

  • 정수를 범위 길이로 단순 나머지 연산하면 편향이 생기므로, 올바른 균등 정수 표본 추출은 생성기 출력 일부를 거부해야 함
  • 정확한 거부 임계값 계산에는 비용이 큰 나머지 연산이 필요함
    • 표본 추출기를 반복 사용한다면 초기 설정 비용으로 감당할 수 있음
    • 값 하나만 생성할 때는 이 비용이 상대적으로 커짐
  • rand는 이러한 차이를 UniformSampler 특성으로 노출함
    • 생성된 UniformInt는 임계값을 미리 계산해 편향 없이 표본을 추출함
    • Rng::random_range는 초기 설정을 피하려고 별도의 sample_single 또는 sample_single_inclusive 훅을 사용함
    • 기본 기능에서는 일회성 단축 경로가 조금 편향된 두 번째 알고리듬을 사용함
    • 선택적 unbiased 기능은 이를 더 복잡한 반복 버전으로 교체함
  • urandom은 임계값을 지연 계산해 재사용 및 일회성 범위 모두에 하나의 비편향 곱셈·거부 구현을 사용함
    • Daniel Lemire의 2018년 논문 Fast Random Integer Generation in an Interval에 기술된 방식을 따름
    • 대부분의 실용적 범위에서는 첫 후보가 나눗셈 전에 반환됨
    • 첫 후보를 반환할 수 없으면 정확한 임계값을 계산한 뒤 편향 없이 반복함
    • 전체 범위가 요청되는 range == 0 예외도 처리함
  • 별도 메서드나 두 번째 알고리듬, 선행 설정 비용, 편향된 고속 경로 없이 동일한 구현으로 재사용 분포와 일회성 범위를 처리함
  • 500..20_000 범위에서 1,000개를 추출한 벤치마크 결과는 다음과 같음
    • 재사용 UniformInt: rand 1,098ns, urandom 950ns
    • 일회성 범위: rand 1,079ns, urandom 942ns
  • rand 결과는 기본 기능 기준이므로 더 빠른 일회성 행도 약간 편향된 경로인 반면, urandom은 비편향 상태로 두 경로보다 빨랐음

릴리스와 아키텍처를 아우르는 재현성

  • urandom재현성을 공개 계약의 일부로 취급함
    • 같은 명시적 시드와 같은 저수준 RNG 호출 순서가 주어지면 결정론적 생성기의 원시 출력이 유지됨
    • 지원 아키텍처와 SemVer 호환 릴리스 전체에서 안정성을 보장함
    • 64비트 서버와 32비트 WebAssembly 클라이언트가 재생을 위한 동일한 생성기 기반을 사용할 수 있음
  • 이 호환성을 유지하기 위해 32비트 아키텍처에서는 성능을 희생함
  • rand의 재현성 정책보다 강한 보장임
    • rand의 이식 가능한 생성기와 표본 추출 알고리듬은 마이너 릴리스에서 출력이 달라질 수 있음
    • SmallRngStdRng은 명시적으로 이식 가능하지 않으며 플랫폼이나 라이브러리 릴리스에 따라서도 바뀔 수 있음

선택의 대가와 적용 기준

  • urandom은 일반 연산을 Random에 모아 확장 특성 없이 쉽게 찾을 수 있게 함
  • 생성기와 분포를 함께 설계해 더 저렴한 Xoshiro 출력 경로와 하나의 비편향 균등 표본 추출 경로를 구현함
  • 명시적으로 시드가 지정된 생성기의 안정적인 원시 스트림은 결정론적 게임과 시뮬레이션에 활용할 수 있음
  • 대신 임의 생성기를 가져올 수 없으며, rand가 제공하는 더 큰 분포 목록과 서드파티 통합 생태계도 갖추지 못함
  • 광범위한 생태계가 필요하면 rand가 적합하고, 작은 API 표면·발견성·통합된 최적화·강한 재현성 정책을 선호하면 urandom을 선택할 수 있음
  • 패키지는 crates.io, API 문서, GitHub 소스에서 확인할 수 있음

댓글과 토론

Lobste.rs 의견들
  • rand를 포크할 이유는 충분하지만, urandom이라는 이름/dev/urandom과 관련된 라이브러리처럼 들림

    • 유용해 보이지만 이름이 혼동을 부를 수 있음. 글을 읽지 않고 이름만 봤다면 파일 입출력에 의존한다고 생각해 살펴보지 않았을 것 같음
  • 문제의식에는 동의하지만 pub fn new() -> Random<impl Rng + Clone>은 마음에 들지 않음
    애플리케이션 전체를 Random<T> where T: Rng으로 매개변수화하면 번거로운 작업이 늘고, 컴파일 시간dyn 관련 문제가 심각해짐. 차라리 struct Random이 구체 타입을 갖게 하거나, 차선책으로 struct Random<T = rng::Xoshiro256Rng>을 택하겠음

  • 비슷한 답답함 때문에 이미 직접 만들어봤지만 포크는 아니며, rand보다 기능이 훨씬 적음

  • 나와 같은 문제를 느낀 누군가가 실제로 해결에 나서서 반가움. Rust에는 이상하게 트레이트 수프 라이브러리를 만들게 하는 경향이 있는 듯함
    업무에서 다루는 데이터베이스의 핵심 자료형은 최소 15개 트레이트를 구현해야 해서 자동완성이 엉망이고 문서도 혼란스러움. 트레이트 수를 일부 줄였지만 순환 의존성이나 핵심 테스트를 작성할 수 없게 되는 문제에 자주 막힘

    • Java 출신의 아키텍처 우주비행사들이 같은 객체지향 스타일을 Rust에 적용해서 생기는 현상임. 순환 의존성은 아직 분리할 수 없는 하나의 대상을 억지로 나눴거나, 세 대상을 제대로 구분하지 못했다는 신호임. 코드를 모두 통제할 수 있다면 트레이트 대신 열거형을 쓰면 됨
    • Rust 암호학 생태계에서는 트레이트 수프 문제가 유독 심해서 미칠 지경임
  • 이 라이브러리는 APOSD의 깊은 인터페이스와 Filippo의 실수하기 어렵게 설계한 암호학 작업을 모두 떠올리게 하며, 둘 다 큰 찬사임

    • 다만 urandom::new()암호학적으로 안전한 난수 생성기를 반환하지 않으므로 완전히 실수 불가능한 설계는 아님. 특히 Linux의 /dev/urandom은 안전하기 때문에 더 헷갈림
  • 또 다른 rand 대안으로 단순하고 빠른 난수 생성기인 fastrand 가 있음. randurandom보다 단순하지만 기능도 더 적음