Open20

Rustに関するチラ裏

まぬけのさひろまぬけのさひろ

まえがき

Markdownの練習も兼ねてなんか適当に
※僕はIT系未経験です AtCoder以外ろくにやってません

どうでもいい詳細

N88BASIC, MASM, C, C++, C#, JSあたりちょっとだけかじったけど、もはやRust以外全然書けない
色々IT系の知識食い散らかしたので微妙に知識はあるのかもしれない
作ったことあるプログラムで一番でかいのは、昔N88BASICで作った高密度300行くらいのクソゲー
あと某MMO用のしょーもないツール作ってました
http://manukeno.web.fc2.com/

適当なチラ裏なのでウソ・大げさ・まぎらわしい、が含まれている可能性があります
若干競プロ的なネタが多くなるかも

とりあえずしょーもない小ネタ

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では、一番上の要素を取り出す際に、

  1. top()で一番上の要素を見て(参照を得て)、コピー/ムーブをする
  2. pop()で一番上の要素を削除する(要素を取り出せない)

という2つの手順を踏む必要がある

pop()のみで一番上の要素を返す&スタックから削除という操作をまとめて行おうとすると要素返すところで例外投げられたときに不整合が起きるとかそんなかんじになって困るので、例外安全のため上記のようにtop()pop()とで分かれた実装となっている

一方RustのVecは、pop()のみで要素が返ってきて末尾の要素が削除されるけど、要素返すのはムーブなので例外が発生しないからセーフ、って感じ

まぬけのさひろまぬけのさひろ

OptionSomeのときだけ中身をVecに追加する方法

vec.extend(option);

なんと1行で済んじゃう
実例(Rust Playground)

extendメソッドがIntoIterを引数にとるOptionIntoIterトレイト持ってる、ってことでこういうことができるんですねー

ちなみにextendメソッドはExtendトレイトのメソッドで、VecExtendトレイトを持っています
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相当のものとしてBoxshared_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++の場合、以下のようにムーブ済みの変数を間違って使っちゃうことがありうる

C++
    std::vector<int> v1 {1, 3, 5};
    auto v2 = std::move(v1);
    std::cout << v1[0] << std::endl; // v2と間違えてムーブ済みのv1にしちゃった!

実例(Wandbox)
この例ではセグフォる

Rustの場合ムーブ済みの変数を使おうとするとコンパイルエラーになる、やったね!

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

実例(Rust Playground)

2. 変数の変更により無効になった参照を使っちゃうのを防げる

「参照」って言ったけどC++ではポインタを使って説明する
コードの見た目がRustの参照と揃うので

例えばC++のvectorpush_backメソッドは、要素の追加によりsizecapacityを超えた時にメモリ再確保等して要素の大移動が起きる
そのため、要素のポインタを取得した後にpush_backをすると、そのポインタが無効な領域を指すものとなる可能性がある

例えばこんなコードで未定義動作になる

C++
    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の場合、変数を変更するには変数の参照が存在していないことがコンパイルを通すために必要となる
例えばVecpushメソッドは変数を変更するので、参照が存在していたらコンパイルエラーとなる

Rust
    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は関数内変数の参照を返しちゃうことを防げる、的な話もしようかと思ったけどクソ長くなったのでやめます
コードだけおいとくね

C++
#include <iostream>

int* foo() {
    int n = 42;
    return &n;
}

int main()
{
    int a = *foo();
    std::cout << a << std::endl;
}

実例(Wandbox)
Optimizationの有無とかclangとgcc切り替えとかで結果変わる、こわい

Rust
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の「名前変更」を使った超微妙なコピペテク

競プロとかで微妙に使えるかもしれないテク

以下のような、変数val1val2を使っためんどくさいコード片があったとします

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でもやりたいが☆で示した箇所の処理は変えたい
...

コピペしてちまちまval1val2に置換してから☆の処理を書き換えてもいいけど、単純置換は変数名次第では事故りそう(例えば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はなんかへぼい)

意外とturbo感あったわ

まぬけのさひろまぬけのさひろ

CはXXXXなRust

CはADTがなくてトレイトがなくてメソッドがなくてジェネリクスがなくて型推論がなくてパターンマッチがなくてタプルがなくてイテレータがなくてデストラクタがなくてマクロが衛生的じゃなくて所有権がなくて安全な参照もなくてヌルはあって文字列はヌル終端文字列でインクリメント/デクリメント演算子があって関数を後ろで定義するにはプロトタイプ宣言が必要なRust!

TwitterでRustはXXXXなC、みたいなネタがあったので逆はどうなるんだっていうネタ
これでも全然たりねーよ

まぬけのさひろまぬけのさひろ

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つ結合したものを返す
  • 引数が「独自型Foousize型の2つ」のとき:Foousize個格納された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の型引数まわりがごちゃっとしているが要するにMulClone

引数が文字列型だけの場合

引数が文字列型だけの場合
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を実装している型だけの範囲内なら割と容易にオーバーロードっぽいことはできる
ここまでのはバッドノウハウではなく、「同じように扱える型」を同じように扱っているだけなので良いことだと思う、たぶん

引数が「独自型Foousize型の2つ」だけの場合

引数の個数が違うの無理なんでタプルにまとめちゃう

引数が「独自型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
trait TwiceExt {
    type Output;
    fn twice_self(self) -> Self::Output;
}

このtraitを実装した型は、twice_selfメソッドによりOutput型を返すことができる
twice_selfメソッドに、自身をtwiceしたものを返すように実装する
そうすると関数twiceは引数のtwice_selfに処理を移譲できる

関数twiceの実装
fn twice<T: TwiceExt>(x: T) -> T::Output {
    x.twice_self()
}

そして関数twiceの引数候補となる型にTwiceExtを実装すればよい
・・のだけど、困ったことにimpl<T: ...> TwiceExt for Tとすることはできない
一度だけならできるけど、これをした後にほかの型にTwiceExtを実装しようとするとコンフリクトしてしまう

そのため、数値っぽい型、文字列型のそれぞれに個別にTwiceExtを実装するハメになる
でもめんどくさいのでi32&str(Foo, usize)だけに実装した例にしちゃう

TwiceExtの実装
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より出力対象の値の文字数のが大きいとき

文字数 > Width
    println!("Hello!{:5}!", "123"); // 数値じゃないやつは左寄せ
    println!("Hello!{:5}!", 345); // 数値は右寄せ
    println!("Hello!{:5}!", "123456");
出力
Hello!123  !
Hello!  456!
Hello!123456!

切り詰められることなくすべて出力される
なので他とそろえようとしているときにはズレる

Widthが偶数のときに中央揃えをするとどうなるか

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によらず常に小数点が使用される例が説明されている
・・・じゃあ小数点にカンマ使いたい場合はどうするんだろ???~カンマ使う文化滅ぼしちゃう?~

上記各例のソースコード (Rust Playground)

まぬけのさひろまぬけのさひろ

AtCoderのインタラクティブ問題でproconioを使う

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(日本語訳)
最近再びちょっと話題になったようなので個人的に思うところを適当に

  1. この記事はDiscordのRead Statesサービスというごく一部の部分のみを改善したという話であり、Discord全体をRustで書き換えたわけではない
  2. 当時のGoはバージョン1.9.2で、バージョン1.12以降と比べてGCのパフォーマンスが悪いという点に注意。つまり今のGoならここまでパフォーマンスは悪くならない可能性がかなり高い。
  3. Goを使っていた時はGCの影響を抑えるためLRUキャッシュのサイズを控えめにしていた。
  4. LRUキャッシュのサイズそのままでRustに書き換えただけの段階では、GCによる影響以外は同程度のパフォーマンスになっている(Max@mentionは明らかに改善されているが桁が変わるほどではない)。
  5. RustではLRUキャッシュのサイズを抑える必要がないためサイズを増やした。これによって文字通り「桁違い」のパフォーマンスが出るようになった。
  6. 以上より、単純に「RustはGoより10倍はやいんだ!」みたいに考えるのは明らかに誤り。
  7. 一方で、「設計が違うからこの比較はバカ」みたいな論調も同意できない。Goの場合GCのせいでLRUキャッシュのサイズを抑える必要がある、という不可避の事情があるため、このパフォーマンスはどう頑張っても出せない。そのため、このパフォーマンスは「Goのままでは達成できなかった」ものであり、改善例の紹介としては適切の範囲だと思う。ここまでハマるのはかなりのレアケースであるという認識は持つべきだけど。

おまけ

Discordにはこれ以前にもRustで改善した事例がある。
Using Rust to Scale Elixir for 11 Million Concurrent Users
こっちはなぜか全然知られていない。