真似び日記
XorshiftのJavaScript版のTypedArray版を書いて、乱数を100000回生成するまでの時間を比較したところ、どちらも20 millisecondsで処理が完了した。昔は「JavaScriptのNumber型に対するbitwise演算」は避けるべきとされていたようだが(入り口と出口にtoInt32
かtoUint32
が挟まるので遅かったらしい)、現代の実行環境は問題なく最適化してくれるようだ(あるいはTypedArrayも遅い?)。
(TypedArray版:)
const Random = function (seed = 88675123) {
this.x = Uint32Array.of(123456789, 362436069, 521288629, seed, 0);
};
Random.prototype.next = function () { // Xorshift128
this.x[4] = this.x[0] ^ (this.x[0] << 11);
this.x.copyWithin(0, 1, 4);
this.x[3] = (this.x[3] ^ (this.x[3] >>> 19)) ^ (this.x[4] ^ (this.x[4] >>> 8));
return this.x[3];
};
Random.prototype.nextInt = function (min, max) { // from min to max
return min + (Math.abs(this.next()) % (max + 1 - min));
};
Xorshift128、「行列内容を全部0
にするのは厳禁」だが、それ以外なら何でもいいらしい。そこは変更する理由はないのでseedはそのままにした。計算途中の一時的な値もTypedArrayの外に出した時点でtoInt32
が適用されてしまうらしいので、元の一時変数t
はTypedArray要素に置き換えた。
TypedArray版の実行結果はNumber版とは微妙に異なる(C言語版と同じ結果になると思われる)。どちらも2進数表現としては等価だが、JavaScriptのNumber型に対するbitwise演算は符号付き32-bit整数にcastingされる。既定値のseedで乱数を10回生成した結果は以下の通り:
const random = new Random(); // seed: default parameters (= 88675123)
random.next(); // 0b11011100101000110100010111101010 // -593279510 or 3701687786
random.next(); // 0b00011011010100010001011011100110 // 458299110
random.next(); // 0b10010101000100000100100110101010 // -1794094678 or 2500872618
random.next(); // 0b11011000100011010000000010110000 // -661847888 or 3633119408
random.next(); // 0b00011110110001111000001001011110 // 516391518
random.next(); // 0b10001101101100100100000101000110 // -1917697722 or 2377269574
random.next(); // 0b10011010111110000001010001000011 // -1695017917 or 2599949379
random.next(); // 0b00101010110000000000111100101100 // 717229868
random.next(); // 0b00001000001101111010110101011000 // 137866584
random.next(); // 0b00010111100100000110010101101001 // 395339113
このようにNumber版には負数が混じるため、絶対値を得るnextInt(min, max)
では結果が変わる。
// random.nextInt(1, 1024) x 30:
// Number: [535, 743, 599, 849, 607, 699, 958, 813, 345, 362, 469, 550, 273, 746, 653, 812, 53, 535, 333, 903, 169, 1013, 173, 601, 866, 922, 606, 737, 795, 916, 358]
// TypedArray: [491, 743, 427, 177, 607, 327, 68, 813, 345, 362, 469, 550, 753, 746, 373, 214, 53, 535, 333, 903, 169, 13, 173, 425, 160, 922, 606, 737, 795, 916, 358]
確率分布としてはどちらも大差なさそうに見えるが、よくわからない。“オートメーション工場”の一部の分岐のように「#RANDOM 1024
、ただし対応する枝は#IF 16
まで」みたいな場合は困るかも。いや、剰余を使うならべつに問題ないんじゃないかな…… ん〜〜まあ無難にC言語版に寄せておくか。
Linterがbitwise演算子に目くじらを立てるので、私は「XorもShiftも使わないXorshift」も書いてみた。Number型を32-bit整数様に整形し、それを"0"
と"1"
が並ぶ長さ32の文字列に変換し、それを一文字ずつ操作して書き戻す愚鈍な実装だ。100000回の乱数生成に4318 msを要した。速度面は元の版に及ぶべくもない(ゆえに実用性もない)が、Number型に対するbitwise演算の動きは理解できた。
(以下のように使えた:)
bitwise.not(-1234567.89); // 1234566
bitwise.and(-1234, -567.89); // -1784
bitwise.or(-1234, -567.89); // -17
bitwise.xor(-1234, -567.89); // 1767
bitwise.leftShift(-1234, -567.89); // -631808
bitwise.rightShift(-1234, -567.89); // -3
bitwise.unsignedRightShift(-1234, -567.89); // 8388605
文字列を使わず数値型のままbitmaskとやらを行えば、たぶん10倍以上高速化できそうな気がするが、実用性が皆無とわかりきっているもどきにこれ以上手を入れるのはさすがに不毛かな〜
Linterがbitwise演算子を戒める理由は、論理演算子の書き間違いに見えるかららしい。未来の私は~~-567.89
や-567.89|0
などを読めないに違いないと思うので、私自身はこういう書き方は避けたいが、他人が読むわけでもないcodeなら自信ニキは好きに書けばいいのでは。あとXorshiftのようにbitwise演算が必須なalgorithmであれば、linterが何といおうと普通にbitwise演算子を使うべきだろう。