Rustに関するチラ裏
まえがき
Markdownの練習も兼ねてなんか適当に
※僕はIT系未経験です AtCoder以外ろくにやってません
どうでもいい詳細
N88BASIC, MASM, C, C++, C#, JSあたりちょっとだけかじったけど、もはやRust以外全然書けない
色々IT系の知識食い散らかしたので微妙に知識はあるのかもしれない
作ったことあるプログラムで一番でかいのは、昔N88BASICで作った高密度300行くらいのクソゲー
あと某MMO用のしょーもないツール作ってました
適当なチラ裏なのでウソ・大げさ・まぎらわしい、が含まれている可能性があります
若干競プロ的なネタが多くなるかも
とりあえずしょーもない小ネタ
std::mem::drop
相当のショートコード
{[foo];} // std::mem::drop(foo); と同じ効果のはず
実例(Rust Playground)
コードゴルフで使える可能性はあるかもしれないけどコードゴルフでdrop
なんてしなさそう
C++とRustのコンテナ対応表的な何か
RustはVec
以外std::collections
名称 | C++ | Rust | 備考 |
---|---|---|---|
動的配列 | vector | Vec | |
スタック | stack | Vecを利用 | C++はvectorでも可 |
キュー | queue | VecDequeを利用 | C++はdequeでも可 |
両端キュー | deque | VecDeque | 添字でランダムアクセス可 |
優先度付きキュー | priority_queue | BinaryHeap | 名前から想像しづらいかも |
双方向連結リスト | list | LinkedList | |
片方向連結リスト | forward_list | なし | まあいらんやろ |
順序なしマップ/セット | unordered_map unordered_set |
HashMap HashSet |
|
順序付きマップ/セット | map set |
BTreeMap BTreeSet |
|
順序なし多重マップ/セット | unordered_multimap unordered_multiset |
なし | |
順序付き多重マップ/セット | multimap multiset |
なし | 超欲しい><(競プロ的に) |
ムーブセマンティクスはメモリ関連以外にもメリットあるよってお話
Rustのムーブセマンティクスは、ビット単位のコピー(memcpy
)&ムーブ元のdrop
無効化で実現されている
他言語の用語で適当に例えると、ムーブ元からムーブ先にシャローコピーをして、かつムーブ元のデストラクタを無効化って感じだと思います、知らんけど
これのなにがうれしいかというと、変数の初期化、代入の際のムーブで例外(panic
)が発生し得ないということ
ビット単位のコピーなので例外が発生しない(これ常に真なのか知らんけど)
これは、変数の初期化、代入時にコンストラクタ、代入演算子で例外が発生し得るC++とは対照的
一部で有名な話だけど、例えばC++のstd::stack
では、一番上の要素を取り出す際に、
-
top()
で一番上の要素を見て(参照を得て)、コピー/ムーブをする -
pop()
で一番上の要素を削除する(要素を取り出せない)
という2つの手順を踏む必要がある
pop()
のみで一番上の要素を返す&スタックから削除という操作をまとめて行おうとすると要素返すところで例外投げられたときに不整合が起きるとかそんなかんじになって困るので、例外安全のため上記のようにtop()
とpop()
とで分かれた実装となっている
一方RustのVec
は、pop()
のみで要素が返ってきて末尾の要素が削除されるけど、要素返すのはムーブなので例外が発生しないからセーフ、って感じ
std::mem::drop
相当のショートコード改
こっちのが短かったわ(´・ω・`)
||foo; //警告でるけどstd::mem::drop(foo); と同じ効果
Option
がSome
のときだけ中身をVec
に追加する方法
vec.extend(option);
なんと1行で済んじゃう
実例(Rust Playground)
extend
メソッドがIntoIter
を引数にとる、Option
がIntoIter
トレイト持ってる、ってことでこういうことができるんですねー
ちなみにextend
メソッドはExtend
トレイトのメソッドで、Vec
はExtend
トレイトを持っています
Vec
モジュールの説明の結構下のほうにあって初見じゃわかりにくい
同一性のチェックについて
Rustでは==
演算子は等価性(同値性ともいう)をチェックする(同一性のチェックではない)
では、Rustで2つの値fooとbarの同一性はどのようにチェックすればよいか
std::ptr::eq(&foo, &bar); // &foo == &bar だと参照先同士の等価性チェックになっちゃう
ポインタに関するモジュールであるstd::ptr
の関数を使うってのは知らないと発想難しそう
std::ptr::eq
はポインタ型を引数にとるけど、参照からポインタへ型強制できるので参照も突っ込める、って塩梅
超微妙な実例(Rust Playground)
RustとC++ってメモリリークあんまり変わんなくね?ってお話
「RustはC++よりメモリリークを防げる!」みたいな発言とかをたまによく見る
でもそれはなんか違うんじゃないかなぁーってお話
以下、名前空間は適当に省略します
そもそもC++ってなんでメモリリーク起きるんですか?!
new
したものをdelete
しないから
なのでそもそもnew
しなければメモリリークはきっと防げる
new
しないってヒープ領域に値作りたいときどうすりゃいいのさ
でも
unique_ptr
基本は基本的にはmake_unique
関数でスマートポインタunique_ptr
を作ればよい
make_unique
が勝手にnew
してヒープ領域に値を作り、その値を指すunique_ptr
を返してくれる
unique_ptr
はスコープ抜けたときにデストラクタが勝手にdelete
してくれる
shared_ptr
共有するときは2以上の変数が1つの値を共有するときはunique_ptr
では無理
そういうときはmake_shared
関数でスマートポインタshared_ptr
を作る
共有する変数がすべてなくなったときにデストラクタが勝手にdelete
してくれる
ただ、循環参照があると永久に共有する変数がなくならないのでリークしちゃう
いずれにしても自分でnew
しないのでdelete
を忘れるということもない
で、Rustとあんまり変わんないのってなんで?
リーク防止という点ではnew
しないC++と同程度のものしか用意されていないから
unique_ptr
相当のものとしてBox
、shared_ptr
相当のものとしてRc
あるいはArc
があるけど、これ以上のものはないんじゃないかなあ、ないような気がする、たぶん
というかこれ以上となるとGCしかないような気がする
new
したいとき多分あるじゃん、Rustでもなんとかならん?
どうしても自分でnew
するってことは、自分でヒープ確保・開放のタイミングをコントロールするってこと
Rustの場合alloc::alloc
するとか変化球でBox::new
してからBox::leak
とかでnew
相当のことはできるけど自分でdealloc
とかしなきゃいけないんでたぶんC++といっしょ
まとめ
RustでもC++でもメモリリークに関しては大して変わらん、たぶん
おまけ
「じゃあRust安全じゃないじゃん!」とお嘆きのあなたへ
Rustではメモリリークは「安全」なんですねぇ
そもそもunsafeじゃないのに意図的にリークさせるメソッドとかもある(Box::leak
, forget
など)
詳細はRustnomiconさんの「安全と危険のご紹介」を
RustはC++に比べて何を防いでくれるのか?
所有権・参照関連でのお話
一般的な話じゃなくて例示です
1. ムーブした変数をうっかり使っちゃうのを防げる
例えばC++の場合、以下のようにムーブ済みの変数を間違って使っちゃうことがありうる
std::vector<int> v1 {1, 3, 5};
auto v2 = std::move(v1);
std::cout << v1[0] << std::endl; // v2と間違えてムーブ済みのv1にしちゃった!
実例(Wandbox)
この例ではセグフォる
Rustの場合ムーブ済みの変数を使おうとするとコンパイルエラーになる、やったね!
let v1 = vec![1, 3, 5];
let v2 = v1; // VecはCopyトレイトないのでムーブになる
println!("{}", v1[0]); // v2と間違えた!
2 | let v1 = vec![1, 3, 5];
| -- move occurs because `v1` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | let v2 = v1; // VecはCopyトレイトないのでムーブになる
| -- value moved here
4 | println!("{}", v1[0]); // v2と間違えた!
| ^^ value borrowed here after move
2. 変数の変更により無効になった参照を使っちゃうのを防げる
「参照」って言ったけどC++ではポインタを使って説明する
コードの見た目がRustの参照と揃うので
例えばC++のvector
のpush_back
メソッドは、要素の追加によりsize
がcapacity
を超えた時にメモリ再確保等して要素の大移動が起きる
そのため、要素のポインタを取得した後にpush_back
をすると、そのポインタが無効な領域を指すものとなる可能性がある
例えばこんなコードで未定義動作になる
std::vector<int> v1 {0};
auto t = &v1[0];
std::cout << "before push: " << *t << std::endl;
for (auto i = 1; i != 10000; ++i) {
v1.push_back(i);
}
std::cout << " after push: " << *t << std::endl;
before push: 0
after push: 1633344504
実例(Wandbox)
プッシュ後は変な値が出力されている、やばい
こんな感じで、C++の場合、参照取得後に変数を変更して参照が無効になったのに、参照を使って未定義動作食らうってのが、たぶんありがち
Rustの場合、変数を変更するには変数の参照が存在していないことがコンパイルを通すために必要となる
例えばVec
のpush
メソッドは変数を変更するので、参照が存在していたらコンパイルエラーとなる
let mut v1 = vec![0];
let t = &mut v1[0]; //&mutを&にしてもコンパイルエラー
println!("before push: {}", *t);
for i in 1..10000 {
v1.push(i);
}
println!(" after push: {}", *t);
|
3 | let t = &mut v1[0]; //&mutを&にしてもコンパイルエラー
| -- first mutable borrow occurs here
...
6 | v1.push(i);
| ^^ second mutable borrow occurs here
7 | }
8 | println!(" after push: {}", *t);
| -- first borrow later used here
ちなみに&mutを&にするとエラーが若干変わる
なお8行目のエラーはあんまり関係ない
こんな感じで、Rustは参照が存在するときは変数を変更できないので、参照が無効になることを防げる
Rustは関数内変数の参照を返しちゃうことを防げる、的な話もしようかと思ったけどクソ長くなったのでやめます
コードだけおいとくね
#include <iostream>
int* foo() {
int n = 42;
return &n;
}
int main()
{
int a = *foo();
std::cout << a << std::endl;
}
実例(Wandbox)
Optimizationの有無とかclangとgcc切り替えとかで結果変わる、こわい
fn foo() -> &'static i32 {
let n = 42;
&n
}
fn main() {
let a = *foo();
println!("{}", a);
}
実例(Rust Playground)
コンパイルエラーになります
これはチェックしたほうがいいかも、っていうネット上の日本語ソース
超雑にいきます
The Rust Programming Language 日本語版
いわずもがなのThe Book
適当にググるとまれにやたら古いバージョンのThe Bookが出てくる点に注意(最近はめったにない?)
「Rust x.xxを早めに深堀り」シリーズ
(便宜上OPTiM社の「Rust」カテゴリの記事一覧へのリンクを張ってます)
Rustのバージョンアップ(6週ごと)があるたびに超速で投稿されます、しゅごい
大型連休時などにはOPTiM社のブログでなく筆者のあずんひ氏の個人ブログに投稿されることもあるので注意
Rustのパターンマッチを完全に理解した
Rustのパターンマッチを完全に理解した (2)
パターンマッチはlet
でも使うRustの基礎部分ですが結構奥深い
2017-2018年の記事だけどあんまり問題ないはず(新たな記法が追加されているとかはある)
Rustのイテレーター完全制覇
イテレータで今一つわからないところがあったら一通りこれを確認しておくと、もしかしたら理解が深まるかもしれない
iter()
とinto_iter()
の違いはなんぞや?って人とかに多分おすすめ
Rustのイテレータの網羅的かつ大雑把な紹介
「イテレータでできること」が網羅的に紹介されている
Rustの便利クレート
定番から思わぬものまで結構イロイロ
Async/Await | Writing an OS in Rust
いくつかasync/awaitの記事あるけど個人的にこれが一番しっくりくるかなーって
「Rustの Arc
を読む」シリーズ
(第1回へのリンクを張ってます)
かなり難しいけど第1回/第2回くらいまではどうにかなると思う
細かいテクニックとかも結構ある
こうやって思い返してみると僕組み込み系全然チェックしとらんな、勉強しとくか
なんか忘れてるなあと思ったらこれ忘れとったわ
VSCodeの「名前変更」を使った超微妙なコピペテク
競プロとかで微妙に使えるかもしれないテク
以下のような、変数val1
とval2
を使っためんどくさいコード片があったとします
let mut val1 = ...;
let mut val2 = ...;
// val1とval2を使った適当な処理
...
// val1を使った複雑な処理(val2を使った処理でも使いまわしたい)
let foo = bar(val1);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val1] == ... {
boo[val1 + 1] = hoge(val1 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
// 上の処理と似たようなことをval2でもやりたいが☆で示した箇所の処理は変えたい
...
コピペしてちまちまval1
をval2
に置換してから☆の処理を書き換えてもいいけど、単純置換は変数名次第では事故りそう(例えばval10
って変数名があるとval20
になってしまう)
そこで、もうちょっと安全にラクできる、かつ多分Rust固有の方法があるよってお話
まずはとりあえずコピペする(let foo = bar(val1);
以下の行のみ記載
// val1を使った複雑な処理(val2を使った処理でも使いまわしたい)
let foo = bar(val1);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val1] == ... {
boo[val1 + 1] = hoge(val1 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
// コピペしただけ
let foo = bar(val1);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val1] == ... {
boo[val1 + 1] = hoge(val1 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
次に、コピペした部分の1行上にlet mut val1 = val2;
と書いてval1
をシャドーイングする
// val1を使った複雑な処理(val2を使った処理でも使いまわしたい)
let foo = bar(val1);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val1] == ... {
boo[val1 + 1] = hoge(val1 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
// シャドーイング後
let mut val1 = val2; // シャドーイング
let foo = bar(val1);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val1] == ... {
boo[val1 + 1] = hoge(val1 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
次に、カーソルをシャドーイング後のval1
にカーソルを合わせて、F2キーを押して「名前変更」
// val1を使った複雑な処理(val2を使った処理でも使いまわしたい)
let foo = bar(val1);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val1] == ... {
boo[val1 + 1] = hoge(val1 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
// 名前変更後
let mut val2 = val2; // シャドーイング
let foo = bar(val2);
for i in 0..foo {
// なんかval1を使った複雑な処理
...
if baz[val2] == ... {
boo[val2 + 1] = hoge(val2 + baz[foo - 1]); // ☆val2を使う処理ではここを変えたい
}
}
これで、後半部分のval1
がすべてval2
になった
単純置換ではないのでコメントはそのままだし、例えばval10
って変数名があってもval20
ってなったりはしない
あとは☆の処理を書き換えるなり、お好みでlet mut val2 = val2;
の行を削除するなりすればOK!
シャドーイングがあるRustならではのテクニック!
Rust以外にもシャドーイングあったはずだけどどの言語か忘れました
ポエム!
turbofishのturbo要素どこ?::<>
所有権の学習には数値型を使うべからず
box構文マダァ?(・∀・ )っ/凵⌒☆チンチン
参照をC++で無理やり例えると参照よりポインタに近い
なんでfilterって|&&x|とか**xなのか知ってますか?私は知ってます
VSCodeに"Rust"アドオンを入れるべからず "rust-analyzer"アドオンを入れるべし
unwrap() ああunwrap() unwrap()
for i in 0..n-2とやらかしてn=1のときTLEをする、の巻(n: usize)
vecをvec.sort().reverse()みたいにうっかりチェーンしがち
Rustからシャドーイングを無くしたら全世界1.55億人のRustaceanが暴動を起こします
Rustでいいじゃん←案外そーでもない
Debugデフォでderiveしといてほしい感ある
dbg!マクロを仕込んだままsubmitしてTLEを食らう、の回
'a'..='z'を0..26にサクっと対応させるテク求む(Mapはなんかへぼい)
CはXXXXなRust
CはADTがなくてトレイトがなくてメソッドがなくてジェネリクスがなくて型推論がなくてパターンマッチがなくてタプルがなくてイテレータがなくてデストラクタがなくてマクロが衛生的じゃなくて所有権がなくて安全な参照もなくてヌルはあって文字列はヌル終端文字列でインクリメント/デクリメント演算子があって関数を後ろで定義するにはプロトタイプ宣言が必要なRust!
TwitterでRustはXXXXなC、みたいなネタがあったので逆はどうなるんだっていうネタ
これでも全然たりねーよ
[T]
のことなのか それとも&[T]
のことなのか
スライスとは公式リファレンスに答えがありました
スライスは[T]
のことで&[T]
は共有スライスだけど、&[T]
を単にスライスと呼ぶこともあるってさ
つまり文脈依存、/(^o^)\ナンテコッタイ
std::prelude
みてて気づいたこと
なんとなーくstd::prelude
モジュールを見ていて「おや?」と思ったことをちらほら
-
std::ops::Drop
/std::mem::drop
が入っている
いちいちフルネームで書いてたわ(´・ω・`)
これprelude
にするほどなのかしら -
std::borrow::ToOwned
が入っている
"FooStr".to_owned()
みたいに書けるからそりゃそうなんだけど、よく考えるとなんでこれが入ってるんだろう
Cow
っぽいようなことそんなせんだろうし謎 -
std::iter
の各種必要そうなトレイトがある
おかげで非常に捗る - Rust2021だと
std::convert::{TryFrom, TryInto}
がついてくる
Rust2018以下にはついてこない
From
/Into
と同じくprelude
に入ってると思い込んでてコンパイル通らず悩んでたことがある - Rust2021だと
std::iter::FromIterator
がついてくる
イテレータからなんか作るなら普通はcollect
だしマジなぜなんだ・・・
【バッドノウハウ】関数オーバーロードにチャレンジ!
Rustにはオーバーロードない言われるけどある程度それっぽいことはできる
一例として以下のような関数twice
を作ることを考えてみる
- 引数が数値っぽい型のとき:引数を2乗したものを返す
- 引数が文字列型のとき:引数を2つ結合したものを返す
- 引数が「独自型
Foo
とusize
型の2つ」のとき:Foo
がusize
個格納されたVec
を返すどこがtwiceやねん
引数が数値っぽい型だけの場合
fn twice<T: std::ops::Mul + Clone>(x: T) -> <T as std::ops::Mul>::Output {
x.clone() * x
}
fn main() {
println!("{}", twice(42usize));
println!("{}", twice(-3i32));
println!("{}", twice(std::f64::consts::PI));
}
twice
の型引数まわりがごちゃっとしているが要するにMul
とClone
だ
引数が文字列型だけの場合
fn twice<T: Into<String>>(x: T) -> String {
let mut res = x.into();
res += &res.clone();
res
}
fn main() {
println!("{}", twice("Hello!"));
println!("{}", twice(42.to_string()));
}
&str
でもString
でも受け取れるっていうよくあるテクを使ってみました
ここまでのまとめ
同一のtraitを実装している型だけの範囲内なら割と容易にオーバーロードっぽいことはできる
ここまでのはバッドノウハウではなく、「同じように扱える型」を同じように扱っているだけなので良いことだと思う、たぶん
Foo
とusize
型の2つ」だけの場合
引数が「独自型引数の個数が違うの無理なんでタプルにまとめちゃう
#[derive(Clone, Debug)]
struct Foo(i32);
fn twice(x: (Foo, usize)) -> Vec<Foo> {
vec![x.0.clone(); x.1]
}
fn main() {
println!("{:?}", twice((Foo(42), 3)));
}
これ単体なら簡単だけど呼び出すときかっこでくくるのだるいね~
全引数に対応してみる
型もtraitも違うし返り値の型も違うので超絶めんどくさくなる ~もう関数名別でよくない?~
まずおもむろにTwiceExt
っていうオレオレtraitを作る
trait TwiceExt {
type Output;
fn twice_self(self) -> Self::Output;
}
このtraitを実装した型は、twice_self
メソッドによりOutput
型を返すことができる
twice_self
メソッドに、自身をtwice
したものを返すように実装する
そうすると関数twice
は引数のtwice_self
に処理を移譲できる
fn twice<T: TwiceExt>(x: T) -> T::Output {
x.twice_self()
}
そして関数twice
の引数候補となる型にTwiceExt
を実装すればよい
・・のだけど、困ったことにimpl<T: ...> TwiceExt for T
とすることはできない
一度だけならできるけど、これをした後にほかの型にTwiceExt
を実装しようとするとコンフリクトしてしまう
そのため、数値っぽい型、文字列型のそれぞれに個別にTwiceExt
を実装するハメになる
でもめんどくさいのでi32
と&str
と(Foo, usize)
だけに実装した例にしちゃう
impl TwiceExt for i32 {
type Output = i32;
fn twice_self(self) -> Self::Output {
self * self
}
}
impl TwiceExt for &str {
type Output = String;
fn twice_self(self) -> Self::Output {
let mut res = self.to_string();
res += self;
res
}
}
impl TwiceExt for (Foo, usize) {
type Output = Vec<Foo>;
fn twice_self(self) -> Self::Output {
vec![self.0.clone(); self.1]
}
}
これで完成!
全ソースはこちらになります(Rust Playground)
結論
無茶なオーバーロードもどきはおよしなさい
フォーマット文字列の書式どんなんあるんやろ
std::fmt
にありました!知らんと結構わかりにくい気がする
println!
とかのアレです
以下、上記説明を前提に気になった点をいくつか
Widthより出力対象の値の文字数のが大きいとき
println!("Hello!{:5}!", "123"); // 数値じゃないやつは左寄せ
println!("Hello!{:5}!", 345); // 数値は右寄せ
println!("Hello!{:5}!", "123456");
Hello!123 !
Hello! 456!
Hello!123456!
切り詰められることなくすべて出力される
なので他とそろえようとしているときにはズレる
Widthが偶数のときに中央揃えをするとどうなるか
println!("|{:^6}|", "x");
println!("|{:-^10}|", "x");
| x |
|----x-----|
左に寄せられる
#?
指定の出力どんなん?
説明では pretty-print the Debug
formatting (adds linebreaks and indentation) とあるがどんなんやねんってことで
println!("{:#?}", vec![Some(1), None, Some(2)]);
[
Some(
1,
),
None,
Some(
2,
),
]
dbg!
使ったときみたいな感じ?
Localizationについて
localeによらず一定の出力結果が得られるとあり、小数を出力するとlocaleによらず常に小数点が使用される例が説明されている
・・・じゃあ小数点にカンマ使いたい場合はどうするんだろ???~カンマ使う文化滅ぼしちゃう?~
【悲報】FerrisさんRustオフィシャルマスコットじゃなかった
proconio
を使う
AtCoderのインタラクティブ問題でAtCoderでは入力取得にproconio
クレートを使えるが、インタラクティブ問題で普通に使おうとしてもTLEしてしまう
結論から言うと以下のようにLineSource
から取得するようにすればよい
use proconio::{input, source::line::LineSource};
let stdin = std::io::stdin();
let mut source = LineSource::new(stdin.lock());
input! {
from &mut source,
n: usize,
}
// 以下、適宜入出力をする
ただし、別の関数でもinput
するとき、その関数で上記のようにしてもロックが取得されたままになっているので、そのままではTLEになってしまう
そういうときは、その関数を呼び出す前にdrop(source)
とすればOK
使用例(AtCoder Language Test 202001: L - Interactive Sorting)
DiscordがGoをRustにしたアレについて
元ソース:Why Discord is switching from Go to Rust(日本語訳)
最近再びちょっと話題になったようなので個人的に思うところを適当に
- この記事はDiscordのRead Statesサービスというごく一部の部分のみを改善したという話であり、Discord全体をRustで書き換えたわけではない。
- 当時のGoはバージョン1.9.2で、バージョン1.12以降と比べてGCのパフォーマンスが悪いという点に注意。つまり今のGoならここまでパフォーマンスは悪くならない可能性がかなり高い。
- Goを使っていた時はGCの影響を抑えるためLRUキャッシュのサイズを控えめにしていた。
- LRUキャッシュのサイズそのままでRustに書き換えただけの段階では、GCによる影響以外は同程度のパフォーマンスになっている(Max@mentionは明らかに改善されているが桁が変わるほどではない)。
- RustではLRUキャッシュのサイズを抑える必要がないためサイズを増やした。これによって文字通り「桁違い」のパフォーマンスが出るようになった。
- 以上より、単純に「RustはGoより10倍はやいんだ!」みたいに考えるのは明らかに誤り。
- 一方で、「設計が違うからこの比較はバカ」みたいな論調も同意できない。Goの場合GCのせいでLRUキャッシュのサイズを抑える必要がある、という不可避の事情があるため、このパフォーマンスはどう頑張っても出せない。そのため、このパフォーマンスは「Goのままでは達成できなかった」ものであり、改善例の紹介としては適切の範囲だと思う。ここまでハマるのはかなりのレアケースであるという認識は持つべきだけど。
おまけ
Discordにはこれ以前にもRustで改善した事例がある。
Using Rust to Scale Elixir for 11 Million Concurrent Users
こっちはなぜか全然知られていない。