高速化日記 (6) - Overload

去年は色々なものを並列化して速くしようとしたわけだが、実環境での trace をみるとしばしば CPU のロードが満杯になっている。つまり並列化しても速くならない。手元で実験しているときはデバイスの状態が clean なので、この実環境の現実は忘れがちである。

CPU のロードは自分たちのアプリや関連サブシステムだけでなく、あんまし関係ないアプリおよび OS のロードに使われている。下の方のレイヤだとたとえば LMK や OS の paging, SystemServer がなんかしてるなど。もうちょっと上だと Play Services がランダムになんかしていたり、システムからの broadcast に応じて寝ていたアプリが起きたり、色々ある。

OS 性能班の人がでてくるとこういうのを直してくれたりしなかったりするが、自分にシステムの大局的な挙動はいじれないのでアプリでできることを探す。

CPU がオーバーロードすると何が問題なのか。たとえば、並列化して追い出したつもりのタスクがサイクルを使いクリティカルパスを遅くする。同じことが投機的実行についても起こる。いわゆる prefetch みたいなコードがサイクルをうばう。そうした non-critical path コードが CPU だけでなく何らかの専有的資源 (I/O とか) をブロックするとさらに遅くなる。

クリティカルパスのコードはそれなりに高い thread priority を持っているが、Linux の scheduler というのは realtime ではないので優先度の低い thread もそれなりにターンが回ってくる。そして low priority threads がいっぱいあると集団として high priority threads を overwhelm してしまう。

一部の HAL は realtime thread を使っているのでこういう問題が起きにくいが, HAL の中に限ってもクリティカルパス全てが realtime に保護されているわけではない。たとえば最近みたやつだと realtime なスレッドから間接的にリクエストを受け取った gralloc HAL での共有メモリ (ION) のアロケータが realtime でないせいで滞り、結果として realtime HAL も詰まらせていた。知らんがな・・・(buffer を preallocate すれば問題を回避できることは確認したが、対価となるメモリ消費量がそれなりなので判断は保留して詳しい人に引き渡した。)

そうした事情から、今年はアプリ起動時の高速化方針を変えた。つまり、色々なものを並列化して eager に初期化するのではなく様々な初期化をなるべく lazy に直して最低限の機能が動くまで先送りし、ベースラインの準備ができたあとで投機的な初期化のタスクを動かすことにした。まあまあの成果。


高負荷状態を手元で再現するのは難しく、まだできてない。単純に CPU やメモリのロードを増やすのは簡単なのだが、それだと SystemServer のようにクリティカルパスをブロックしがちな platform のコンポーネントを stress することができない。もうちょっと platform を exercise するような load generator が必要。OS の中の人はそういうテストをしてるらしいけど、関心は systems としての throughput や stability なので、アプリ単位での latency みたいのは当事者(自分)がなんとかしないといけない。

いまのところ、たまにヒープを baloon するプロセスを動かしつつ CPU コアを半分くらいとめて trace を眺めるといったローテクかつアドホックでいまいちな方法を使い、ふーん・・・とかいってる。それでも先の gralloc のレイテンシは再現できた。しかしもうちょっとなんとかしたい、しかし自動化やってるとあっという間に月日が流れてしまいがちなので誰かにやってほしい、というかやってくれることになっているので気長に待ってる。