The previous pitch was never made into a proposal largely because we were waiting on a safe partially uninitialized buffer type. Now that we have such a type in OutputRawSpan, now might be a good time to consider adding such an API to RandomNumberGenerator.
Also, and this may require a seperate proposal, I think we should extend RandomNumberGenerator to support non-copyable types.
My proposed API is very similar to the previous pitch:
// Initializes the remaining free capacity of the buffer with random bytes.
mutating func initialize(_ buffer: inout OutputRawSpan)
// Fills the entirety of the buffer with random bytes.
mutating func fill(_ buffer: inout MutableRawSpan)
These methods would be required to draw from the same random stream as next() -> UInt64
This would include the obvious implementations for SystemRandomNumberGenerator, as well as less obvious default implementations for use in other RandomNumberGenerators.
public struct SystemRandomNumberGenerator: RandomNumberGenerator {
// ...
public mutating func initialize(_ buffer: inout OutputRawSpan) {
let count = bytes.freeCapacity
buffer.withUnsafeMutableBytes { buffer, initializedCount in
guard let addr = buffer.baseAddress else { return }
swift_stdlib_random(addr + initializedCount, count)
initializedCount &+= count
}
}
mutating func fill(_ buffer: inout MutableRawSpan) {
buffer.withUnsafeMutableBytes { buffer in
guard let addr = buffer.baseAddress else { return }
swift_stdlib_random(addr, buffer.count)
}
}
}
extension RandomNumberGenerator {
// ...
public mutating func initialize(_ buffer: inout OutputRawSpan) {
while buff.freeCapacity >= MemoryLayout<UInt64>.size {
buffer.append(next(), as: UInt64.self)
}
if buffer.isFull { return }
// Many PRNGs are more random in their high bytes, so we'll
// prefer them over the low ones.
withUnsafeBytes(of: next().bigEndian) { block in
for i in 0..<buffer.freeCapacity {
buffer.append(block[i], as: UInt8.self)
}
}
}
public mutating func fill(_ buffer: inout MutableRawSpan) {
let blockSize = MemoryLayout<UInt64>.size
let (count, remainder) = buffer.count.quotientAndRemainder(dividingBy: blockSize)
for i in stride(from: 0, to: count, by: blockSize) {
buffer.storeBytes(of: next(), toByteOffset: i, as: UInt64.self)
}
if remainder == 0 { return }
// Many PRNGs are more random in their high bytes, so we'll
// prefer them over the low ones.
withUnsafeBytes(of: next().bigEndian) { block in
for i in 0..<remainder {
buffer.storeBytes(of: block[i], toByteOffset: count &+ i, as: UInt8.self)
}
}
}
}
Future directions:
swift_stdlib_random isn't neccessarilly efficient for large bulk operations, but I believe it's implementation can be improved separately and without an evolution proposal. The current implementation should at the very least be suitable for use in seeding pseudorandom number generators.
ConvertibleFromRawBytes will allow us to write a safe zero-copy random factory function for arbitrary types.
extension ConvertibleFromRawBytes {
public static func random(using rng: inout RandomNumberGenerator) -> Self {
withUnsafeTemporaryAllocation(of: Self.self, capacity: 1) { buffer in
let bytes = UnsafeMutableRawBufferPointer(buffer)
var span = OutputRawSpan(buffer: bytes, initializedCount: 0)
rng.initialize(&span)
return buffer.moveElement(from: 0)
}
}
public static func random() -> Self {
var rng = SystemRandomNumberGenerator()
return random(using: &rng)
}
}
This can be made even simpler with OutputSpan versions of withTemporaryAllocation
PS. There's probably a very obvious way to extract bytes from next() highest to lowest without using withUnsafeBytes, but I'm having trouble thinking of it.
Alternatives considered:
Only having a method that takes an OutputRawSpan. While a method that doesn't need to track which bytes are initalized is potentially more efficient, one can always convert a MutableRawSpan to an OutputRawSpan if you're willing to launder it through UnsafeMutableRawBufferPointer.