futures-rsのjoin_allのパフォーマンス変遷
はじめに
Rustには非同期処理を扱うためのutilityとして、 futures-rs というライブラリがあります。
ある時社内で、このライブラリが提供しているjoin_all
やtry_join_all
に関する下記issueが話題に上がりました。
issueはtokio-rsに上げられていますが、実質的にはfutures-rs
が提供している関数の内容になります。
このjoin_all
やtry_join_all
は実際に何箇所かで使用していたので、このissueに関して調べてみました。
※注)
このissueは少し前のもので、後述しますが最新版のfutures-rs
にはこの課題を緩和するようなPRがすでに取り込まれて、v0.3.17でリリースされています。
join_allの実行モデル (v0.3.16以前)
join_all
の復習になりますが、こいつは複数の Future を並行して実行し、すべての Future が完了するのを待つ関数です。イメージ的にはJavaScriptのPromise.all()に近しいと思います。
use futures::future::join_all;
async fn foo(i: u32) -> u32 { i }
let futures = vec![foo(1), foo(2), foo(3)];
let result = join_all(futures).await;
assert_eq!(result, [1, 2, 3]); // executed **only after** join_all completes
さて、ライブラリがこの挙動をどう実現しているか、実装もとてもシンプルで読みやすいので実際にコードを追ってみます。(version的には v0.3.16 で、後述する改善PRが入る前の最新版です)
join_all
のエントリ
上記がjoin_all
のエントリですが、最終的にはJoinAll
というEnumを返しています。結論このJoinAll
がFuture
を実装しているので、こいつのpoll
がどういう実装になっているか見てあげれば良さそうです。
JoinAll
のFuture
実装
poll
の実装を見ると、join_all()
に渡したcollectionの1つ1つの要素をイテレーションして、それぞれのpoll
を実行しているのが分かります。最終的に、collectionに含まれる全てのFutureがReady
になった時はじめて、JoinAll
自身もReady
を返して処理が終了します。
この実装によって、join_all
には引数に渡すcollectionの要素数が大きくなった際に、下記のような課題感があったようです。[1]
-
Executor
からJoinAll
のpoll
が呼ばれた際に、毎回全ての要素のpollが呼ばれてしまう。 - (さらにtokioと組み合わせた場合、) cooperative task yieldingにより、
JoinAll
の1度のpollでReady
にできるFutureの数が制限され、collectionの全てのFutureをReady
にするのに余計に時間がかかる
FuturesOrdered
を使った改善 (v0.3.17以降)
上記のような課題感から、join_all
には下記のようなPRが投げられました。(try_join_all
も同様)
この変更により、join_all
に渡されたcollectionの要素数が30より大きい場合、FuturesOrderedという非同期タスクキューのようなデータ構造を使うように変更されました。 [2]
FuturesOrdered
は、キューに入れられたFutureを先頭から順番にpoll()していき、完了していればReady
を返すような挙動をします。以前の実装と比較すると、この実装はひとたびキューに入れられた要素がReady
になると、その要素は再びpoll
されることはないので、要素数が多い場合こっちの実装の方が遥かに効率的です。
0.3.17以降は、この修正が入ったjoin_all
が提供されています。
まとめ
futures-rs
クレートのjoin_all
のちょっとした変遷を紹介しました。
最新のバージョンを使っていたり、少ない数のFuture
を扱っているだけだったら大して問題にならないとは思いますが、かなり大きなオーダーのFuture
をjoin_all
等で待つみたいな処理を書いている場合は知っておいて損はないかなと思います。
-
FuturesOrdered
とは別で、順序を保証しないFuturesUnorderedというデータ構造もあります。join_all
の実装ではこっちのFuturesUnordered
でも良いのでは?と思ったのですが、FuturesOrdered
ではないといけない理由があるのかもしれないです。(詳しい方がいたら教えてください!) ↩︎
Discussion