livaの雑記帳

LKL(ライブラリOS版Linux)のramfsアクセスを二倍高速化した

概要

LKLでramfsへのread/writeが遅かったので、原因究明をしました。結果、LKLではmemcpyがカーネルが汎用的に提供しているmemcpyを使用していたため、libcのmemcpyを呼ぶようにした所、およそ二倍の性能改善を達成しました。結構簡単な事で性能改善ができるので、皆もLKLを触って、contributeしていこう!

LKLの紹介

最近研究の一環としてバイト先でLKL(The Linux Kernel Library)を触っています。LKLは有り体に言えばLinuxカーネルを改変してライブラリOS化したものです。

参考:www.iij.ad.jp

ライブラリOSとは名前が指し示す通り、OSをライブラリ化したものです。汎用PCにおけるOSは、モノリシックカーネルとしてOSの殆どを特権空間内で動かすか、マイクロカーネルとしてカーネルを最小限の大きさにした上で、OSの機能はアプリケーションとは別のユーザープロセスから提供するという設計が広く知られています。両者とも、アプリケーションと同じアドレス空間で資源調停をすると、アプリケーションが資源管理、配分機構を破壊したりハイジャックできたりしてしまうのでアドレス空間を分けてあげようね、と考えるのが基本です。一方のライブラリOSではアプリケーションとOSが同一のアドレス空間に同居しています。要はシステムコールを単なる関数コールで実現できる、と考えると良いでしょう。こう書くと組み込み方面から「そんなんただの組み込みOSやん」という声が上がりそうですが、既に下に何かしらの資源調停機構(普通のOSなり、ハイパーバイザなり)がおり、そいつが大雑把に資源配分やアイソレーションの面倒を見てくれる事を前提とした上で、従来のOSのfunctionalityをより優れた方法で提供するのが、組み込みOSや普通のライブラリとは異なるライブラリOSの特徴なのかなと僕は捉えています。

ライブラリOSは結構古くからある概念ですが、最近でも比較的ホットな研究ネタではあります。まあOSの研究の世界なんて、システムソフトウェアをユーザー空間で動かすか、カーネル空間で動かすか(或いはハイパーバイザレイヤーで動かすか)のせめぎ合いを延々としていると言っても過言ではない(※一個人の意見です)ので、その流れの一つとしてライブラリOSが取り上げられ続けるのは自然な事なのかもしれません。ライブラリOSはユニカーネルという形で仮想化技術とセットになって語られたり、OSのインターフェースを変化させずにセキュリティやポータビリティ、I/O性能を向上するコンテキストで見かける事が多いです。ライブラリOSではないものの、Linuxカーネルをユーザー空間で動かすという点ではUser Mode Linuxもありますね。

僕個人がOSの研究をする際の一番の関心はパフォーマンスであり、その観点でライブラリOSを見れば「ホストOSと違ってユーザー空間とカーネル空間の遷移オーバーヘッドが無いため、その分早い」というのがワクワクするポイントなわけです。しかしLKLを触っていた所、システムコールが遅い、特にramfsへのreadやwriteに掛かる時間がホストよりも明らかに遅い事が分かりました。こりゃ修正するっきゃねぇべ。

解析

LKLの解析に限らずパフォーマンス改善を行う上で気をつけなければいけないポイントは、「その怪しそうな箇所は本当に全体のパフォーマンスに影響を与えているか?」です。当たり前っちゃ当たり前なんですが、幾ら怪しそうでも最終的な数値に与える影響が軽微であれば改善する必要はありません。

例えばLKLがシステムコールを呼び出す課程上にはmutexの獲得やセマフォ操作があり、一見遅そうに見えます(し、実際アトミック操作が重いアーキテクチャでfcntlを単発で呼び出す時はボトルネックとなっています)が、今回のramfsのreadやwriteにおける支配的なコストではありません。システムコール呼び出しの際のLKL固有のオーバーヘッドはあくまでシステムコールごとに生じる物ですが、ramfsへのreadやwriteでは(下のグラフの通り)I/Oサイズに比例したオーバーヘッドが顕著だからです。

それを念頭においてgdbで処理を負いつつ(ユーザースペースなのでgdbが使える)ソースコードを眺めていくと一つ気になるポイントがあります。libc/string.cのmemcpyが呼ばれているのです。

github.com

このmemcpyは1byteづつデータをコピーするというもので、確かに1byteづつコピーすれば非常に丁寧な仕事ができるでしょうし、コードがシンプルにもなるのですが、如何せんクソ遅いです。

多くのアーキテクチャではこのように最適化されたmemcpyが提供されています。

github.com

lib/string.cのmemcpyはあくまでポータビリティのために存在する、といった所でしょうか。

解決策は簡単で、arch/lkl/include/asm/string.hでアーキテクチャ固有memcpyを宣言し、実態は単にlibcのmemcpyが呼び出されるようにするだけです。LKLがユーザースペースで動く以上libcも呼び出せるわけですから、libcの洗練されたmemcpyを使わない手は無いですよね。

評価結果

修正を加え性能計測した結果が以下のグラフの通りです。

f:id:liva_h:20200520215903p:plain

lkl_read/writeが未修正の物、lkl_patched_read/writeが修正済みの物です。host_read/writeは、素のlinuxカーネルです。 (計測は一回だけで、複数回の平均を取ったりはしてないです。論文じゃないし)

計測環境は以下の通りです。

CPU: Intel(R) Xeon(R) CPU E3-1275 v5 @ 3.60GHz(8コア)

メモリ:DDR4 SDRAM 16GB

ホストOS:Ubuntu 16.04(4.4.0-134-generic)

ベースのLKL:8a1fc6c(5.3ベース)

カーネルのバージョンが揃ってないのも、まあ論文じゃないんで。

とまあ適当な計測ではあるんですが、修正を加えると大幅に性能が改善されていることが一目瞭然です。所によりホストOSより早かったりもしますね。(最近ramfsが早くなってたりするのであれば話はまた別ですが)ライブラリOSがホストOSより早くあって欲しいとユーザーが願うのは自然な事なので、こういうポイントがどんどん増えていくとLKLを使う旨味が増すのかなぁ、と思う次第です。

ホストOSより遅い部分の要因も後輩に分析してもらって、一つ大きなポイントが見つかってるんですが、こちらは研究になりそうなので、このくらいで。

まとめ

この記事ではLKLでのramfsアクセスが遅い原因を分析し、簡単な修正によって性能が大幅に向上する事を実験結果とともに示しました。

Linuxカーネルの内部を深く調べるという行為にとっつきにくさを感じる人も多いかもしれません。(少なくとも自分はそうだった)これはUser Mode Linuxにも共通する話だとは思いますが、LKLはuserspaceで動作するので実行やデバッグを気楽に行える他、アーキテクチャ依存コードが比較的単純で読みやすいです。また、このように簡単な性能改善ができるポイントもまだまだ多そうです。「低レイヤーに触れてみたいけど、Linuxカーネルを触るのはちょっと・・・」と感じているなら、LKLを試してみるのはどうでしょうか。

LKLに触れる人が増え将来実用的に広く使われるようになれば、僕の作ったパッチの価値が高まって超嬉しいので、早く皆さんLKL触ってください。

おまけ

見ようによってはここからが本編かもしれない。

NEC Sx-Aurora TSUBASAというアクセラレータ(PCIeデバイス型のコプロセッサ。ノリとしてはGPUみたいなもん)上でLKLを動かすのが個人的に最近アツいです。

んで、上のパッチを適用した所、200K ramfs readが513μsから74μsになりました。わっほいわっほい。

ベンダ提供のglibcのmemcpyが割と早いからでしょうか。ベクトルプロセッサの本気がほんの少し垣間見える結果となりましたね!