The OpenD Programming Language

1 /++
2 SplitMix generator family.
3 
4 An n-bit splitmix PRNG has an internal n-bit counter and an n-bit increment.
5 The state is advanced by adding the increment to the counter and output is
6 the counter's value <a href="#fmix64">mixed</a>. The increment remains constant
7 for an instance over its lifetime, so each instance of the PRNG needs to
8 explicitly store its increment only if the `split()` operation is needed.
9 
10 The first version of splitmix was described in
11 $(LINK2 http://gee.cs.oswego.edu/dl/papers/oopsla14.pdf, Fast Splittable
12 Pseudorandom Number Generators) (2014) by Guy L. Steele Jr., Doug Lea, and
13 Christine H. Flood. A key selling point of the generator was the ability
14 to $(I split) the sequence:
15 
16 <blockquote>
17 "A conventional linear PRNG object provides a generate method that returns
18 one pseudorandom value and updates the state of the PRNG, but a splittable
19 PRNG object also has a second operation, split, that replaces the original
20 PRNG object with two (seemingly) independent PRNG objects, by creating and
21 returning a new such object and updating the state of the original object.
22 Splittable PRNG objects make it easy to organize the use of pseudorandom
23 numbers in multithreaded programs structured using fork-join parallelism."
24 </blockquote>
25 
26 However, splitmix $(LINK2 http://xoroshiro.di.unimi.it/splitmix64.c,
27 is also used) as a non-splittable PRNG with a constant increment that
28 does not vary from one instance to the next. This cuts the needed space
29 in half. This module provides predefined fixed-increment $(LREF SplitMix64)
30 and splittable $(LREF Splittable64).
31 +/
32 module mir.random.engine.splitmix;
33 import std.traits: TemplateOf;
34 
35 @nogc:
36 nothrow:
37 @safe:
38 
39 /++
40 64-bit $(LINK2 https://en.wikipedia.org/wiki/MurmurHash,
41 MurmurHash3)-style bit mixer, parameterized.
42 
43 Pattern is:
44 ---
45 ulong fmix64(ulong x)
46 {
47     x = (x ^ (x >>> shift1)) * m1;
48     x = (x ^ (x >>> shift2)) * m2;
49     return x ^ (x >>> shift3);
50 }
51 ---
52 
53 As long as m1 and m2 are odd each operation is invertible with the consequence
54 that `fmix64(a) == fmix64(b)` if and only if `(a == b)`.
55 
56 Good parameters for fmix64 are found empirically. Several sets of
57 <a href="#murmurHash3Mix">suggested parameters</a> are provided.
58 +/
59 ulong fmix64(ulong m1, ulong m2, uint shift1, uint shift2, uint shift3)(ulong x) @nogc nothrow pure @safe
60 {
61     enum bits = ulong.sizeof * 8;
62     //Sets of parameters for this function are selected empirically rather than
63     //on the basis of theory. Nevertheless we can identify minimum reasonable
64     //conditions. Meeting these conditions does not imply that a set of
65     //parameters is suitable, but any sets of parameters that fail to meet
66     //these conditions are obviously unsuitable.
67     static assert(m1 != 1 && m1 % 2 == 1, "Multiplier must be odd number other than 1!");
68     static assert(m2 != 1 && m2 % 2 == 1, "Multiplier must be odd number other than 1!");
69     static assert(shift1 > 0 && shift1 < bits, "Shift out of bounds!");
70     static assert(shift2 > 0 && shift2 < bits, "Shift out of bounds!");
71     static assert(shift3 > 0 && shift3 < bits, "Shift out of bounds!");
72     static assert(shift1 + shift2 + shift3 >= bits - 1,
73         "Shifts must be sufficient for most significant bit to affect least significant bit!");
74     static assert(ulong.max / m1 <= m2,
75         "Multipliers must be sufficient for least significant bit to affect most significant bit!");
76 
77     pragma(inline, true);
78     x = (x ^ (x >>> shift1)) * m1;
79     x = (x ^ (x >>> shift2)) * m2;
80     return x ^ (x >>> shift3);
81 }
82 
83 /++
84 Well known sets of parameters for $(LREF fmix64).
85 Predefined are murmurHash3Mix and staffordMix01 through staffordMix14.
86 
87 See David Stafford's 2011 blog entry
88 $(LINK2 https://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html,
89 Better Bit Mixing - Improving on MurmurHash3's 64-bit Finalizer).
90 +/
91 alias murmurHash3Mix() = .fmix64!(0xff51afd7ed558ccdUL, 0xc4ceb9fe1a85ec53UL, 33, 33, 33);
92 ///ditto
93 alias staffordMix01() = .fmix64!(0x7fb5d329728ea185UL, 0x81dadef4bc2dd44dUL, 31, 27, 33);
94 ///ditto
95 alias staffordMix02() = .fmix64!(0x64dd81482cbd31d7UL, 0xe36aa5c613612997UL, 33, 31, 31);
96 ///ditto
97 alias staffordMix03() = .fmix64!(0x99bcf6822b23ca35UL, 0x14020a57acced8b7UL, 31, 30, 33);
98 ///ditto
99 alias staffordMix04() = .fmix64!(0x62a9d9ed799705f5UL, 0xcb24d0a5c88c35b3UL, 33, 28, 32);
100 ///ditto
101 alias staffordMix05() = .fmix64!(0x79c135c1674b9addUL, 0x54c77c86f6913e45UL, 31, 29, 30);
102 ///ditto
103 alias staffordMix06() = .fmix64!(0x69b0bc90bd9a8c49UL, 0x3d5e661a2a77868dUL, 31, 27, 30);
104 ///ditto
105 alias staffordMix07() = .fmix64!(0x16a6ac37883af045UL, 0xcc9c31a4274686a5UL, 30, 26, 32);
106 ///ditto
107 alias staffordMix08() = .fmix64!(0x294aa62849912f0bUL, 0x0a9ba9c8a5b15117UL, 30, 28, 31);
108 ///ditto
109 alias staffordMix09() = .fmix64!(0x4cd6944c5cc20b6dUL, 0xfc12c5b19d3259e9UL, 32, 29, 32);
110 ///ditto
111 alias staffordMix10() = .fmix64!(0xe4c7e495f4c683f5UL, 0xfda871baea35a293UL, 30, 32, 33);
112 ///ditto
113 alias staffordMix11() = .fmix64!(0x97d461a8b11570d9UL, 0x02271eb7c6c4cd6bUL, 27, 28, 32);
114 ///ditto
115 alias staffordMix12() = .fmix64!(0x3cd0eb9d47532dfbUL, 0x63660277528772bbUL, 29, 26, 33);
116 ///ditto
117 alias staffordMix13() = .fmix64!(0xbf58476d1ce4e5b9UL, 0x94d049bb133111ebUL, 30, 27, 31);
118 ///ditto
119 alias staffordMix14() = .fmix64!(0x4be98134a5976fd3UL, 0x3bc0993a5ad19a13UL, 30, 29, 31);
120 ///
121 @nogc nothrow pure @safe version(mir_random_test) unittest
122 {
123     enum ulong x1 = murmurHash3Mix(0x1234_5678_9abc_defeUL);//Mix some number at compile time.
124     static assert(x1 == 0xb194_3cfe_a4f7_8f08UL);
125 
126     immutable ulong x2 = murmurHash3Mix(0x1234_5678_9abc_defeUL);//Mix some number at run time.
127     assert(x1 == x2);//Same result.
128 }
129 ///
130 @nogc nothrow pure @safe version(mir_random_test) unittest
131 {
132     //Verify all sets of predefined parameters are valid
133     //and no two are identical.
134     ulong[15] array;
135     array[0] = murmurHash3Mix(1);
136     array[1] = staffordMix01(1);
137     array[2] = staffordMix02(1);
138     array[3] = staffordMix03(1);
139     array[4] = staffordMix04(1);
140     array[5] = staffordMix05(1);
141     array[6] = staffordMix06(1);
142     array[7] = staffordMix07(1);
143     array[8] = staffordMix08(1);
144     array[9] = staffordMix09(1);
145     array[10] = staffordMix10(1);
146     array[11] = staffordMix11(1);
147     array[12] = staffordMix12(1);
148     array[13] = staffordMix13(1);
149     array[14] = staffordMix14(1);
150     foreach (i; 1 .. array.length)
151         foreach (e; array[0 .. i])
152             if (e == array[i])
153                 assert(0, "fmix64 predefines are not all distinct!");
154 }
155 
156 /++
157  Canonical fixed increment (non-splittable) SplitMix64 engine.
158 
159  64 bits of state, period of `2 ^^ 64`.
160  +/
161 alias SplitMix64 = SplitMixEngine!(staffordMix13, false);
162 ///
163 @nogc nothrow pure @safe version(mir_random_test) unittest
164 {
165     import mir.random;
166     static assert(isSaturatedRandomEngine!SplitMix64);
167     auto rng = SplitMix64(1u);
168     ulong x = rng.rand!ulong;
169     assert(x == 10451216379200822465UL);
170 }
171 ///
172 @nogc nothrow pure @safe version(mir_random_test) unittest
173 {
174     import mir.random;
175     import std.range.primitives: isRandomAccessRange;
176     // SplitMix64 should be both a Mir-style saturated
177     // random engine and a Phobos-style uniform RNG
178     // and random access range.
179     static assert(isPhobosUniformRNG!SplitMix64);
180     static assert(isRandomAccessRange!SplitMix64);
181     static assert(isSaturatedRandomEngine!SplitMix64);
182 
183     SplitMix64 a = SplitMix64(1);
184     immutable ulong x = a.front;
185     SplitMix64 b = a.save;
186     assert (x == a.front);
187     assert (x == b.front);
188     assert (x == a[0]);
189 
190     immutable ulong y = a[1];
191     assert(x == a());
192     assert(x == b());
193     assert(a.front == y);
194 }
195 
196 /++
197  Canonical splittable (specifiable-increment) SplitMix64 engine.
198 
199  128 bits of state, period of `2 ^^ 64`.
200  +/
201 alias Splittable64 = SplitMixEngine!(staffordMix13, true);
202 ///
203 @nogc nothrow pure @safe version(mir_random_test) unittest
204 {
205     import mir.random;
206     static assert(isSaturatedRandomEngine!Splittable64);
207     auto rng = Splittable64(1u);
208     ulong x = rng.rand!ulong;
209     assert(x == 10451216379200822465UL);
210 
211     //Split example:
212     auto rng1 = Splittable64(1u);
213     auto rng2 = rng1.split();
214 
215     assert(rng1.rand!ulong == 17911839290282890590UL);
216     assert(rng2.rand!ulong == 14201552918486545593UL);
217     assert(rng1.increment != rng2.increment);
218 }
219 ///
220 @nogc nothrow pure @safe version(mir_random_test) unittest
221 {
222     import mir.random;
223     import std.range.primitives: isRandomAccessRange;
224     // Splittable64 should be both a Mir-style saturated
225     // random engine and a Phobos-style uniform RNG
226     // and random access range.
227     static assert(isPhobosUniformRNG!Splittable64);
228     static assert(isRandomAccessRange!Splittable64);
229     static assert(isSaturatedRandomEngine!Splittable64);
230 
231     Splittable64 a = Splittable64(1);
232     immutable ulong x = a.front;
233     Splittable64 b = a.save;
234     assert (x == a.front);
235     assert (x == b.front);
236     assert (x == a[0]);
237 
238     immutable ulong y = a[1];
239     assert(x == a());
240     assert(x == b());
241     assert(a.front == y);
242 }
243 
244 
245 /++
246 Default increment used by $(LREF SplitMixEngine).
247 Defined in $(LINK2 http://gee.cs.oswego.edu/dl/papers/oopsla14.pdf,
248 Fast Splittable Pseudorandom Number Generators) (2014) as "the odd integer
249 closest to (2 ^^ 64)/φ, where φ = (1 + √5)/2 is the
250 $(LINK2 https://en.wikipedia.org/wiki/Golden_ratio, golden ratio)."
251 In the paper this constant is referred to as "GOLDEN_GAMMA".
252 
253 From the authors:
254 <blockquote>
255 [...] our choice of the odd integer closest to (2 ^^ 64)/φ was based
256 only on the intuition that it might be a good idea to keep γ values
257 “well spread out” and the fact that prefixes of the Weyl sequence
258 generated by 1/φ are known to be “well spread out” [citing vol. 3 of
259 Knuth's <i>The Art of Computer Programming</i>, exercise 6.4-9]
260 </blockquote>
261 +/
262 enum ulong DEFAULT_SPLITMIX_INCREMENT = 0x9e3779b97f4a7c15UL;
263 /// ditto
264 alias GOLDEN_GAMMA = DEFAULT_SPLITMIX_INCREMENT;
265 
266 /++
267 Generic SplitMixEngine.
268 
269 The first parameter $(D_PARAM mixer) should be a explicit instantiation
270 of $(LREF fmix64) or a predefined parameterization of `fmix64` such as
271 $(LREF murmurHash3Mix) or $(LREF staffordMix13).
272 
273 The second parameter is whether the $(LREF split) operation is enabled.
274 Allows each instance to have a distinct increment, increasing the size
275 from 64 bits to 128 bits. If `split` is not enabled then the `opCall`,
276 `seed`, and `skip` operations will be `shared` provided the platform
277 supports 64-bit atomic operations.
278 
279 The third parameter is the $(LREF default_increment). If the
280 SplitMixEngine has a fixed increment this value will be used for
281 each instance. If omitted this parameter defaults to
282 $(LREF DEFAULT_SPLITMIX_INCREMENT).
283 +/
284 struct SplitMixEngine(alias mixer, bool split_enabled = false, OptionalArgs...)
285     if ((__traits(compiles, {static assert(__traits(isSame, TemplateOf!(mixer!()), fmix64));})
286             || __traits(compiles, {static assert(__traits(isSame, TemplateOf!mixer, fmix64));}))
287         && (OptionalArgs.length < 1 || (is(typeof(OptionalArgs[1]) == ulong) && OptionalArgs[1] != DEFAULT_SPLITMIX_INCREMENT))
288         && OptionalArgs.length < 2)
289 {
290     @nogc:
291     nothrow:
292     pure:
293     @safe:
294 
295     static if (__traits(compiles, {static assert(__traits(isSame, TemplateOf!(mixer!()), fmix64));}))
296         alias fmix64 = mixer!();
297     else
298         alias fmix64 = mixer;
299 
300     static if (OptionalArgs.length >= 1)
301         /++
302          + Either $(LREF DEFAULT_SPLITMIX_INCREMENT) or the optional
303          + third argument of this template.
304          +/
305         enum ulong default_increment = OptionalArgs[1];
306     else
307         /// ditto
308         enum ulong default_increment = DEFAULT_SPLITMIX_INCREMENT;
309 
310     static assert(default_increment % 2 != 0, "Increment must be an odd number!");
311 
312     /// Marks as a Mir random engine.
313     enum bool isRandomEngine = true;
314     /// Largest generated value.
315     enum ulong max = ulong.max;
316 
317     /// Full period (2 ^^ 64).
318     enum uint period_pow2 = 64;
319 
320     /++
321     Whether each instance can set its increment individually.
322     Enables the $(LREF split) operation at the cost of increasing
323     size from 64 bits to 128 bits.
324     +/
325     enum bool increment_specifiable = split_enabled;
326 
327     /// Current internal state of the generator.
328     public ulong state;
329 
330     static if (increment_specifiable)
331     {
332         /++
333         Either an enum or a settable value depending on whether `increment_specifiable == true`.
334         This should always be an odd number. The paper refers to this as `γ` so it is aliased
335         as `gamma`.
336         +/
337         ulong increment = default_increment;
338     }
339     else
340     {
341         /// ditto
342         enum ulong increment = default_increment;
343     }
344     /// ditto
345     alias gamma = increment;
346 
347     @disable this();
348     @disable this(this);
349 
350     /++
351      + Constructs a $(D SplitMixEngine) generator seeded with $(D_PARAM x0).
352      +/
353     this()(ulong x0)
354     {
355         static if (increment_specifiable)
356             increment = default_increment;
357         this.state = x0;
358     }
359 
360     /++
361      + Constructs a $(D SplitMixEngine) generator seeded with $(D_PARAM x0)
362      + using the specified $(D_PARAM increment).
363      +
364      + Note from the authors (the paper uses `γ` to refer to the _increment):
365      +
366      + <blockquote>
367      + [W]e tested PRNG objects with “sparse” γ values whose representations
368      +  have either very few 1-bits or very few 0-bits, and found that such
369      + cases produce pseudorandom sequences that DieHarder regards as “weak”
370      + just a little more often than usual.
371      + </blockquote>
372      +
373      + As a consequence the provided $(LREF split) function guards against this
374      + and also against increments that have long consecutive runs of either 1
375      + or 0. However, this constructor only forces $(D_PARAM increment) to be
376      + an odd number and performs no other transformation.
377      +/
378     this()(ulong x0, ulong increment) if (increment_specifiable)
379     {
380         this.increment = increment | 1UL;
381         this.state = x0;
382     }
383 
384     /// Advances the random sequence.
385     ulong opCall()()
386     {
387         version(LDC) pragma(inline, true);
388         else pragma(inline);
389         return fmix64(state += increment);
390     }
391     /// ditto
392     ulong opCall()() shared if (!increment_specifiable)
393     {
394         import core.atomic : atomicOp;
395         return fmix64(atomicOp!"+="(state, increment));
396     }
397     ///
398     @nogc nothrow pure @safe version(mir_random_test) unittest
399     {
400         auto rnd = SplitMixEngine!staffordMix13(1);
401         assert(rnd() == staffordMix13(1 + GOLDEN_GAMMA));
402     }
403 
404     /++
405     Produces a splitmix generator with a different counter-value
406     and increment-value than the current generator. Only available
407     when <a href="#.SplitMixEngine.increment_specifiable">
408     `increment_specifiable == true`</a>.
409     +/
410     typeof(this) split()() if (increment_specifiable)
411     {
412         immutable state1 = opCall();
413         //Use a different mix function for the increment.
414         static if (fmix64(1) == .murmurHash3Mix(1))
415             ulong gamma1 = .staffordMix13(state += increment);
416         else
417             ulong gamma1 = .murmurHash3Mix(state += increment);
418         gamma1 |= 1UL;//Ensure increment is odd.
419         import core.bitop: popcnt;
420         //Approximately 2.15% chance.
421         if (popcnt(gamma1 ^ (gamma1 >>> 1)) < 24)
422             gamma1 ^= 0xaaaa_aaaa_aaaa_aaaaUL;
423         return typeof(this)(state1, gamma1);
424     }
425     ///
426     @nogc nothrow pure @safe version(mir_random_test) unittest
427     {
428         auto rnd1 = SplitMixEngine!(staffordMix13,true)(1);
429         auto rnd2 = rnd1.split();
430         assert(rnd1.state != rnd2.state);
431         assert(rnd1.increment != rnd2.increment);
432         assert(rnd1() != rnd2());
433     }
434 
435     /++
436     Skip forward in the random sequence in $(BIGOH 1) time.
437     +/
438     void skip()(size_t n)
439     {
440         state += n * increment;
441     }
442     /// ditto
443     void skip()(size_t n) shared if (!increment_specifiable)
444     {
445         import core.atomic : atomicOp;
446         atomicOp!"+="(state, n * increment);
447     }
448 
449     /++
450     Compatibility with $(LINK2 https://dlang.org/phobos/std_random.html#.isUniformRNG,
451     Phobos library methods). Presents this RNG as an InputRange.
452     +/
453     enum bool isUniformRandom = true;
454     /// ditto
455     enum ulong min = ulong.min;
456     /// ditto
457     enum bool empty = false;
458     /// ditto
459     @property ulong front()() const
460     {
461         version (LDC) pragma(inline, true);
462         else pragma(inline);
463         return fmix64(state + increment);
464     }
465     /// ditto
466     void popFront()()
467     {
468         pragma(inline, true);
469         state += increment;
470     }
471     /// ditto
472     void seed()(ulong x0)
473     {
474         this.__ctor(x0);
475     }
476     /// ditto
477     void seed()(ulong x0) shared if (!increment_specifiable)
478     {
479         import core.atomic : atomicStore;
480         atomicStore(this.state, x0);
481     }
482     /// ditto
483     void seed()(ulong x0, ulong increment) if (increment_specifiable)
484     {
485         this.__ctor(x0, increment);
486     }
487     /// ditto
488     @property typeof(this) save()() const
489     {
490         static if (increment_specifiable)
491             return typeof(this)(state, increment);
492         else
493             return typeof(this)(state);
494     }
495     /// ditto
496     ulong opIndex()(size_t n) const
497     {
498         return fmix64(state + (n + 1) * increment);
499     }
500     /// ditto
501     size_t popFrontN()(size_t n)
502     {
503         skip(n);
504         return n;
505     }
506     /// ditto
507     alias popFrontExactly() = skip;
508 }
509 ///
510 @nogc nothrow pure @safe version(mir_random_test) unittest
511 {
512     //Can specify engine like this:
513     alias RNG1 = SplitMixEngine!staffordMix13;
514     alias RNG2 = SplitMixEngine!(fmix64!(0xbf58476d1ce4e5b9UL, 0x94d049bb133111ebUL, 30, 27, 31));
515 
516     //Each way of writing it results in the same sequence.
517     assert(RNG1(1).opCall() == RNG2(1).opCall());
518 
519     //However not each result's name is equally informative.
520     static assert(RNG1.stringof == `SplitMixEngine!(staffordMix13, false)`);
521     static assert(RNG2.stringof == `SplitMixEngine!(fmix64, false)`);//Doesn't include parameters of fmix64!
522 }
523 
524 @nogc nothrow pure @safe version(mir_random_test) unittest
525 {
526     SplitMix64 a = SplitMix64(1);
527     a.popFrontExactly(1);
528     import std.meta: AliasSeq;
529     foreach (f; AliasSeq!(murmurHash3Mix,staffordMix11,staffordMix13))
530     {
531         auto rnd = SplitMixEngine!(f, true)(0);
532         auto rnd2 = rnd.split();
533     }
534 }
535 
536 @nogc nothrow @safe version(mir_random_test) unittest
537 {
538     // Check there is no inconsistency between shared and unshared.
539     auto localRNG = SplitMixEngine!staffordMix13(1);
540     shared static sharedRNG = typeof(localRNG)(2);
541     assert(localRNG() != sharedRNG());
542     localRNG.seed(3);
543     sharedRNG.seed(3);
544     assert(localRNG() == sharedRNG());
545     localRNG.skip(10);
546     sharedRNG.skip(10);
547     assert(localRNG() == sharedRNG());
548 }