この記事のcommandパターンの実装を見て少し気になったので動的ディスパッチでどれくらい遅くなるかベンチマークを取って比較してみた。
なお例の通りでもよかったが、値を使用している感じ(データベースなら入力する値)と状態を持っている感じ(データベースの状態)を表したかったので要件を変えている。
具体的には数字を入れて決められた数を足すか、掛けるか、ゼロにするというコマンドを実装してみた。
実装法
トレイトオブジェクト
挙げられてた手法1
Fnのトレイトオブジェクトも同じなのでここに含めている
pub trait Command {
fn ops(&self, value: i32) -> i32;
}
pub struct AddOp(i32);
impl Command for AddOp {
fn ops(&self, value: i32) -> i32 {
self.0 + value
}
}
pub struct MulOp(i32);
impl Command for MulOp {
fn ops(&self, value: i32) -> i32 {
self.0 * value
}
}
pub struct ToZero;
impl Command for ToZero {
fn ops(&self, _value: i32) -> i32 {
0
}
}
関数ポインタ
挙げられてた手法2
トレイトオブジェクトより早いといわれていたが…。
状態が欲しかったので状態を表すフィールドとともに渡している。
pub struct FuncCommand(pub fn(i32, i32) -> i32, pub i32);
impl Command for FuncCommand {
fn ops(&self, value: i32) -> i32 {
(self.0)(self.1, value)
}
}
enum
操作が決まっているならenumを使うべきではないか?
pub enum CommandEnum {
Add(i32),
Mul(i32),
ToZero,
}
impl Command for CommandEnum {
fn ops(&self, value: i32) -> i32 {
match self {
CommandEnum::Add(i) => value + i,
CommandEnum::Mul(i) => value * i,
CommandEnum::ToZero => 0,
}
}
}
enum + トレイトオブジェクト
決まっている操作に拡張性を加える手法。この時ToZero
を拡張する操作とした。
pub enum CommandDynEnum {
Add(i32),
Mul(i32),
Dynamic(Box<dyn Command>),
}
impl Command for CommandDynEnum {
fn ops(&self, value: i32) -> i32 {
match self {
CommandDynEnum::Add(i) => value + i,
CommandDynEnum::Mul(i) => value * i,
CommandDynEnum::Dynamic(x) => x.ops(value),
}
}
}
ベンチマーク
ランダムにコマンドを発生させた。どのベンチでも同じコマンドが生成するようにしている。また生成はベンチに含んでいない。
Generate Commands
pub fn list_enum(seed: u64, length: usize) -> Vec<CommandEnum> {
let mut vec = Vec::with_capacity(length);
let mut rand = StdRng::seed_from_u64(seed);
let three = Uniform::from(0..=2);
let twelve = Uniform::from(0..=12);
for _ in 0..length {
match three.sample(&mut rand) {
0 => vec.push(CommandEnum::Add(twelve.sample(&mut rand))),
1 => vec.push(CommandEnum::Mul(twelve.sample(&mut rand))),
2 => vec.push(CommandEnum::ToZero),
_ => unreachable!(),
}
}
vec
}
pub fn list_enum_box(seed: u64, length: usize) -> Vec<CommandDynEnum> {
let mut vec = Vec::with_capacity(length);
let mut rand = StdRng::seed_from_u64(seed);
let three = Uniform::from(0..=2);
let twelve = Uniform::from(0..=12);
for _ in 0..length {
match three.sample(&mut rand) {
0 => vec.push(CommandDynEnum::Add(twelve.sample(&mut rand))),
1 => vec.push(CommandDynEnum::Mul(twelve.sample(&mut rand))),
2 => vec.push(CommandDynEnum::Dynamic(Box::new(ToZero))),
_ => unreachable!(),
}
}
vec
}
pub fn list_box_dyn(seed: u64, length: usize) -> Vec<Box<dyn Command>> {
let mut vec: Vec<Box<dyn Command>> = Vec::with_capacity(length);
let mut rand = StdRng::seed_from_u64(seed);
let three = Uniform::from(0..=2);
let twelve = Uniform::from(0..=12);
for _ in 0..length {
match three.sample(&mut rand) {
0 => vec.push(Box::new(AddOp(twelve.sample(&mut rand)))),
1 => vec.push(Box::new(MulOp(twelve.sample(&mut rand)))),
2 => vec.push(Box::new(ToZero)),
_ => unreachable!(),
}
}
vec
}
pub fn list_fn(seed: u64, length: usize) -> Vec<FuncCommand> {
let mut vec: Vec<FuncCommand> = Vec::with_capacity(length);
let mut rand = StdRng::seed_from_u64(seed);
let three = Uniform::from(0..=2);
let twelve = Uniform::from(0..=12);
for _ in 0..length {
match three.sample(&mut rand) {
0 => vec.push(FuncCommand(|x, y| x + y, twelve.sample(&mut rand))),
1 => vec.push(FuncCommand(|x, y| x * y, twelve.sample(&mut rand))),
2 => vec.push(FuncCommand(|_, _| 0, 0)),
_ => unreachable!(),
}
}
vec
}
Criterion
fn command_patterns(c: &mut Criterion) {
let mut g = c.benchmark_group("command_pattern");
let seed = 1234_u64;
g.bench_with_input("box_dyn", &seed, |b, seed| {
let vec = list_box_dyn(*seed, 1000);
b.iter(|| {
let mut s = 0;
for x in &vec {
s = x.ops(s);
}
black_box(s)
})
});
g.bench_with_input("enum_box", &seed, |b, seed| {
let vec = list_enum_box(*seed, 1000);
b.iter(|| {
let mut s = 0;
for x in &vec {
s = x.ops(s);
}
black_box(s)
})
});
g.bench_with_input("enum", &seed, |b, seed| {
let vec = list_enum(*seed, 1000);
b.iter(|| {
let mut s = 0;
for x in &vec {
s = x.ops(s);
}
black_box(s)
})
});
g.bench_with_input("fn", &seed, |b, seed| {
let vec = list_fn(*seed, 1000);
b.iter(|| {
let mut s = 0;
for x in &vec {
s = x.ops(s);
}
black_box(s)
})
});
}
criterion_group!(benches, command_patterns);
criterion_main!(benches);
トレイトオブジェクト | 4.6625 us | 4.7189 us | 4.7899 us |
関数ポインタ | 3.9561 us | 3.9816 us | 4.0092 us |
enum | 493.79 ns | 497.79 ns | 502.07 ns |
enum + トレイトオブジェクト | 1.3763 us | 1.4585 us | 1.5440 us |
というわけでenumが最速で桁が一つ違った。意外だったのは関数ポインタが遅かったことか。ここまで差がないならわざわざ関数ポインタを使う必要性を感じないがどうだろうか。
ところでなぜ『Rust Design Patterns』のコマンドパターンでenumの紹介がなかったのだろうか。それともこのパターンの使い方を誤解しているのだろうか。