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 }