🐥

Rustで構造解析プログラムをつくってみたときの備忘録

2022/04/10に公開

はじめてのRust

表題のとおり、初めてRustで本格的にプログラムを書きました。
いろいろ悩みつつ一応まともに動いたのでまとめとして記事を書きます。
記事として書いていますが、備忘録的な側面が強いため、あまり誰にでもわかりやすくは書いていないです。ご了承ください。

なお、環境はWindowsです。

なぜRustか

これまで主にC#,F#でプログラムを書いてきましたが、やはり実務で利用するには計算速度の面で商用プログラムよりも性能が劣ると感じていました。
ただ、C,C++などの低レベルのプログラミング言語は苦手意識があったため手を出しづらかったところがあります。
そこで、最近パフォーマンス面と言語機能で注目を集めているRustを試してみることにしました。

難しかったところ

難しかったところをまとめます。
※初めてのRustかつ低級言語に慣れてないなので非常に初歩的な内容で詰まっていると思います。

なにはなくとも、借用が難しい

Rust大きな特徴である、借用の概念が難しかったです。
最初はなぜエラーが出るのかわからなかったですし、正直いまでもよくわからないときがあります。

ただ、ありがたいのは、Rustのエラーメッセージが親切であることです。
エラーメッセージで、「ここは借用にしないといけないよ」と教えてくれるので、その指示に従ってコードを修正していけばうまくいくようになってきます。
本当は、そのあたりの挙動の理解してコードを構成していくべきですが、エラーに従って直していけば一応ゴールにたどり着けるというのは学習するうえでは挫折しにくい理由になったのかなと思いました。

↓こんな感じで、どこで借用されてしまっているかちゃんと教えてくれます。

配列の使い方

Rustでは配列は、コンパイル時にサイズが確定している必要があります。
今回作ったプログラムは、実行時に計算対象のサイズを確定するため、C#で実装するときは double[] values; のような形で型宣言だけおこない、実行時に values = new double[size]; のような形で使っていました。
Rustではこのような記述ができません。
サイズが実行時に確定する場合にはVecを使うということになっていますが、Vecは実行時にサイズが確定できる反面、インデックスアクセスは配列に比べると遅いことがベンチマークの結果としてわかりました(予想通りですが)。
大きめの固定長の配列を用意することも考えましたが、やはりもやもやしたのでもう少し調べてみたところBoxを使う方法があることがわかりました。
これを使えば、実行時にサイズを確定しつつ、インデックスアクセスも配列なみに高速でした。いちいち記述が冗長になりますが、目的は果たせそうなのでよしとしました。
もっといい方法もありそうな気がするので、詳しい方教えていただけると嬉しいです。

let mut values = vec![ 0; size ].into_boxed_slice();
let x = values[0];
values[1] = 100.0;

疎行列計算

対象とする問題の最も計算負荷大きいところが疎行列の計算だったのですが、うまく既存のCrateが使いこなせませんでした。以下を試しましたが、ドキュメントを見てもうまく扱えませんでした。主に自分のスキル不足が原因であり、それぞれのCrateについて評価できるほど調べ切れていないです。
ですので、本当はやりたい機能を持っていたのかもしれませんが、使いこなせませんでした。

sprs
https://docs.rs/sprs/latest/sprs/

sparse21
https://docs.rs/sparse21/latest/sparse21/

結果的に、スカイラインマトリクスによる行列演算を自前で実装しました。
別の言語で実装したことはあったので、しょうもないところで苦労しましたが、全体的には比較的簡単に移植できました。

よかったところ

F#での実装よりかなり高速化した

実装の問題もあると思いますが、F#で実装したものより300倍程度処理時間が短縮しました。もちろん、F#版のほうはRust版よりやっていることが多く、また関数型的な書き方を重視しているのでフェアな比較ではありません。そのため、言語自体の速度差を表すものではありません。ですが、F#版では10倍オーダーでの高速化は難しいだろうと思っていたので、この結果には満足です。

その過程での学んだことがあります。
というのも、Rustで実装しても、最初は期待通り速くなりませんでした。
以前実装した言語でパフォーマンスが出なかったのは言語速度の問題と決めつけていたのですが、見直すとアルゴリズムの問題だということに気付きました。これに気付けたのも、「Rustで遅いはずはないから、コードに問題があるに違いない」という考えがあったからで、こういったことも速い言語で実装するメリットなのかなと思いました。

デバッガの活用

開発はVSCodeで行いましたが、デバッガが使えました。
以下の記事を参考にしました。
https://zenn.dev/parauser/articles/795b4fb48b77c5

ブレークポイントが普通に使えたため、思ったよりスムーズに使えてトライ&エラーしやすかったです。
ただ、C#+VisualStudioとは違い、参照となっている値はデバッグ中に値の中身が見えず追いかけにくいところがあったので、その点は少し難しさを感じました。

まだよくわかっていないところ

イテレータ関係

イテレータ関係で、ラムダ式に渡される引数が参照になったりしているのが、あまりよくわかっていないです。よく考えれば必然性がわかると思うのですが、いまはコンパイラに言われるがままに直している感じです。

たとえば以下で、map の中でentryが参照になっているのがなかなか理解できていません。(今の理解だと、d の各要素をラムダ式に渡すときに参照にしないと関数を出た後に破棄されてしまうからなのかなと思っていますが、ちょっとまだ十分な理解ではありません)

let d = vec![ 0.0; m.size ].into_boxed_slice();
d.iter()
 .enumerate()
 .map(|(index, &entry)| vec![(index, entry)])
 .collect::<Vec<Vec<(usize, f64)>>>()
 .into_boxed_slice()

また、個人的にはできるだけ map などを活用して関数型的なコードにしていきたいのですが、for文を使った副作用を駆使したコードに対してパフォーマンスがどの程度変わるのかわかっていないので、安易にfor文に逃げてしまっているところもあります。

プロファイラーの使い方

以下を試しましたが、うまく動かせませんでした。
Windowsだと調整が必要なのかもしれません。

https://keens.github.io/blog/2016/05/14/cargo_profilerwotamesu/

https://zenn.dev/termoshtt/books/b4bce1b9ea5e6853cb07/viewer/flamegraph

感想

わからないなりにコンパイラやドキュメントの支援を受けて何とか完成させることができました。最終的なパフォーマンスとしても申し分なく、改めて高速な言語であることがよくわかりました。

今回やってみて、以下のような感想を得ました。

  • 難しい概念はあるが、言語仕様としては高級言語的なエッセンスが入っていて個人的には好み
  • 比較的簡単にかけるにもかかわらずパフォーマンスが出るというのはすごくありがたい
  • コンパイラが賢くドキュメントもしっかりしているため、はまっても調べていけば何とか解決できて挫折せずに完成させられた

今後はWebAssembly化を試みたいと思います。

Discussion