在Swift中实现xoshiro256+ PRNG并生成指定范围随机数的问题
Hey there! Let's tackle the issues you're facing with your xoshiro256+ PRNG implementation and get it working smoothly.
1. The Root Cause of Your Crash: Unsigned Overflow
Your crash after 6 calls is almost certainly due to unsigned integer overflow. In C, unsigned integer overflow is well-defined (it wraps around modulo 2^64), but Swift defaults to trapping on overflow for arithmetic operations.
In your next() function, the line:
let result_plus = s[0] + s[3]
will trigger a runtime error when s[0] + s[3] exceeds UInt64.max. To match the C behavior, you need to use Swift's overflow addition operator &+:
let result_plus = s[0] &+ s[3]
This lets the value wrap around as intended by the xoshiro256+ algorithm.
2. Converting to a 0-1 Double
To convert the 64-bit unsigned output to a Double in the [0, 1) range, you have two solid options:
Option A: Direct Conversion (Simpler)
Treat the UInt64 result as a value between 0 and 2^64 - 1, then divide by 2^64. Since UInt64.max is 2^64 - 1, we add 1.0 to get the full range:
func nextDouble() -> Double { let raw = next() return Double(raw) / (Double(UInt64.max) + 1.0) }
Option B: Precision-Optimized Conversion (Better for Double)
Double only has 53 bits of mantissa precision, so using all 64 bits of the PRNG output is wasteful. We can shift the result right by 11 bits to align with Double's precision, then divide by 2^53:
func nextDouble() -> Double { let raw = next() let shifted = raw >> 11 // Keep the top 53 bits return Double(shifted) / Double(1 << 53) }
This gives you the maximum possible precision for a Double, which is ideal for most use cases.
3. Generating Random Numbers in a Custom Range
Once you have your nextDouble() function, generating numbers in a range [min, max) is straightforward:
func next(in range: ClosedRange<Double>) -> Double { let min = range.lowerBound let max = range.upperBound return min + (max - min) * nextDouble() } // Or for integer ranges (if needed): func next(in range: ClosedRange<Int>) -> Int { let min = range.lowerBound let max = range.upperBound let doubleValue = Double(min) + (Double(max - min + 1) * nextDouble()) return Int(doubleValue) }
4. Full Working Implementation
Here's the updated code with all fixes and additions:
func rotl(_ x: UInt64, _ k: Int) -> UInt64 { return (x << k) | (x >> (64 - k)) } class Xoshiro256Plus { private var s: [UInt64] // Initialize with a seed vector (replace with proper seeding in production!) init(seed: [UInt64]) { precondition(seed.count == 4, "Seed must be a 4-element UInt64 array") self.s = seed } func next() -> UInt64 { let result_plus = s[0] &+ s[3] // Use overflow addition let t = s[1] << 17 s[2] ^= s[0] s[3] ^= s[1] s[1] ^= s[2] s[0] ^= s[3] s[2] ^= t s[3] = rotl(s[3], 45) return result_plus } func nextDouble() -> Double { let raw = next() let shifted = raw >> 11 return Double(shifted) / Double(1 << 53) } func next(in range: ClosedRange<Double>) -> Double { let min = range.lowerBound let max = range.upperBound return min + (max - min) * nextDouble() } } // Usage example: let prng = Xoshiro256Plus(seed: [1, 2, 3, 4]) for _ in 0..<10 { print(prng.nextDouble()) print(prng.next(in: 5.0...10.0)) }
Note: I wrapped the PRNG in a class to encapsulate the state vector s—this is cleaner than using a global variable, especially if you need multiple independent PRNG instances.
5. Quick Notes
- Seeding: Your initial seed
[1,2,3,4]works for testing, but in production, you should use a cryptographically secure seed (e.g., from platform-specific APIs likeSecRandomCopyByteson Apple devices) to avoid predictable outputs. - No Need to Discard Low Bits: The overflow fix is all you need to stop the crash. The precision optimization (shifting right) is optional but recommended for Double conversion.
内容的提问来源于stack exchange,提问作者ANoobSwiftly

