authorgravatar for thejoshwolfe@gmail.comJosh Wolfe <thejoshwolfe@gmail.com> 2018-09-27 00:35:38-04:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2018-09-27 00:35:38-04:00
loge7d9d00ac8c2bb075d0f67842b58718aececdc09
treea9debecbf1bcfeca594c7f69557697a1e76c5c1e
parent1c26c2f4d5c9029fe39cc413f75e547636e42a14

overhaul api for getting random integers (#1578)

* rand api overhaul * no retry limits. instead documented a recommendation to call int(T) % len directly.

1 files changed, 267 insertions(+), 66 deletions(-)

std/rand/index.zig+267-66
...@@ -5,11 +5,11 @@...@@ -5,11 +5,11 @@
5// ```5// ```
6// var buf: [8]u8 = undefined;6// var buf: [8]u8 = undefined;
7// try std.os.getRandomBytes(buf[0..]);7// try std.os.getRandomBytes(buf[0..]);
8// const seed = mem.readInt(buf[0..8], u64, builtin.Endian.Little);8// const seed = mem.readIntLE(u64, buf[0..8]);
9//9//
10// var r = DefaultPrng.init(seed);10// var r = DefaultPrng.init(seed);
11//11//
12// const s = r.random.scalar(u64);12// const s = r.random.int(u64);
13// ```13// ```
14//14//
15// TODO(tiehuis): Benchmark these against other reference implementations.15// TODO(tiehuis): Benchmark these against other reference implementations.
...@@ -35,60 +35,117 @@ pub const Random = struct {...@@ -35,60 +35,117 @@ pub const Random = struct {
35 r.fillFn(r, buf);35 r.fillFn(r, buf);
36 }36 }
3737
38 /// Return a random integer/boolean type.38 pub fn boolean(r: *Random) bool {
39 pub fn scalar(r: *Random, comptime T: type) T {39 return r.int(u1) != 0;
40 var rand_bytes: [@sizeOf(T)]u8 = undefined;40 }
41
42 /// Returns a random int `i` such that `0 <= i <= @maxValue(T)`.
43 /// `i` is evenly distributed.
44 pub fn int(r: *Random, comptime T: type) T {
45 const UnsignedT = @IntType(false, T.bit_count);
46 const ByteAlignedT = @IntType(false, @divTrunc(T.bit_count + 7, 8) * 8);
47
48 var rand_bytes: [@sizeOf(ByteAlignedT)]u8 = undefined;
41 r.bytes(rand_bytes[0..]);49 r.bytes(rand_bytes[0..]);
4250
43 if (T == bool) {51 // use LE instead of native endian for better portability maybe?
44 return rand_bytes[0] & 0b1 == 0;52 // TODO: endian portability is pointless if the underlying prng isn't endian portable.
53 // TODO: document the endian portability of this library.
54 const byte_aligned_result = mem.readIntLE(ByteAlignedT, rand_bytes);
55 const unsigned_result = @truncate(UnsignedT, byte_aligned_result);
56 return @bitCast(T, unsigned_result);
57 }
58
59 /// Returns an evenly distributed random unsigned integer `0 <= i < less_than`.
60 /// This function assumes that the underlying ::fillFn produces evenly distributed values.
61 /// Within this assumption, the runtime of this function is exponentially distributed.
62 /// If ::fillFn were backed by a true random generator,
63 /// the runtime of this function would technically be unbounded.
64 /// However, if ::fillFn is backed by any evenly distributed pseudo random number generator,
65 /// this function is guaranteed to return.
66 /// If you need deterministic runtime bounds, consider instead using `r.int(T) % less_than`,
67 /// which will usually be biased toward smaller values.
68 pub fn uintLessThan(r: *Random, comptime T: type, less_than: T) T {
69 assert(T.is_signed == false);
70 assert(0 < less_than);
71
72 const last_group_size_minus_one: T = @maxValue(T) % less_than;
73 if (last_group_size_minus_one == less_than - 1) {
74 // less_than is a power of two.
75 assert(math.floorPowerOfTwo(T, less_than) == less_than);
76 // There is no retry zone. The optimal retry_zone_start would be @maxValue(T) + 1.
77 return r.int(T) % less_than;
78 }
79 const retry_zone_start = @maxValue(T) - last_group_size_minus_one;
80
81 while (true) {
82 const rand_val = r.int(T);
83 if (rand_val < retry_zone_start) {
84 return rand_val % less_than;
85 }
86 }
87 }
88
89 /// Returns an evenly distributed random unsigned integer `0 <= i <= at_most`.
90 /// See ::uintLessThan, which this function uses in most cases,
91 /// for commentary on the runtime of this function.
92 pub fn uintAtMost(r: *Random, comptime T: type, at_most: T) T {
93 assert(T.is_signed == false);
94 if (at_most == @maxValue(T)) {
95 // have the full range
96 return r.int(T);
97 }
98 return r.uintLessThan(T, at_most + 1);
99 }
100
101 /// Returns an evenly distributed random integer `at_least <= i < less_than`.
102 /// See ::uintLessThan, which this function uses in most cases,
103 /// for commentary on the runtime of this function.
104 pub fn intRangeLessThan(r: *Random, comptime T: type, at_least: T, less_than: T) T {
105 assert(at_least < less_than);
106 if (T.is_signed) {
107 // Two's complement makes this math pretty easy.
108 const UnsignedT = @IntType(false, T.bit_count);
109 const lo = @bitCast(UnsignedT, at_least);
110 const hi = @bitCast(UnsignedT, less_than);
111 const result = lo +% r.uintLessThan(UnsignedT, hi -% lo);
112 return @bitCast(T, result);
113 } else {
114 // The signed implementation would work fine, but we can use stricter arithmetic operators here.
115 return at_least + r.uintLessThan(T, less_than - at_least);
116 }
117 }
118
119 /// Returns an evenly distributed random integer `at_least <= i <= at_most`.
120 /// See ::uintLessThan, which this function uses in most cases,
121 /// for commentary on the runtime of this function.
122 pub fn intRangeAtMost(r: *Random, comptime T: type, at_least: T, at_most: T) T {
123 assert(at_least <= at_most);
124 if (T.is_signed) {
125 // Two's complement makes this math pretty easy.
126 const UnsignedT = @IntType(false, T.bit_count);
127 const lo = @bitCast(UnsignedT, at_least);
128 const hi = @bitCast(UnsignedT, at_most);
129 const result = lo +% r.uintAtMost(UnsignedT, hi -% lo);
130 return @bitCast(T, result);
45 } else {131 } else {
46 // NOTE: Cannot @bitCast array to integer type.132 // The signed implementation would work fine, but we can use stricter arithmetic operators here.
47 return mem.readInt(rand_bytes, T, builtin.Endian.Little);133 return at_least + r.uintAtMost(T, at_most - at_least);
48 }134 }
49 }135 }
50136
137 /// Return a random integer/boolean type.
138 /// TODO: deprecated. use ::boolean or ::int instead.
139 pub fn scalar(r: *Random, comptime T: type) T {
140 if (T == bool) return r.boolean();
141 return r.int(T);
142 }
143
51 /// Return a random integer with even distribution between `start`144 /// Return a random integer with even distribution between `start`
52 /// inclusive and `end` exclusive. `start` must be less than `end`.145 /// inclusive and `end` exclusive. `start` must be less than `end`.
146 /// TODO: deprecated. renamed to ::intRangeLessThan
53 pub fn range(r: *Random, comptime T: type, start: T, end: T) T {147 pub fn range(r: *Random, comptime T: type, start: T, end: T) T {
54 assert(start < end);148 return r.intRangeLessThan(T, start, end);
55 if (T.is_signed) {
56 const uint = @IntType(false, T.bit_count);
57 if (start >= 0 and end >= 0) {
58 return @intCast(T, r.range(uint, @intCast(uint, start), @intCast(uint, end)));
59 } else if (start < 0 and end < 0) {
60 // Can't overflow because the range is over signed ints
61 return math.negateCast(r.range(uint, math.absCast(end), math.absCast(start)) + 1) catch unreachable;
62 } else if (start < 0 and end >= 0) {
63 const end_uint = @intCast(uint, end);
64 const total_range = math.absCast(start) + end_uint;
65 const value = r.range(uint, 0, total_range);
66 const result = if (value < end_uint) x: {
67 break :x @intCast(T, value);
68 } else if (value == end_uint) x: {
69 break :x start;
70 } else x: {
71 // Can't overflow because the range is over signed ints
72 break :x math.negateCast(value - end_uint) catch unreachable;
73 };
74 return result;
75 } else {
76 unreachable;
77 }
78 } else {
79 const total_range = end - start;
80 const leftover = @maxValue(T) % total_range;
81 const upper_bound = @maxValue(T) - leftover;
82 var rand_val_array: [@sizeOf(T)]u8 = undefined;
83
84 while (true) {
85 r.bytes(rand_val_array[0..]);
86 const rand_val = mem.readInt(rand_val_array, T, builtin.Endian.Little);
87 if (rand_val < upper_bound) {
88 return start + (rand_val % total_range);
89 }
90 }
91 }
92 }149 }
93150
94 /// Return a floating point value evenly distributed in the range [0, 1).151 /// Return a floating point value evenly distributed in the range [0, 1).
...@@ -97,12 +154,12 @@ pub const Random = struct {...@@ -97,12 +154,12 @@ pub const Random = struct {
97 // Note: The lowest mantissa bit is always set to 0 so we only use half the available range.154 // Note: The lowest mantissa bit is always set to 0 so we only use half the available range.
98 switch (T) {155 switch (T) {
99 f32 => {156 f32 => {
100 const s = r.scalar(u32);157 const s = r.int(u32);
101 const repr = (0x7f << 23) | (s >> 9);158 const repr = (0x7f << 23) | (s >> 9);
102 return @bitCast(f32, repr) - 1.0;159 return @bitCast(f32, repr) - 1.0;
103 },160 },
104 f64 => {161 f64 => {
105 const s = r.scalar(u64);162 const s = r.int(u64);
106 const repr = (0x3ff << 52) | (s >> 12);163 const repr = (0x3ff << 52) | (s >> 12);
107 return @bitCast(f64, repr) - 1.0;164 return @bitCast(f64, repr) - 1.0;
108 },165 },
...@@ -142,12 +199,167 @@ pub const Random = struct {...@@ -142,12 +199,167 @@ pub const Random = struct {
142199
143 var i: usize = 0;200 var i: usize = 0;
144 while (i < buf.len - 1) : (i += 1) {201 while (i < buf.len - 1) : (i += 1) {
145 const j = r.range(usize, i, buf.len);202 const j = r.intRangeLessThan(usize, i, buf.len);
146 mem.swap(T, &buf[i], &buf[j]);203 mem.swap(T, &buf[i], &buf[j]);
147 }204 }
148 }205 }
149};206};
150207
208const SequentialPrng = struct {
209 const Self = @This();
210 random: Random,
211 next_value: u8,
212
213 pub fn init() Self {
214 return Self{
215 .random = Random{ .fillFn = fill },
216 .next_value = 0,
217 };
218 }
219
220 fn fill(r: *Random, buf: []u8) void {
221 const self = @fieldParentPtr(Self, "random", r);
222 for (buf) |*b| {
223 b.* = self.next_value;
224 }
225 self.next_value +%= 1;
226 }
227};
228
229test "Random int" {
230 testRandomInt();
231 comptime testRandomInt();
232}
233fn testRandomInt() void {
234 var r = SequentialPrng.init();
235
236 assert(r.random.int(u0) == 0);
237
238 r.next_value = 0;
239 assert(r.random.int(u1) == 0);
240 assert(r.random.int(u1) == 1);
241 assert(r.random.int(u2) == 2);
242 assert(r.random.int(u2) == 3);
243 assert(r.random.int(u2) == 0);
244
245 r.next_value = 0xff;
246 assert(r.random.int(u8) == 0xff);
247 r.next_value = 0x11;
248 assert(r.random.int(u8) == 0x11);
249
250 r.next_value = 0xff;
251 assert(r.random.int(u32) == 0xffffffff);
252 r.next_value = 0x11;
253 assert(r.random.int(u32) == 0x11111111);
254
255 r.next_value = 0xff;
256 assert(r.random.int(i32) == -1);
257 r.next_value = 0x11;
258 assert(r.random.int(i32) == 0x11111111);
259
260 r.next_value = 0xff;
261 assert(r.random.int(i8) == -1);
262 r.next_value = 0x11;
263 assert(r.random.int(i8) == 0x11);
264
265 r.next_value = 0xff;
266 assert(r.random.int(u33) == 0x1ffffffff);
267 r.next_value = 0xff;
268 assert(r.random.int(i1) == -1);
269 r.next_value = 0xff;
270 assert(r.random.int(i2) == -1);
271 r.next_value = 0xff;
272 assert(r.random.int(i33) == -1);
273}
274
275test "Random boolean" {
276 testRandomBoolean();
277 comptime testRandomBoolean();
278}
279fn testRandomBoolean() void {
280 var r = SequentialPrng.init();
281 assert(r.random.boolean() == false);
282 assert(r.random.boolean() == true);
283 assert(r.random.boolean() == false);
284 assert(r.random.boolean() == true);
285}
286
287test "Random intLessThan" {
288 @setEvalBranchQuota(10000);
289 testRandomIntLessThan();
290 comptime testRandomIntLessThan();
291}
292fn testRandomIntLessThan() void {
293 var r = SequentialPrng.init();
294 r.next_value = 0xff;
295 assert(r.random.uintLessThan(u8, 4) == 3);
296 r.next_value = 0xff;
297 assert(r.random.uintLessThan(u8, 3) == 0);
298 assert(r.next_value == 1);
299
300 r.next_value = 0xff;
301 assert(r.random.intRangeLessThan(u8, 0, 0x80) == 0x7f);
302 r.next_value = 0xff;
303 assert(r.random.intRangeLessThan(u8, 0x7f, 0xff) == 0xfe);
304
305 r.next_value = 0xff;
306 assert(r.random.intRangeLessThan(i8, 0, 0x40) == 0x3f);
307 r.next_value = 0xff;
308 assert(r.random.intRangeLessThan(i8, -0x40, 0x40) == 0x3f);
309 r.next_value = 0xff;
310 assert(r.random.intRangeLessThan(i8, -0x80, 0) == -1);
311
312 r.next_value = 0xff;
313 assert(r.random.intRangeLessThan(i64, -0x8000000000000000, 0) == -1);
314 r.next_value = 0xff;
315 assert(r.random.intRangeLessThan(i3, -4, 0) == -1);
316 r.next_value = 0xff;
317 assert(r.random.intRangeLessThan(i3, -2, 2) == 1);
318
319 // test retrying and eventually getting a good value
320 // start just out of bounds
321 r.next_value = 0x81;
322 assert(r.random.uintLessThan(u8, 0x81) == 0);
323}
324
325test "Random intAtMost" {
326 @setEvalBranchQuota(10000);
327 testRandomIntAtMost();
328 comptime testRandomIntAtMost();
329}
330fn testRandomIntAtMost() void {
331 var r = SequentialPrng.init();
332 r.next_value = 0xff;
333 assert(r.random.uintAtMost(u8, 3) == 3);
334 r.next_value = 0xff;
335 assert(r.random.uintAtMost(u8, 2) == 0);
336 assert(r.next_value == 1);
337
338 r.next_value = 0xff;
339 assert(r.random.intRangeAtMost(u8, 0, 0x7f) == 0x7f);
340 r.next_value = 0xff;
341 assert(r.random.intRangeAtMost(u8, 0x7f, 0xfe) == 0xfe);
342
343 r.next_value = 0xff;
344 assert(r.random.intRangeAtMost(i8, 0, 0x3f) == 0x3f);
345 r.next_value = 0xff;
346 assert(r.random.intRangeAtMost(i8, -0x40, 0x3f) == 0x3f);
347 r.next_value = 0xff;
348 assert(r.random.intRangeAtMost(i8, -0x80, -1) == -1);
349
350 r.next_value = 0xff;
351 assert(r.random.intRangeAtMost(i64, -0x8000000000000000, -1) == -1);
352 r.next_value = 0xff;
353 assert(r.random.intRangeAtMost(i3, -4, -1) == -1);
354 r.next_value = 0xff;
355 assert(r.random.intRangeAtMost(i3, -2, 1) == 1);
356
357 // test retrying and eventually getting a good value
358 // start just out of bounds
359 r.next_value = 0x81;
360 assert(r.random.uintAtMost(u8, 0x80) == 0);
361}
362
151// Generator to extend 64-bit seed values into longer sequences.363// Generator to extend 64-bit seed values into longer sequences.
152//364//
153// The number of cycles is thus limited to 64-bits regardless of the engine, but this365// The number of cycles is thus limited to 64-bits regardless of the engine, but this
...@@ -622,17 +834,6 @@ test "Random float" {...@@ -622,17 +834,6 @@ test "Random float" {
622 }834 }
623}835}
624836
625test "Random scalar" {
626 var prng = DefaultPrng.init(0);
627 const s = prng.random.scalar(u64);
628}
629
630test "Random bytes" {
631 var prng = DefaultPrng.init(0);
632 var buf: [2048]u8 = undefined;
633 prng.random.bytes(buf[0..]);
634}
635
636test "Random shuffle" {837test "Random shuffle" {
637 var prng = DefaultPrng.init(0);838 var prng = DefaultPrng.init(0);
638839
...@@ -664,16 +865,16 @@ test "Random range" {...@@ -664,16 +865,16 @@ test "Random range" {
664 testRange(&prng.random, -4, 3);865 testRange(&prng.random, -4, 3);
665 testRange(&prng.random, -4, -1);866 testRange(&prng.random, -4, -1);
666 testRange(&prng.random, 10, 14);867 testRange(&prng.random, 10, 14);
667 // TODO: test that prng.random.range(1, 1) causes an assertion error868 testRange(&prng.random, -0x80, 0x7f);
668}869}
669870
670fn testRange(r: *Random, start: i32, end: i32) void {871fn testRange(r: *Random, start: i8, end: i8) void {
671 const count = @intCast(usize, end - start);872 const count = @intCast(usize, i32(end) - i32(start));
672 var values_buffer = []bool{false} ** 20;873 var values_buffer = []bool{false} ** 0x100;
673 const values = values_buffer[0..count];874 const values = values_buffer[0..count];
674 var i: usize = 0;875 var i: usize = 0;
675 while (i < count) {876 while (i < count) {
676 const value = r.range(i32, start, end);877 const value: i32 = r.intRangeLessThan(i8, start, end);
677 const index = @intCast(usize, value - start);878 const index = @intCast(usize, value - start);
678 if (!values[index]) {879 if (!values[index]) {
679 i += 1;880 i += 1;