Rust で no_std で乱数生成する (速く)

Rust で no_std で乱数生成する (速く)

ふとしです。

この記事では rand クレートの SmallRng とベンチマークを紹介します。

まえおき

最近はずっと Rust で組み込みプログラミングをやっています。

組み込み Rust では基本 no_std 環境下でコードを書くため、世に転がるスニペットをそのまま使えない局面があります。乱数生成用の代表的なクレート rand の使用についてもそうです。

そこで no_std 環境下で rand を使う手順を調べました。

よくあるスニペット

Generate Random Values - Rust Cookbook に従い書くと no_std な環境下では以下のような結果が得られます。

#![no_std]

use rand::Rng;

#[test]
fn test() {
    let mut rng = rand::thread_rng();
    let n1 = rng.gen::<u8>();
}
8 | extern crate std;
  | ^^^^^^^^^^^^^^^^^ can't find crate

(ちなみに単に #![no_std] しただけだと rand クレートが std を連れてきます。一見テストなどは通るが std を備えない target に対してのビルドでなにもかもおわりになるので気をつけましょう)

no_std で rand クレートを使う

rand クレートでは std 使用がデフォルトとなっているので、まず README.md に従って Cargo.toml を編集します。

Rand supports limited functionality in no_std mode (enabled via default-features = false).

[dependencies]
rand = { version = "0.7.3", default-features = false }

上記の README.md からの続き

In this case, OsRng and from_entropy are unavailable (unless getrandom is enabled), large parts of seq are unavailable (unless alloc is enabled), and thread_rng and random are unavailable.

に書いてあるとおり thread_rng は使えないので、最初の例のスニペットは依然としてコンパイルエラーになります。

5 |     let mut rng = rand::thread_rng();
  |                         ^^^^^^^^^^ not found in `rand`

乱数生成器を直接作成する

rand::thread_rng は乱数生成器を作成する関数なので、その替わりに自分で rand クレートが提供する乱数生成器を直接作成します。

まず、スタンダードという名前から代表的なものであろうと推測した StdRng を作成しました。

StdRng は暗号論的に十分セキュアな乱数生成器です。

#[test]
fn test() {
    let mut rng = rand::rngs::StdRng::from_seed([0; 32]);

    assert_eq!(118, rng.gen::<u8>());
    assert_eq!(160, rng.gen::<u8>());
    assert_eq!(64, rng.gen::<u8>());
}

乱数が生成できました。

(セキュアである・セキュアでないにかかわらず、シードが同じだとクレート内のアルゴリズムが変わらない限り同じ実行環境では同じ乱数が生成されるので、シードが同じテストは常に通ります。実用では注意しましょう)

より速い乱数生成器を直接作成する

ところで今回乱数生成したかったのは暗号のためではなく単純なゲームのためです。できるだけ速い生成器を使いたいと思いました。

そこでおなじく rand クレートが提供する SmallRng を使います。

SmallRng はセキュアでなくてもかまわない局面で高速に乱数を生成したい用途に向いています。

導入

セキュアでないためかどうかはわかりませんが SmallRng を使うには明示的に features に指定する必要があります。

[dependencies]
rand = { version = "0.7.3", default_features = false, features = ["small_rng"] }

あとは同じように作成し、同じように使うだけです。

#[test]
fn test_small() {
    let mut rng = rand::rngs::SmallRng::from_seed([0; 16]);

    assert_eq!(171, rng.gen::<u8>());
    assert_eq!(213, rng.gen::<u8>());
    assert_eq!(13, rng.gen::<u8>());
}

ベンチマーク

セキュアであることを諦めた場合にわたしたちが得るのはどれぐらいの時間でしょうか。10,000 個の u8 を生成するコードで測定しました。

test bench_small_rng ... bench:         942 ns/iter (+/- 12)
test bench_std_rng   ... bench:      32,668 ns/iter (+/- 445)

結果を見る限り、特に資源が限られる組み込みでは、セキュアでなくてかまわない局面では積極的に SmallRng を使用したほうがよさそうです。

セキュアでない乱数生成器をまちがって使わないために

rand クレートでは乱数生成器に Rng という Trait を実装しています。くわえて、セキュアとされている乱数生成器に CryptoRng というマーカー Trait が実装されています。

このマーカー Trait を Rng とあわせて要求することで、乱数生成器の機能を求める局面でセキュアな乱数生成器の使用を強制できます。

fn work<T: rand::Rng>(rng: &mut T) {
    //
}

fn work_secure<T: rand::Rng + rand::CryptoRng>(rng: &mut T) {
    //
}

このようにしておくと、まちがった場合はコンパイルタイムに怒られます。

#[test]
fn test_sec() {
    let mut std = rand::rngs::StdRng::from_seed([0; 32]);
    let mut small = rand::rngs::SmallRng::from_seed([0; 16]);

    work(&mut std);
    work(&mut small);

    work_secure(&mut std);
    work_secure(&mut small);
}
66 |     work_secure(&mut small);
   |                 ^^^^^^^^^^ the trait `rand::CryptoRng` is not implemented for `rand::prelude::SmallRng`

まとめ

no_std 環境下でも rand クレートにより乱数が生成できること、StdRng より SmallRng を使うとずいぶん速く乱数が生成できることがわかりました。

また Trait によって不適切な生成器を使ってしまうことを防止できることもわかりました。

Rust が提供する機能を活用して、しっかり安全に使っていきたいですね。