Log4j 2にも採用されたLMAX Disruptorはなぜ狂ったように速いのか?

dis

LMAXという会社はおそらくFX業者で、筆者はLMAXの開発者の講演を、InfoQの動画で何度か見たことがあった。
彼らは非常に特異な集団で、さしずめ「Javaのスピード狂」という感じだ。
印象的なのは、シングルスレッドで仕事を片付けることを強調している点だ。
「Javaならマルチスレッドで並列処理すれば性能が出ると広く思われているが、我々の仕事においてはシングルスレッドが最速だ」というような主張を何度も見た。
ゴールドマンサックスといいLMAXといい、やはり多額の金が動く会社でガチでJavaをやっている連中はカリカリにチューニングするため、技術的には非常に面白い。
彼らがコアのライブラリをOSS化してくれるというのは、金融業界を否定的な目で見る筆者からすると複雑だが、悔しいことに参考になる。

LMAX DisruptorはJavaのライブラリだ。Producer/Consumerパターンであるスレッドから別のスレッドへメッセージを渡す際のパフォーマンスを追求したものである。Log4j2のAsync実装に採用されているようだ。

Disruptorの速さの秘密はいくつかのブログで詳細に解説されているので、じっくり知りたい人はソースコードを読むとともにそちらを参照するとよいだろう。

  • ロックを極力排除し、Producerからのアクセスの一ヶ所でしか使用していない
  • データ構造としてリングバッファを使い、メッセージが格納されている領域のインデックスを各Consumer等がシーケンス番号として管理することで、ロックを減らしている

何とも、非常に頭が良い解決方法である。

シーケンス番号はどんどん上がっていくので、配列を用意すると無限の大きさの配列が必要となってしまう。しかしリングバッファをぐるぐる廻して、全Consumerが処理済みの領域を上書きして使っていくことでこの問題を解決している。

どんなConsumerが存在するのか?が中央で管理されているのが面白い。オブジェクト指向的にはイケていないが、ロックを排除するための工夫ということだろう。

2015/01/06追記
最後の「オブジェクト指向的にはイケていないが…」の部分に対して「別にイケていなくてもいいのでは」的な反応を頂いたので追記しておく。

まさしくその通りで、Disruptorはパフォーマンスを極めるためのものなので、「オブジェクト指向的にイケて」いなくてもOKである。
しかし実は筆者ではなく、LMAXの中の人が、この点について(かなり強く)言及しており、ここは大きなポイントだ。
オブジェクト指向的に綺麗に設計する(関心を分離する)ことにこだわることがパフォーマンス劣化(正確にはcontention)につながっている
と、http://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdfに記述されている。

Further investigation and a focus on the computer science made us realise that the conflation of concerns inherent in
conventional approaches, (e.g. queues and processing nodes) leads to contention in multi-threaded implementations,
suggesting that there may be a better approach.

同様にこちらのブログの以下の部分も同様。

The ConsumerTrackingProducerBarrier has a list of all the Consumers that are accessing the ring buffer. Now to me this seemed a bit odd – I wouldn’t expect the ProducerBarrier to know anything about the consuming side. But wait, there is a reason. Because we don’t want the “conflation of concerns” a queue has (it has to track the head and tail which are sometimes the same point), our consumers are responsible for knowing which sequence number they’re up to, not the ring buffer. So, if we want to make sure we don’t wrap the buffer, we need to check where the consumers have got to.

筆者の書き方が悪かったのだが、つまり「オブジェクト指向的にきれいな設計を捨てることで性能向上を実現している」という、トレードオフの部分が非常に面白いのだ。(そのため「オブジェクト指向的にはイケていない」と書いたことは文脈的におかしくないし、むしろもっと強調してもよい部分だと考えている。)

ちなみに上記の指摘をされた方からは「アルゴリズム的な話に、オブジェクト指向をもってくるのがナンセンスなんです。」とも言われたのだが、上記PDFの図を見てもらえばわかるようにDisruptorは典型的なオブジェクト指向で書かれたソフトウェアであるので、Disruptorについて言及している本エントリにおいて「オブジェクト指向をもってくるのがナンセンス」と言われても困る。おそらく指摘された方はDisruptorのドキュメントやソース等は読まずに、本エントリだけに脊髄反射されただけだろうと思う。

Disruptorはいわば「守破離」的に、全体としてはオブジェクト指向に従いながら、オブジェクト指向で当たり前とされる「関心の分離」の一部だけをわざと崩すことで最高のパフォーマンスを実現しているところが素晴らしい。


WekaのIBkはデフォルトで正規化してくれる

フリーソフトではじめる機械学習入門という本があります。

フリーソフトではじめる機械学習入門
荒木 雅弘
森北出版
売り上げランキング: 18,141

筆者的にはかなりツボな本で、既に2度ほど読み通しています。
今度は実際に手を動かしながら(Wekaを使ったりJavaのコードを書きながら)もう一度読んでみようかと思っています。

18ページあたりから、KnowledgeFlow Environment(KFE)を使ってWekaに格納されているirisのデータをK近傍法で分類する例が始まります。
28ページ真ん中あたりで正規化(1~10程度の値を取る属性と1~1000程度の属性などが混在する場合に、データ間の距離の計算において、後者の属性の影響が強くならないようにすること)が紹介され、下図のようにNormalize(左から3つめ)のが追加されます。

weka-nfe

筆者はまず、本書の通りにKFEを使ってみました。

次に、KFEのようなGUIのツールではなく、JavaのコードからIBkクラスを使ったirisデータの分類をしたかったので、Googleで調べながら独学で進めてみました。これは、特に難しいところはありませんでしたが、その時点で手元で書いてみたコードでは、データの正規化を行っていませんでした。そのためこのときの結果は不正確だろうと考えました。

影響を確認するために、わざと極端にレンジの広い属性と狭い属性を持つデータを用意して分類させてみたのですが、特におかしな結果になりません。仕方ないのでNormalizeクラスの使い方を学んできちんと正規化してみましたが、結果はNormalizeクラスを使っても使わなくても同じで、正常な結果となりました。

原因を調べてみると、どうやらIBkを利用したK近傍法による分類では、デフォルトで正規化を行ってくれるようです。デフォルトではインスタンス間の距離を計算するのにweka.core.EuclideanDistanceクラスが使われますが、こちらのクラスが計算の際に正規化をしてくれます。-Dオプションを渡すと、正規化を行わないという動作になるようです。

そのため、本書の28ページから29ページにかけて行われているNormalizeの追加は、省略しても同じ結果になります。