Xorshiftによる擬似乱数生成

Last updated:

Introduction

擬似乱数を発生させるアルゴリズムには、線形合同法、Xorshift、メルセンヌ・ツイスタ、などいくつも有名なものがある。 その中でも操作が単純で高速なXorshiftについて、ざっくり何をやっているのかをMarsagliaによる論文[1,2]を参照しながら考えてみたい。 Xorshiftの実装自体は難しくないが、ここではアルゴリズムの背後にあるモチベーション的な部分を考えていこう。

バイナリ行列による擬似乱数の生成

擬似乱数を発生させる方法には様々なアプローチがあり得るが、大きな方向性として、 n次のバイナリβ\betaに対して、n×nn \times nバイナリ行列TTを作用させていき、βT, βT2, \beta T,~\beta T^2,~\cdotsという数列を作ることで、擬似乱数を発生させる方法を考えよう。

このとき、以下の主張は同値になる。

  1. ゼロでない任意のn次バイナリに対して、n×nn \times nバイナリ行列TTを作用させていくと、ゼロ以外のn次バイナリを全て作ってくれる。
  2. n×nn \times nバイナリ行列TTの周期は2n12^n-1

同値性に関して、元の論文[2]では固有多項式を用いたりして議論しているが、感覚的には自然に納得できると思う。

1 \Leftarrow 2 に関して:

TTの周期がk=2n1k = 2^n-1ということは、あるバイナリβ\betaに対してβTk=β\beta T^k= \betaで、当然それまで(βT, βT2, \beta T,~\beta T^2,~\cdots)にβ\betaは踏んでいない。 なのでその間、n次バイナリのとりうる元が2n2^n個あるうち、ゼロと初期バイナリを除いたすべての元を踏んでいかないといけないことが分かる(途中で同じものを踏むと、そこでループする)。

1 \Rightarrow 2 に関して:

行列TTを作用させると、あるバイナリβ\betaに対して、一意にバイナリβT\beta Tを与えるので、とりうるバイナリを全て発生させていくには、毎回異なるバイナリを発生させ続けなければならない。なので、2n12^n-1回の操作で元のバイナリに戻るという周期になるはずだ。

バイナリ行列

Xorshiftアルゴリズムの中身について議論する前に、バイナリ行列の性質を簡単に振り返っておこう。 まず最初の注意として、ここで扱うバイナリ行列とその演算は、ブール代数の定義とは異なる(Boolean行列の基礎については、Luceの論文[3]などを参照すると良い)。 基本的な演算(加算と乗算)は、(1)および(2)のように定義される。 ブール代数との違いは、加算がORではなくXORで定義されている点である。 今回の定義の下では、通常の実数/複素数行列の性質の多くがバイナリ行列の場合にも適用できる。

{0+0=00+1=11+0=11+1=0\begin{align} \left\{\begin{array}{l} 0 + 0 = 0\\ 0 + 1 = 1\\ 1 + 0 = 1\\ 1 + 1 = 0 \end{array}\right. \end{align} {00=001=010=011=1\begin{align} \left\{ \begin{array}{l} 0 \cdot 0 = 0\\ 0 \cdot 1 = 0\\ 1 \cdot 0 = 0\\ 1 \cdot 1 = 1 \end{array} \right. \end{align}

バイナリ行列の計算の順序は、基本的に普通の行列と同じで、そこに先ほどの加算と乗算のルールを適用していく。 例えば、3×33 \times 3のバイナリ行列AABBを(3)のように定義した場合、加算と乗算の演算は次のように表される。

A=[101010001],B=[001111010]\begin{align} A = \begin{bmatrix} 1 & 0 & 1 \\ 0 & 1 & 0 \\ 0 & 0 & 1 \end{bmatrix}, \quad B = \begin{bmatrix} 0 & 0 & 1 \\ 1 & 1 & 1 \\ 0 & 1 & 0 \end{bmatrix} \end{align} A+B=[100101011],AB=[011111010]\begin{align} A + B = \begin{bmatrix} 1 & 0 & 0 \\ 1 & 0 & 1 \\ 0 & 1 & 1 \end{bmatrix}, \quad AB = \begin{bmatrix} 0 & 1 & 1 \\ 1 & 1 & 1 \\ 0 & 1 & 0 \end{bmatrix} \end{align}

Xorshiftを使う理由

さて、周期2n12^n-1n×nn \times nバイナリ行列TTはどうやって見つければいいだろうか。 重要な特徴として、行列TTは正則でなければならない。これは以下のように考えることができる。

  • 行列TTは全てのnn次バイナリを生成できる必要があるので、nn次バイナリ全体の集合に関して全単射でなければならない。
  • 行列TTが全単射である ⇔ 行列TTには逆行列T1T^{-1}が存在する。

ただこれだけでは行列の形は絞れないし、実際、周期の条件を満たすような行列は大量に存在する。

むしろ方針としては、どのような行列の作り方をすれば行列計算のコストがかからないか、という視点から行列の候補を出していき、それらが周期の条件を満たすかチェックしていきたい。 そこで出てくるのがXorshiftという操作だ。

ビットシフトの操作はコンピュータ内で高速に実行可能な操作で、行列ではshift matrixとして表される。 例えば4×44 \times 4の行列では、左に1ビットシフトさせる行列L1L^1、右に1ビットシフトさせる行列R1R^1、はそれぞれ(5)のように表される。 行ベクトルをL1L^1に適用すると、右に1ビットシフトしたように見え、行ベクトルをR1R^1に適用すると、左に1ビットシフトしたように見える。 ここでは「左シフト」はMSB(最上位ビット)方向にビットをシフトし、「右シフト」はLSB(最下位ビット)方向にビットをシフトすること、という風に理解しておこう。

L1=[0100001000010000],R1=[0000100001000010]\begin{align} L^1 = \begin{bmatrix} 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 \\ \end{bmatrix}, \quad R^1 = \begin{bmatrix} 0 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ \end{bmatrix} \end{align}

これらは正則でないので、XOR(排他的論理和)の操作を加えることでT=I+LaT=I+L^aまたはT=I+RbT=I+R^bとすると、正則かつ計算コストが少ない操作を作ることが出来る。 この操作は行列計算を行わずにバイナリ演算として実行できる。32ビットの場合の、1ステップのXorshift操作を行うコード例を以下に示す。

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BinaryVector32 {
    row: u32,
}

impl BinaryVector32 {
    pub fn new() -> Self {
        Self { row: 0 }
    }

    pub fn xorshift_r(mut self, bits: usize) -> Self {
        self.row ^= self.row >> bits;
        self
    }

    pub fn xorshift_l(mut self, bits: usize) -> Self {
        self.row ^= self.row << bits;
        self
    }
}

正則行列の積は正則になるので、これらの行列を掛け合わせて行列TTの候補を作ることができる。 元の論文[1]によると、n=32n=32の場合、T=(I+La)(I+Rb)T=(I+L^a)(I+R^b)またはT=(I+Rb)(I+La)T=(I+R^b)(I+L^a)という形で、周期の条件を満たすものはなく、T=(I+La)(I+Rb)(I+Rc)T=(I+L^a)(I+R^b)(I+R^c)の形であれば、条件を満たす(a,b,c)(a, b, c)の組み合わせが多数存在するそうだ。

周期の条件を満たすパラメタの探索

では、周期の条件を満たすパラメータ(a,b,c)(a, b, c)を探してみよう。 32×3232 \times 32バイナリ行列TTの周期は23212^{32}-1でなければならない。 ただし、23212^{32}-1は以下のように因数分解できる。

2321=(216+1)(28+1)(24+1)(22+1)(221)=655372571753\begin{align} 2^{32} - 1 % &= (2^{16} + 1)(2^{16} - 1) \notag \\ % &= (2^{16} + 1)(2^8 + 1)(2^8 - 1) \notag \\ % &= (2^{16} + 1)(2^8 + 1)(2^4 + 1)(2^4 - 1) \notag \\ &= (2^{16} + 1)(2^8 + 1)(2^4 + 1)(2^2 + 1)(2^2 - 1) \notag \\ &= 65537 \cdot 257 \cdot 17 \cdot 5 \cdot 3 \end{align}

いま確認したいのは、行列TT23212^{32}-1回の乗算で単位行列IIになること、かつ、それより短い周期でIIにならないこと、の2点である。 これを言い換えると、以下のように表すことができる(条件(8)—(12)を確認すると、T3I, T5I, T^{3} \neq I,~ T^{5} \neq I,~ \cdots であることが確認できる)。

T2321=IorT232=TT(2321)/3IT(2321)/5IT(2321)/17IT(2321)/257IT(2321)/65537I\begin{align} &T^{2^{32}-1} = I \quad \text{or} \quad T^{2^{32}} = T \\ &T^{(2^{32}-1) / 3} \neq I \\ &T^{(2^{32}-1) / 5} \neq I \\ &T^{(2^{32}-1) / 17} \neq I \\ &T^{(2^{32}-1) / 257} \neq I \\ &T^{(2^{32}-1) / 65537} \neq I \end{align}

以下のRustコードは、32×3232 \times 32バイナリ行列を実装して、上記の条件を確認する。 バイナリ行列の累乗に関しては、繰り返し2乗法を用いて計算している。

/// A 32x32 binary matrix implemented using bitwise operations for efficiency
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BinaryMatrix32 {
    /// Each u32 represents a row of 32 binary values
    rows: [u32; 32],
}

impl BinaryMatrix32 {
    /// Create a new zero matrix
    pub fn new() -> Self {
        Self { rows: [0; 32] }
    }

    /// Create an identity matrix
    pub fn identity() -> Self {
        let mut matrix = Self::new();
        for i in 0..32 {
            matrix.rows[i] = 1 << i;
        }
        matrix
    }

    /// Get a specific bit at (row, col)
    pub fn get(&self, row: usize, col: usize) -> bool {
        if row >= 32 || col >= 32 {
            panic!("Index out of bounds: ({}, {})", row, col);
        }
        (self.rows[row] & (1 << col)) != 0
    }

    /// Set a specific bit at (row, col)
    pub fn set(&mut self, row: usize, col: usize, value: bool) {
        if row >= 32 || col >= 32 {
            panic!("Index out of bounds: ({}, {})", row, col);
        }
        if value {
            self.rows[row] |= 1 << col;
        } else {
            self.rows[row] &= !(1 << col);
        }
    }

    /// Binary matrix addition
    pub fn add(&self, other: &Self) -> Self {
        let mut result = Self::new();
        for i in 0..32 {
            result.rows[i] = self.rows[i] ^ other.rows[i];
        }
        result
    }

    /// Create a shift matrix for right shift by `bits` positions
    /// Example for 3 x 3 matrix with bits = 1
    ///                              |0, 0, 0|
    /// [v1, v2, 0] = [v0, v1, v2] * |1, 0, 0|
    ///                              |0, 1, 0|
    pub fn shift_right(bits: usize) -> Self {
        let mut matrix = Self::new();
        if bits >= 32 || bits == 0 {
            return matrix;
        }

        for i in 0..(32 - bits) {
            matrix.rows[i + bits] = 1 << i;
        }
        matrix
    }

    /// Create a shift matrix for left shift by `bits` positions
    /// Example for 3 x 3 matrix with bits = 1
    ///                              |0, 1, 0|
    /// [0, v0, v1] = [v0, v1, v2] * |0, 0, 1|
    ///                              |0, 0, 0|
    pub fn shift_left(bits: usize) -> Self {
        let mut matrix = Self::new();
        if bits >= 32 || bits == 0 {
            return matrix;
        }

        for i in bits..32 {
            matrix.rows[i - bits] = 1 << i;
        }
        matrix
    }

    /// Binary matrix multiplication
    pub fn mul(&self, other: &Self) -> Self {
        let mut result = Self::new();
        for i in 0..32 {
            for j in 0..32 {
                let mut sum = false;
                for k in 0..32 {
                    sum ^= self.get(i, k) && other.get(k, j);
                }
                if sum {
                    result.rows[i] |= 1 << j;
                }
            }
        }
        result
    }

    pub fn pow(&self, n: u64) -> Self {
        if n == 0 {
            return Self::identity(); // T^0 = I
        }
        if n == 1 {
            return *self; // T^1 = T
        }

        // binary exponentation in iterative form
        let mut base = *self;
        let mut result = Self::identity();
        let mut exp = n;

        while exp > 0 {
            if exp & 1 == 1 {               // if LSB is odd, multiply base
                result = result.mul(&base);
            }
            base = base.mul(&base);         // calculate next base
            exp >>= 1;                      // shift right by 1
        }
        result
    }

    /// Comprehensive check for correct xorshift period (2^32 - 1)
    /// Verifies that T^(2^32) = T and that T doesn't have smaller periods
    pub fn has_correct_full_period(&self) -> bool {
        // First check: T^(2^32) = T
        let matrix_pow_2_32 = self.pow(2_u64.pow(32));
        if *self != matrix_pow_2_32 {
            return false;
        }

        let identity = Self::identity();

        // Check that no smaller periods exist by testing T^k != I for proper divisors k of (2^32-1)
        // 2^32 - 1 = 3 × 5 × 17 × 257 × 65537, so check each quotient
        let quotients = [
            (2_u64.pow(32) - 1) / 3,     // Missing factor 3
            (2_u64.pow(32) - 1) / 5,     // Missing factor 5
            (2_u64.pow(32) - 1) / 17,    // Missing factor 17
            (2_u64.pow(32) - 1) / 257,   // Missing factor 257
            (2_u64.pow(32) - 1) / 65537, // Missing factor 65537
        ];

        !quotients.iter().any(|&quotient| self.pow(quotient) == identity)
    }

    /// Test if a triplet (a, b, c) creates a valid xorshift matrix
    pub fn test_xorshift_triplet(a: usize, b: usize, c: usize) -> bool {
        let identity = Self::identity();
        let il_a = identity.add(&Self::shift_left(a));
        let ir_b = identity.add(&Self::shift_right(b));
        let il_c = identity.add(&Self::shift_left(c));

        let xorshift = il_a.mul(&ir_b).mul(&il_c);
        xorshift.has_correct_full_period()
    }

    /// Search for all valid xorshift parameter triplets in the range 1..32
    pub fn search_valid_triplets() -> Vec<(usize, usize, usize)> {
        let mut valid_triplets = Vec::new();
        let mut tested = 0;
        const TOTAL: usize = 15376; // Total cases (a, b, c) with a <= c

        for a in 1..32 {
            for b in 1..32 {
                for c in a..32 {
                    tested += 1;

                    if tested % 1000 == 0 {
                        println!("Progress: {}/{} ({:.1}%)", tested, TOTAL,
                               (tested as f64 / TOTAL as f64) * 100.0);
                    }

                    if Self::test_xorshift_triplet(a, b, c) {
                        valid_triplets.push((a, b, c));
                    }
                }
            }
        }

        valid_triplets
    }
}

このコードを実行すると、周期の条件を満たすパラメータ(a,b,c)(a, b, c)の組み合わせが見つけられる。 実際に結果を表示してみると、元の論文[1]と一致することが確認できる。

Found 81 valid triplets:
| 1, 3,10| 1, 5,16| 1, 5,19| 1, 9,29| 1,11, 6| 1,11,16| 1,19, 3| 1,21,20| 1,27,27|
| 2, 5,15| 2, 5,21| 2, 7, 7| 2, 7, 9| 2, 7,25| 2, 9,15| 2,15,17| 2,15,25| 2,21, 9|
| 3, 1,14| 3, 3,26| 3, 3,28| 3, 3,29| 3, 5,20| 3, 5,22| 3, 5,25| 3, 7,29| 3,13, 7|
| 3,23,25| 3,25,24| 3,27,11| 4, 3,17| 4, 3,27| 4, 5,15| 5, 3,21| 5, 7,22| 5, 9, 7|
| 5, 9,28| 5, 9,31| 5,13, 6| 5,15,17| 5,17,13| 5,21,12| 5,27, 8| 5,27,21| 5,27,25|
| 5,27,28| 6, 1,11| 6, 3,17| 6,17, 9| 6,21, 7| 6,21,13| 7, 1, 9| 7, 1,18| 7, 1,25|
| 7,13,25| 7,17,21| 7,25,12| 7,25,20| 8, 7,23| 8, 9,23| 9, 5,14| 9, 5,25| 9,11,19|
| 9,21,16|10, 9,21|10, 9,25|11, 7,12|11, 7,16|11,17,13|11,21,13|12, 9,23|13, 3,17|
|13, 3,27|13, 5,19|13,17,15|14, 1,15|14,13,15|15, 1,29|17,15,20|17,15,23|17,15,26|

Reference

  1. Marsaglia, G. (2003). Xorshift RNGs. Journal of Statistical Software, 8(14), 1–6. doi: 10.1109/IEEESTD.2019.8766229.
  2. George Marsaglia, Liang-Huei Tsay, Matrices and the structure of random number sequences, Linear Algebra and its Applications, Volume 67, 1985, Pages 147-156, ISSN 0024-3795, doi: 10.1016/0024-3795(85)90192-2.
  3. Luce, R. Duncan. “A note on Boolean matrix theory.” Proceedings of the American Mathematical Society 3.3, 1952, 382-388, doi: 10.2307/2031888.

関連記事

浮動小数点数の丸め誤差と誤差伝搬

浮動小数点数を使った数値計算では、必ず丸め誤差が発生します。特に複数回演算操作を行った後、誤差がどう伝搬するかはそれほど明らかではありません。この記事では、IEEE 754-2019をもとに浮動小数点数の定義を確認し、誤差推定の基本的な方法について議論します。

いろいろな形状の表面に点を一様分布させる

輻射熱伝達をレイトレーシングによって評価する場合、物体表面から一様に光線を発生させる必要があります。今回は、長方形・三角形・円板・球面・円柱・円錐・放物面などの基本的な表面形状について、0–1の範囲のランダム値からどうやって一様分布を発生させるかを解説します。

ルンゲクッタ法(RK4)の精度確認

常微分方程式の時間発展を計算する際に、手軽に用いられる手法にルンゲクッタ法(RK4)があります。この記事では、ルンゲクッタ法が4次精度を達成していることを確認します。