Showing pcg32.pl (raw)


  1. #!/usr/bin/env perl
  2.  
  3. ###############################################################################
  4. # Two implementations of PCG32. One in native Perl with no dependencies, and
  5. # one that uses Math::Int64. Surprisingly the native version is significantly
  6. # faster.
  7. #
  8. # A lot of work was done here to mimic how C handles overflow multiplication
  9. # on large uint64_t numbers. Perl converts scalars that are larger than 2^64-1
  10. # to floating point on the backend. We do *NOT* want that for PCG, because
  11. # PCG (and more PRNGs) rely on overflow math to do their magic. We utilize
  12. # 'use integer' to force Perl to do all math with regular 64bit values. When
  13. # overflow occurs Perl likes to convert large values to negative numbers. In
  14. # the original C all math is done with uint64_t, so we have to convert the
  15. # IV/negative numbers back into UV/unsigned (positive) values. PCG also uses
  16. # some uint32_t variables internally, so we mimic that by doing the math in
  17. # 64bit and then masking down to only the 32bit number.
  18. #
  19. ###############################################################################
  20. #
  21. # Original C code from: https://www.pcg-random.org/download.html
  22. #
  23. # typedef struct { uint64_t state;  uint64_t inc; } pcg32_random_t;
  24. #
  25. # uint32_t pcg32_random_r(pcg32_random_t* rng) {
  26. #     uint64_t oldstate = rng->state;
  27. #     // Advance internal state
  28. #     rng->state = oldstate * 6364136223846793005ULL + (rng->inc|1);
  29. #     // Calculate output function (XSH RR), uses old state for max ILP
  30. #     uint32_t xorshifted = ((oldstate >> 18u) ^ oldstate) >> 27u;
  31. #     uint32_t rot = oldstate >> 59u;
  32. #     return (xorshifted >> rot) | (xorshifted << ((-rot) & 31));
  33. # }
  34. #
  35. ###############################################################################
  36.  
  37. use strict;
  38. use warnings;
  39. use v5.16;
  40. use Math::Int64 qw(uint64 uint64_to_number);
  41. use Getopt::Long;
  42. use Test::More;
  43.  
  44. ###############################################################################
  45. ###############################################################################
  46.  
  47. my $debug = 0;
  48. my $s1    = 15939250660798104135; # Default 64bit seed1
  49. my $s2    = 3988331200502121509;  # Default 64bit seed2
  50. my $s3    = 15939250660798104135; # Default 64bit seed1
  51. my $s4    = 3988331200502121509;  # Default 64bit seed2
  52.  
  53. GetOptions(
  54.     'debug'      => \$debug,
  55.     'seed1=i'    => \$s1,
  56.     'seed2=i'    => \$s2,
  57.     'random'     => \&randomize_seeds,
  58.     'unit-tests' => \&run_unit_tests,
  59. );
  60.  
  61. my $seeds32 = [$s1, $s2];
  62. my $seeds64 = [$s1, $s2];
  63.  
  64. my $num = $ARGV[0] || 8;
  65.  
  66. print color('yellow', "Seeding PRNG with: $s1 / $s2\n\n");
  67.  
  68. for (my $i = 1; $i <= $num; $i++) {
  69.     my $num32 = pcg32_perl($seeds32);
  70.     my $num64 = pcg64_perl($seeds64);
  71.     printf("%2d) %10u / %20u\n", $i, $num32, $num64);
  72. }
  73.  
  74. ################################################################################
  75. ################################################################################
  76. ################################################################################
  77.  
  78. #my $seeds = [12, 34];
  79. #my $rand  = pcg32_perl($seeds);
  80. sub pcg32_perl {
  81.     # state/inc are passed in by reference
  82.     my ($seeds)  = @_;
  83.     my $oldstate = $seeds->[0]; # Save original state
  84.  
  85.     # We use interger math because Perl converts to floats any scalar
  86.     # larger than 2^64 - 1. PCG *requires* 64bit uint64_t math, with overflow,
  87.     # to calculate correctly. We have to unconvert the overflowed signed integer (IV)
  88.     # to an unsigned integer (UV) using bitwise or against zero. (weird hack)
  89.     use integer;
  90.     $seeds->[0]  = ($oldstate * 6364136223846793005 + ($seeds->[1] | 1));
  91.     no integer;
  92.     #$seeds->[0] |= 0; # Only needed if you look at the seeds cuz they might be negative
  93.  
  94.     my $xorshifted = ((($oldstate >> 18) ^ $oldstate) >> 27) & 0xFFFFFFFF;
  95.  
  96.     # -$rot on a uint32_t is the same as (2^32 - $rot)
  97.     my $rot    = ($oldstate >> 59);
  98.     my $invrot = 4294967296 - $rot;
  99.     my $ret    = (($xorshifted >> $rot) | ($xorshifted << ($invrot & 31))) & 0xFFFFFFFF;
  100.  
  101.     if (defined($debug) && $debug > 0) {
  102.         # $oldstate is the state at the start of the function and $inc
  103.         # doesn't change so we can print out the initial values here
  104.         print color('orange', "State : " . ($oldstate | 0) . "/" . ($seeds->[1] | 0) . "\n");
  105.         print color('orange', "State2: " . ($seeds->[0] | 0) . "\n");
  106.         print color('orange', "Xor   : $xorshifted\n");
  107.         print color('orange', "Rot   : $rot\n");
  108.     }
  109.  
  110.     return $ret;
  111. }
  112.  
  113. # Based on the C algorithm: https://chatgpt.com/share/693cc99c-8068-800d-858e-be16ec1d7521
  114. #my $seeds = [12, 34];
  115. #my $rand  = pcg64_perl($seeds);
  116. sub pcg64_perl {
  117.     my $seeds = $_[0];
  118.     my $ret   = (($seeds->[0] >> (($seeds->[0] >> 59) + 5)) ^ $seeds->[0]);
  119.  
  120.     use integer;
  121.     $ret *= 12605985483714917081;
  122.     $seeds->[0] = $seeds->[0] * 6364136223846793005 + $seeds->[1];
  123.     no integer;
  124.     #$seeds->[0] |= 0; # Only needed if you look at the seeds cuz they might be negative
  125.  
  126.     $ret = ($ret >> 43) ^ $ret;
  127.  
  128.     return $ret;
  129. }
  130.  
  131. # To get a 64bit number from PCG32 you create two different generators
  132. # and combine the results into a single 64bit value. All the examples
  133. # online show 1 for the inc/seed2 value. I'm not sure why that is, but
  134. # I copied it for my implementation.
  135. #
  136. #my $seeds = [[12, 1], [34,1]];
  137. #my $rand  = pcg64_perl_chained($seeds);
  138. sub pcg64_perl_chained {
  139.     my ($seeds) = @_;
  140.  
  141.     # Get two 32bit ints
  142.     my $high = pcg32_perl($seeds->[0]);
  143.     my $low  = pcg32_perl($seeds->[1]);
  144.  
  145.     # Combine the two 32bits into one 64bit int
  146.     my $ret = ($high << 32) | $low;
  147.  
  148.     return $ret;
  149. }
  150.  
  151. #my $seeds = [uint64(12), uint64(34)];
  152. #my $rand  = pcg32_math64($seeds);
  153. sub pcg32_math64 {
  154.     # state/inc are passed in by reference
  155.     my ($s) = @_;
  156.  
  157.     my $oldstate = $s->[0];
  158.     $s->[0]      = $oldstate * 6364136223846793005 + ($s->[1] | 1);
  159.  
  160.     my $xorshifted = (($oldstate >> 18) ^ $oldstate) >> 27;
  161.     $xorshifted    = $xorshifted & 0xFFFFFFFF; # Convert to uint32_t
  162.  
  163.     my $rot    = $oldstate >> 59;
  164.     my $invrot = 4294967296 - $rot;
  165.  
  166.     my $ret = ($xorshifted >> $rot) | ($xorshifted << ($invrot & 31));
  167.     $ret    = $ret & 0xFFFFFFFF; # Convert to uint32_t
  168.  
  169.     $ret = uint64_to_number($ret);
  170.  
  171.     if ($debug) {
  172.         # $oldstate is the state at the start of the function and $inc
  173.         # doesn't change so we can print out the initial values here
  174.         print color('orange', "State : $oldstate/$s->[1]\n");
  175.         print color('orange', "State2: $s->[0]\n");
  176.         print color('orange', "Xor   : $xorshifted\n");
  177.         print color('orange', "Rot   : $rot\n");
  178.     }
  179.  
  180.     return $ret;
  181. }
  182.  
  183. ###############################################################################
  184. ###############################################################################
  185.  
  186. # String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue'
  187. sub color {
  188.     my ($str, $txt) = @_;
  189.  
  190.     # If we're NOT connected to a an interactive terminal don't do color
  191.     if (-t STDOUT == 0) { return $txt || ""; }
  192.  
  193.     # No string sent in, so we just reset
  194.     if (!length($str) || $str eq 'reset') { return "\e[0m"; }
  195.  
  196.     # Some predefined colors
  197.     my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0);
  198.     $str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg;
  199.  
  200.     # Get foreground/background and any commands
  201.     my ($fc,$cmd) = $str =~ /^(\d{1,3})?_?(\w+)?$/g;
  202.     my ($bc)      = $str =~ /on_(\d{1,3})$/g;
  203.  
  204.     if (defined($fc) && int($fc) > 255) { $fc = undef; } # above 255 is invalid
  205.  
  206.     # Some predefined commands
  207.     my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7);
  208.     my $cmd_num = $cmd_map{$cmd // 0};
  209.  
  210.     my $ret = '';
  211.     if ($cmd_num)      { $ret .= "\e[${cmd_num}m"; }
  212.     if (defined($fc))  { $ret .= "\e[38;5;${fc}m"; }
  213.     if (defined($bc))  { $ret .= "\e[48;5;${bc}m"; }
  214.     if (defined($txt)) { $ret .= $txt . "\e[0m";   }
  215.  
  216.     return $ret;
  217. }
  218.  
  219. sub randomize_seeds {
  220.     print color(51, "Using random seeds\n");
  221.  
  222.     $s1 = perl_rand64();
  223.     $s2 = perl_rand64();
  224. }
  225.  
  226. sub perl_rand64 {
  227.     my $low  = int(rand() * (2**32-1));
  228.     my $high = int(rand() * (2**32-1));
  229.  
  230.     my $ret = ($high << 32) | $low;
  231.  
  232.     return $ret;
  233. }
  234.  
  235. # Creates methods k() and kd() to print, and print & die respectively
  236. BEGIN {
  237.     if (eval { require Data::Dump::Color }) {
  238.         *k = sub { Data::Dump::Color::dd(@_) };
  239.     } else {
  240.         require Data::Dumper;
  241.         *k = sub { print Data::Dumper::Dumper(\@_) };
  242.     }
  243.  
  244.     sub kd {
  245.         k(@_);
  246.  
  247.         printf("Died at %2\$s line #%3\$s\n",caller());
  248.         exit(15);
  249.     }
  250. }
  251.  
  252. # Run a test with a given seed and return a string of the results
  253. sub quick_test32 {
  254.     my $seed = $_[0];
  255.  
  256.     my @data = ();
  257.     for (my $i = 0; $i < 4; $i++) {
  258.         my $num = pcg32_perl($seed);
  259.         push(@data, $num);
  260.     }
  261.  
  262.     my $ret = join(", ", @data);
  263.     return $ret;
  264. }
  265.  
  266. sub quick_test64 {
  267.     my ($seed) = @_;
  268.  
  269.     my @data = ();
  270.     for (my $i = 0; $i < 4; $i++) {
  271.         my $num = pcg64_perl($seed);
  272.         push(@data, $num);
  273.     }
  274.  
  275.     my $ret = join(", ", @data);
  276.     return $ret;
  277. }
  278.  
  279. sub run_unit_tests {
  280.     # Seeds < 2**32
  281.     is(quick_test32([11   , 22])   , '0, 1425092920, 3656087653, 1104107026');
  282.     is(quick_test32([33   , 44])   , '0, 3850707138, 2930351490, 1110209703');
  283.     is(quick_test32([55   , 66])   , '0, 1725101930, 224698313, 2870828486');
  284.     is(quick_test32([12345, 67890]), '0, 8251198, 44679150, 3046830521');
  285.     is(quick_test32([9999 , 9999]) , '0, 521292032, 3698775557, 199399470');
  286.  
  287.     is(quick_test64([11   , 22])   , '9538631804898304851, 16158778725070734108, 11691277237799343826, 3387200422953703275');
  288.     is(quick_test64([33   , 44])   , '16009909930975141620, 326681257729406768, 10608485012141334170, 3059691087832193363');
  289.     is(quick_test64([55   , 66])   , '16640429467063018515, 10892804362730022438, 297264128773379188, 844739387753726856');
  290.     is(quick_test64([12345, 67890]), '17650027671492790999, 1218468377349889116, 7481073335483023155, 18104476594962223303');
  291.     is(quick_test64([9999 , 9999]) , '7871854434682127697, 8791668826882079131, 4042756844426893633, 14361836536518626214');
  292.  
  293.     # Seeds > 2**32
  294.     is(quick_test32([42862460907032573  , 519456495312580246])  , '319349001, 562730850, 2229409754, 561058538');
  295.     is(quick_test32([6120727489207695446, 7904312005358798897]) , '635930912, 2099303707, 1638577555, 1426136496');
  296.     is(quick_test32([4841811808465514507, 7141191103728083377]) , '1986408540, 4264878569, 3066617590, 731859269');
  297.  
  298.     is(quick_test64([42862460907032573  , 519456495312580246])  , '15573818271454563608, 11676002511341419670, 2091042206243276651, 3904012745602952106');
  299.     is(quick_test64([6120727489207695446, 7904312005358798897]) , '408103921297353512, 3309375775245630061, 17384267947741920157, 2626915900692044254');
  300.     is(quick_test64([4841811808465514507, 7141191103728083377]) , '9460770724321175617, 12493231060469799668, 934078138728949589, 16830977504107995527');
  301.  
  302.     done_testing();
  303.     exit(0);
  304. }
  305.  
  306. # vim: tabstop=4 shiftwidth=4 noexpandtab autoindent softtabstop=4