How to Test with PractRand
I first mentioned and gave an overview of PractRand in this blog post. Since then, I've been having “fun” testing with PractRand and the results have shown up in several subsequent posts. You could be having similar fun, too! Let's see how, with a worked example: testing the Mersenne Twister.
Downloading and Building PractRand
You can find PractRand's website
here. The instructions are a bit
basic, but you basically just compile all the files, link them
together to build the RNG_test
program, and you're done.
That said, if you want it to run a bit faster, you might want to apply this patch which causes it to use a larger buffer when reading from standard input.
Here's a complete start-to-finish build process for the PractRand's test program on Linux or OS X:
mkdir PractRand
cd PractRand
curl -OL https://downloads.sourceforge.net/project/pracrand/PractRand_0.93.zip
unzip -q PractRand_0.93.zip
curl -sL http://www.pcg-random.org/downloads/practrand-0.93-bigbuffer.patch | patch -p0
g++ -std=c++14 -c src/*.cpp src/RNGs/*.cpp src/RNGs/other/*.cpp -O3 -Iinclude -pthread
ar rcs libPractRand.a *.o
rm *.o
g++ -std=c++14 -o RNG_test tools/RNG_test.cpp libPractRand.a -O3 -Iinclude -pthread
Making a Output Program
The easiest way to test with PractRand is to make a program that
spews a stream of random bytes (in binary) to standard output.
Writing code to do so is very straightforward when you have a
generator that produces random unsigned 8-bit, 16-bit, 32-bit,
64-bit, or even 128-bit numbers. Some old generators (e.g., Unix
rand()
, random()
and drand()
and C++'s std::minstd_rand
)
don't have a full use-all-the-bits power-of-two output range, but
general-purpose modern ones almost always do.
Testing the 32-Bit Mersenne Twister
Now we'll put together some code for to test C++'s Mersenne Twister. As you can see, the essence of the code we need is very, very simple:
#include <cstdio>
#include <cstdint>
#include <random>
int main()
{
freopen(NULL, "wb", stdout); // Only necessary on Windows, but harmless.
std::mt19937 rng(42);
while (1) {
uint32_t value = rng();
fwrite((void*) &value, sizeof(value), 1, stdout);
}
}
We can compile it like this,
linux$ g++ -std=c++14 -O3 -Wall -o mt19937-min mt19937-min.cpp
(on Linux, gcc
will warn about the freopen
line; you can just
comment it out because it's only needed if you're using Windows).
With the code compiled, we can do a quick check:
linux$ ./mt19937-min | hexdump -Cv | head
00000000 66 dc e1 5f b3 3d ea cb 5c 03 62 f3 0e 95 f5 2e |f.._.=..\.b.....|
00000010 6a f4 63 bb 47 d4 99 c7 bc ae 41 99 14 2c cb 98 |j.c.G.....A..,..|
00000020 66 d6 f0 27 79 18 22 72 d2 41 ef 27 d6 f4 97 19 |f..'y."r.A.'....|
00000030 4a 91 de 0e ca 55 91 75 57 b8 bd dd 74 ed 6d 55 |J....U.uW...t.mU|
00000040 63 ac e2 99 67 eb 92 24 97 3e 44 b5 82 a0 a0 a6 |c...g..$.>D.....|
00000050 95 06 45 05 34 fd 70 0e 01 03 4c f8 57 e9 d4 b8 |..E.4.p...L.W...|
00000060 eb f5 1a d5 9d fd 44 f0 25 db 5b 36 81 09 33 00 |......D.%.[6..3.|
00000070 bf 14 8c 2e bb 93 01 fe 14 99 f3 2e a0 44 13 9e |.............D..|
00000080 cb d1 e2 4d 39 4d 95 9c 15 70 56 86 fc 18 cf 01 |...M9M...pV.....|
00000090 eb f2 93 6e 58 6b e7 05 30 fe 8d 4a da a1 57 86 |...nXk..0..J..W.|
You should always do a quick sanity check with a hex dump just to guard against face-palm-level bugs. For example, if you see output like
00000000 66 dc e1 5f 00 00 00 00 b3 3d ea cb 00 00 00 00 |f.._.....=......|
00000010 5c 03 62 f3 00 00 00 00 0e 95 f5 2e 00 00 00 00 |\.b.............|
00000020 6a f4 63 bb 00 00 00 00 47 d4 99 c7 00 00 00 00 |j.c.....G.......|
00000030 bc ae 41 99 00 00 00 00 14 2c cb 98 00 00 00 00 |..A......,......|
00000040 66 d6 f0 27 00 00 00 00 79 18 22 72 00 00 00 00 |f..'....y."r....|
00000050 d2 41 ef 27 00 00 00 00 d6 f4 97 19 00 00 00 00 |.A.'............|
00000060 4a 91 de 0e 00 00 00 00 ca 55 91 75 00 00 00 00 |J........U.u....|
00000070 57 b8 bd dd 00 00 00 00 74 ed 6d 55 00 00 00 00 |W.......t.mU....|
00000080 63 ac e2 99 00 00 00 00 67 eb 92 24 00 00 00 00 |c.......g..$....|
00000090 97 3e 44 b5 00 00 00 00 82 a0 a0 a6 00 00 00 00 |.>D.............|
it means that that the value variable is a 64-bit variable, but the output it 32 bits! (With GCC, for example, a uint_fast32_t
holds a 32-bit value but is 64-bits wide.)
Finally, we can run PractRand to test it,
linux$ ./mt19937-min | ./RNG_test stdin32
RNG_test using PractRand version 0.93
RNG = RNG_stdin32, seed = 0xc98d226b
test set = normal, folding = standard (32 bit)
rng=RNG_stdin32, seed=0xc98d226b
length= 128 megabytes (2^27 bytes), time= 3.9 seconds
no anomalies in 117 test result(s)
rng=RNG_stdin32, seed=0xc98d226b
length= 256 megabytes (2^28 bytes), time= 8.0 seconds
...
We can see that it runs without problem and doesn't see any statistical flaws (yet!). When starting out with a test suite, it's good to make sure everything is working by using a generator that won't fail immediately. The Mersenne Twister is one such generator, it'll fail at 256 GB of output, but it'll behave okay until then.
Testing the 64-bit Mersenne Twister
For the 64-bit Mersenne Twister, we could leave the test program almost the same. We just need to change the
declared type of the generator and the type of the value
variable,
#include <cstdio>
#include <cstdint>
#include <random>
int main()
{
freopen(NULL, "wb", stdout); // Only necessary on Windows, but harmless.
std::mt19937_64 rng(42);
while (1) {
uint64_t value = rng();
fwrite((void*) &value, sizeof(value), 1, stdout);
}
}
and then run it, remembering to use the stdin64
option,
linux$ ./mt19937_64-min | ./RNG_test stdin64
RNG_test using PractRand version 0.93
RNG = RNG_stdin64, seed = 0x82bfa213
test set = normal, folding = standard (64 bit)
rng=RNG_stdin64, seed=0x82bfa213
length= 128 megabytes (2^27 bytes), time= 3.6 seconds
no anomalies in 148 test result(s)
rng=RNG_stdin64, seed=0x82bfa213
length= 256 megabytes (2^28 bytes), time= 8.1 seconds
no anomalies in 159 test result(s)
^C
Notice that it's a tad faster because it's generating twice as much data at every iteration. In fact, outputting values in a one-at-a-time fashion is easy but needlessly slow. We'll get better performance if we generate a larger block of numbers to output. Adding a little bit more code to create a buffer,
#include <cstdio>
#include <cstddef>
#include <cstdint>
#include <random>
int main()
{
freopen(NULL, "wb", stdout); // Only necessary on Windows, but harmless.
std::mt19937_64 rng(42);
constexpr size_t BUFFER_SIZE = 1024 * 1024 / sizeof(uint64_t);
static uint64_t buffer[BUFFER_SIZE];
while (1) {
for (size_t i = 0; i < BUFFER_SIZE; ++i)
buffer[i] = rng();
fwrite((void*) buffer, sizeof(buffer[0]), BUFFER_SIZE, stdout);
}
}
gives us results more quickly:
linux$ ./mt19937_64-buffer | ./RNG_test stdin64
RNG_test using PractRand version 0.93
RNG = RNG_stdin64, seed = 0xd26c00ae
test set = normal, folding = standard (64 bit)
rng=RNG_stdin64, seed=0xd26c00ae
length= 128 megabytes (2^27 bytes), time= 2.3 seconds
no anomalies in 148 test result(s)
rng=RNG_stdin64, seed=0xd26c00ae
length= 256 megabytes (2^28 bytes), time= 5.3 seconds
no anomalies in 159 test result(s)
rng=RNG_stdin64, seed=0xd26c00ae
length= 512 megabytes (2^29 bytes), time= 11.0 seconds
Test Name Raw Processed Evaluation
BCFN(2+2,13-2,T) R= -7.0 p =1-1.2e-3 unusual
[Low1/64]BCFN(2+2,13-6,T) R= -5.7 p =1-1.0e-3 unusual
...and 167 test result(s) without anomalies
rng=RNG_stdin64, seed=0xd26c00ae
length= 1 gigabyte (2^30 bytes), time= 21.7 seconds
Test Name Raw Processed Evaluation
[Low1/64]DC6-9x1Bytes-1 R= +5.5 p = 7.1e-3 unusual
...and 179 test result(s) without anomalies
rng=RNG_stdin64, seed=0xd26c00ae
length= 2 gigabytes (2^31 bytes), time= 43.2 seconds
no anomalies in 191 test result(s)
rng=RNG_stdin64, seed=0xd26c00ae
length= 4 gigabytes (2^32 bytes), time= 82.8 seconds
no anomalies in 201 test result(s)
^C
We see a couple of blips here where PractRand observes slightly unusual output. With lots of tests applied, that'll happen once in a while and doesn't necessarily indicate a real problem. But if we could set the seed, we could do another run to be sure. I'll leave writing the code as an exercise for you, but here's the full test results for the 64-bit Mersenne Twister with that functionality added and code to print the generator name and seed to standard error:
linux$ ./mt19937_64 | ./RNG_test stdin64
std::mt19937_64(0xcfa51619e9100201) initialized.
RNG_test using PractRand version 0.93
RNG = RNG_stdin64, seed = 0x42cee57d
test set = normal, folding = standard (64 bit)
rng=RNG_stdin64, seed=0x42cee57d
length= 128 megabytes (2^27 bytes), time= 2.6 seconds
Test Name Raw Processed Evaluation
[Low4/64]DC6-9x1Bytes-1 R= -4.8 p =1-2.2e-3 unusual
...and 147 test result(s) without anomalies
rng=RNG_stdin64, seed=0x42cee57d
length= 256 megabytes (2^28 bytes), time= 5.5 seconds
no anomalies in 159 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 512 megabytes (2^29 bytes), time= 11.7 seconds
no anomalies in 169 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 1 gigabyte (2^30 bytes), time= 21.7 seconds
no anomalies in 180 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 2 gigabytes (2^31 bytes), time= 41.6 seconds
no anomalies in 191 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 4 gigabytes (2^32 bytes), time= 78.7 seconds
no anomalies in 201 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 8 gigabytes (2^33 bytes), time= 160 seconds
no anomalies in 212 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 16 gigabytes (2^34 bytes), time= 298 seconds
Test Name Raw Processed Evaluation
[Low16/64]BCFN(2+9,13-4,T) R= +11.5 p = 5.6e-5 unusual
...and 222 test result(s) without anomalies
rng=RNG_stdin64, seed=0x42cee57d
length= 32 gigabytes (2^35 bytes), time= 623 seconds
no anomalies in 233 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 64 gigabytes (2^36 bytes), time= 1186 seconds
no anomalies in 244 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 128 gigabytes (2^37 bytes), time= 2351 seconds
no anomalies in 255 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 256 gigabytes (2^38 bytes), time= 4505 seconds
no anomalies in 265 test result(s)
rng=RNG_stdin64, seed=0x42cee57d
length= 512 gigabytes (2^39 bytes), time= 9219 seconds
Test Name Raw Processed Evaluation
BRank(12):24K(1) R=+99759 p~= 0 FAIL !!!!!!!!
[Low16/64]BRank(12):16K(1) R= +1165 p~= 1.3e-351 FAIL !!!!!!!
[Low1/64]BCFN(2+0,13-0,T) R= +8.2 p = 6.0e-4 unusual
...and 273 test result(s) without anomalies
So there we go! In PractRand's tests, the 64-bit Mersenne Twister is good to 256 GB, but fails (badly!) at 512 GB. (TestU01 has a more specialized and sensitive test that can detect the same linear complexity issues in only 50,000 outputs, but arguably PractRand's more generalized test provides a more realistic measure of the seriousness of the issue.)
Integrating into PractRand Instead?
Instead of using the standard-input interface, you can integrate your code directly into PractRand. It actually already has a built-in Mersenne Twister, so we can see if using it offers any advantage:
linux$ ./RNG_test mt19937
RNG_test using PractRand version 0.93
RNG = mt19937, seed = 0xc5937f7
test set = normal, folding = standard (32 bit)
rng=mt19937, seed=0xc5937f7
length= 128 megabytes (2^27 bytes), time= 3.0 seconds
no anomalies in 117 test result(s)
rng=mt19937, seed=0xc5937f7
length= 256 megabytes (2^28 bytes), time= 6.4 seconds
Test Name Raw Processed Evaluation
DC6-9x1Bytes-1 R= -3.9 p =1-7.2e-3 unusual
...and 123 test result(s) without anomalies
rng=mt19937, seed=0xc5937f7
length= 512 megabytes (2^29 bytes), time= 11.7 seconds
no anomalies in 132 test result(s)
^C
Meh. It's faster but not a lot faster. I don't think it's worth
the hassle of learning the internals of PractRand or the risks
(e.g., no more hexdump
checks to make sure we've not made a silly blunder).
I like having a separate program I can test. But if you want to go
that way, go for it!
Conclusion
Using a random-number test suite isn't hard. You can do it, too!
(I recommend you don't try Xoroshiro128+ or Xorshift128+ as your first generator to test; you might think your setup isn't working. Try something else first.)