Rust入門:基本構文の確認と繰り返し処理のパフォーマンス検証
今回の記事ではRust言語に取り組んでみます。
■開発環境の用意
詳細は検索すれば出てきますので省きますが、
必要なものとしては以下の2点でした。
1.Microsoft C++ Build Toolsのインストール
2.Rustのインストール
■Hello worldを作ってみる
やはり、はじめての言語に触るときは「hello world」が基本です。
hello.rsファイルを作成し、下記のコードを実装しました。
fn main()
{
println!("Hello world!!");
}
このファイルをコマンドプロンプトからコンパイルして実行!
(今回はcargoを使わず、コンパイラのrustcを直接使用していきます)
#rustc -C opt-level=3 hello.rs
#hello.exe
#Hello world!!
簡単ですね。
■電卓の作成
次は電卓を作成してみましょう。
これも定番です。
(生成AIの助けを借りてコーディングしてみました)
calc.rsファイルを用意して、入力、チェック、計算のロジックを実装します。
use std::io;
fn main() {
// 1. 数値の入力
let num1 = match get_number("数値を入力してください: ") {
Ok(n) => n,
Err(e) => {
eprintln!("入力エラー: {}", e);
return;
}
};
// 2. 演算子の入力
let operator = match get_operator("演算子 (+, -, *, /) を入力してください: ") {
Ok(op) => op,
Err(e) => {
eprintln!("入力エラー: {}", e);
return;
}
};
// 3. 2番目の数値の入力
let num2 = match get_number("2番目の数値を入力してください: ") {
Ok(n) => n,
Err(e) => {
eprintln!("入力エラー: {}", e);
return;
}
};
// 4. 計算の実行と結果の表示
match calculate(num1, operator, num2) {
Ok(result) => println!("結果: {} {} {} = {}", num1, operator, num2, result),
Err(e) => eprintln!("計算エラー: {}", e),
}
}
// ユーザーから数値を読み取り、f64にパースするヘルパー関数
fn get_number(prompt: &str) -> Result<f64, String> {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| e.to_string())?;
input.trim().parse::<f64>().map_err(|_| "有効な数値を入力してください".to_string())
}
// 演算子を読み取り、検証
fn get_operator(prompt: &str) -> Result<char, String> {
println!("{}", prompt);
let mut input = String::new();
io::stdin().read_line(&mut input).map_err(|e| e.to_string())?;
let op = input.trim().chars().next().ok_or("演算子を入力してください".to_string())?;
match op {
'+' | '-' | '*' | '/' => Ok(op),
_ => Err("無効な演算子です。+, -, *, / のいずれかを使用してください".to_string()),
}
}
// 計算ロジック
fn calculate(num1: f64, operator: char, num2: f64) -> Result<f64, String> {
match operator {
'+' => Ok(num1 + num2),
'-' => Ok(num1 - num2),
'*' => Ok(num1 * num2),
'/' => {
if num2 == 0.0 {
Err("ゼロによる除算はできません".to_string())
} else {
Ok(num1 / num2)
}
}
// get_operator関数で検証済みのため、ここは基本的に到達しない
_ => Err("予期せぬ演算子エラー".to_string()),
}
}
このファイルをコンパイルして実行します。
#rustc -C opt-level=3 calc.rs
#calc.exe
#数値を入力してください:
#10
#演算子 (+, -, *, /) を入力してください:
#*
#2番目の数値を入力してください:
#128
#結果: 10 * 128 = 1280
計算処理などは生成AIに聞いて作成してみましたが
ちゃんと動きました。
すごい時代になったものです。
■繰り返し文のパフォーマンスの確認
新しい言語を触るときは必ずと言っていいほど、
繰り返しや分岐などのパフォーマンスを確認しています。
では今回は繰り返しの簡易的な速度確認処理を作成してみます。
※black_boxを使用してRustの最適化を抑制した処理を作成しますが、
black_boxによる最適化の抑制はbest-effort(ベストエフォート)とのことなので、
速度測定の結果はある程度の参考値としてお考えください。
loops.rsファイルを用意して、
「for」「loop」「while」「while let」の
4種類の繰り返し文を時間計測するためのコードを実装します。
use std::{io, time};
use std::hint::black_box;
fn main()
{
println!("Loopする繰り返し数を入力してください:");
let mut input_count = String::new();
io::stdin().read_line(&mut input_count).expect("入力エラー");
let counts: u64 = input_count.trim().parse().expect("数値を入力してください");
println!("{}回繰り返します!", input_count.trim());
println!("for");
// forの経過時間を調査
let now = time::Instant::now();
for i in 1..=counts {
black_box(i);
}
// 経過時間を表示
println!("経過時間{:?}", now.elapsed());
// loopの経過時間を調査
let now = time::Instant::now();
let mut loop_cnt = 0;
println!("loop");
loop {
if loop_cnt == counts {
break;
}
loop_cnt += 1;
black_box(loop_cnt);
};
// 経過時間を表示
println!("経過時間{:?}", now.elapsed());
// whileの経過時間を調査
let now = time::Instant::now();
let mut loop_cnt = 0;
println!("while");
while loop_cnt != counts{
loop_cnt += 1;
black_box(loop_cnt);
};
// 経過時間を表示
println!("経過時間{:?}", now.elapsed());
// while letの経過時間を調査
let now = time::Instant::now();
let mut loop_cnt = Some(1);
println!("while let");
while let Some(i) = loop_cnt{
if i == counts {
loop_cnt = None;
}else{
loop_cnt = Some(i + 1);
}
black_box(i);
}
// 経過時間を表示
println!("経過時間{:?}", now.elapsed());
}
これもコンパイルして実行してみます。
(処理速度は12回計測し、最速値と最遅値を排除して平均化しています)
とりあえず10万回繰り返すように指定します。
#rustc -C opt-level=3 loops.rs
#loops.exe
#Loopする繰り返し数を入力してください:
#100000
#100000回繰り返します!
#for
#経過時間106.8μs
#loop
#経過時間207.9μs
#while
#経過時間220.5μs
#while let
#経過時間201μs
繰り返し回数には10万回を入力して何回かテストしてみましたが、
「for」が平均的に速いという結果となりました。
次点では「while let」でしたが「while」「loop」と大きな差はありませんでした。
「while」「loop」が平均的に遅かったのは意外でしたが、
特段遅いという感じではありませんでした。
しかし、繰り返し回数を100万回に増やして検証すると下のような結果となりました。
(処理速度は12回計測し、最速値と最遅値を排除して平均化しています)
#for
#経過時間1156.2μs
#loop
#経過時間663.7μs
#while
#経過時間646.9μs
#while let
#経過時間600.5μs
ここで「for」は極端に遅くなりました。
「while let」、「while」、「loop」は10万回の時と同じような結果でしたが、
速度的には十分速く「for」よりは随分良い結果となりました。
(この結果は私の環境の結果です、全ての環境で同じ結果ということではありません)
「for」が遅くなった原因として、
black_boxの使用によるオーバーヘッドや、テストの実行順(「for」が常に最初)
1..=counts(Inclusive Range)の構造による最適化の困難さが原因ではないかと検討を付けましたが、
今回の処理速度計測の性質上、これらは避けられない制約と考え、
そのまま計測を進めることにしました。
ということで、単純なパフォーマンスの比較として
100万回を超えるようなケースでは、
書き方によってパフォーマンスに顕著な差が出るという興味深い結果が得られました。
■パフォーマンスの検証結果
今回の私の環境での検証では、
回数が増えるにつれて「while let」が優位という意外な結果になりました。
Rustで「for」はゼロコスト抽象化で一般的に効率的と言われています。
しかし、極限までパフォーマンスを求めるシーンでは、
実際の処理内容に近い形で計測して、
最適な繰り返し文を選択する重要性を実感しました。
ただ、勘違いしてはいけないのは、平均的に速かったからといって
常に「while let」を使えばよいということではありません。
それぞれの繰り返し文には、ちゃんと特徴があります。
それぞれの特徴を踏まえ、適材適所を意識して使う必要があります。
| 構文 | 特徴 |
|---|---|
| for | 配列、ベクタなどの要素や範囲に対して簡潔に処理を繰り返す |
| loop | 明示的に終了(break)させるまで無限に処理を繰り返す |
| while | 指定した条件式が真(true)である間処理を繰り返す |
| while let | パターンマッチが成功し続ける限り処理を繰り返す |
■最後に
最近は生成AIを使うことで新しい言語に挑戦するハードルは低くなっていると感じます。
ただ、危惧しているところでもあります。
生成AIを使うことで誰でも簡単にコーディングできるようになってきているのですが、
その生成AIの吐き出したコードが最適なのかについては
まだ技術者の力に頼らざるを得ないでしょう。
個人的な感想なのですが、
これからの技術者に求められるのは、
「最適を見極められるレビュー力」
なのかもしれません。
■参考情報
・ベンチマーク環境
プロセッサ:Intel(R) Core(TM) Ultra 7 155H (3.80 GHz)
実装 RAM :16.0 GB (15.5 GB 使用可能)
OS :Windows 11 Pro
・繰り返し文のパフォーマンスの確認の実行結果
処理速度は連続で12回計測し、最速値と最遅値を排除し、下記のように平均化した値を計測値として使用しています。
