uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

サーバーサイドGroovyのための5つの性能TIPS

以下、記事「5 Performance Tips for Server Side Groovy」の訳です。

GroovyでGitHubに公開できた投資のコードを書いてたときに性能面をいじくっていたんですが、サーバサイドGroovyをやってる人ためのいくつかの一般的なTIPSを書いておきたいと思います。

1. Groovyはダックタイピングを好む。プリミティブ型には注意せよ。

数値データのためのコレクションクラスで、最初私はkeyフィールドをdouble型で宣言しました。なぜかというと並列処理を伴う割引処理のストラテジがそれぞれ列のベクター(double[])をスレッドプールに投入可能なタスクとして処理するからです。プールされたタスクは、Javaで記述されていて、プリミティブのmathを、DoubleやBigDecimalnの配列やリストより効率良く扱えます。でも、Groovyはboxおよびunboxの処理をプリミティブ型を使うたびに実行してしまいます。この変換は必要なんですけど、性能面では致命的な結果をもたらし、Javaを使うことの性能面での小さな利点を殺してしまいます。加えて、明示的な型指定はメソッド実行についてのわずかな性能低下をもたらします。Groovyコード中でフィールドを以下のように宣言することで、

      def matrix = new double[][]

Groovyコードで性能が向上することがわかりました(double[]中のベクターが、autoboxingを必要としない配列オブジェクトとして扱われる)。数学操作を行う実際のJavaコードでは、強い型付けをともなうプリミティブのシグネチャのままにしておきます。

2. 無名クロージャに気を配ること

つまり、できるだけCollection.each{ ... }の代わりにfor(... in ...)を使うということです。両者の違いは、for loopはバイトコードレベルでコードブロックを囲んだループロジックとして働くのに対して、eachメソッドは新しい無名オブジェクト(クロージャ)を生成するということです。クロージャはオブジェクト生成とガーベージコレクションのオーバーヘッドを伴います。もしクロージャのポータビリティの利点を得る、つまりクロージャを他のメソッドに受け渡し、そのクロージャをループ中で使うなら、これはクロージャを使うべき状況であり、ぜひともクロージャを使いましょう。しかし、クロージャが無名オブジェクトで、決して再利用されず、あとでガーベジコレクションが必要になるなら、避けるのが良いでしょう。

3. 単純化されたベンチマークには懐疑的になること。起動コストを考慮に入れること

Groovyはコンパイルや他の処理を実行時に行う必要があります。コードブロックの最初の実行は常に最も高価です。Javaでも同様なのですが、その度合いは少ないです。Groovyは1.5.xリリース以降、1.6.xではさらに、コンパイルスピードと実行スピードの両方が劇的に向上しました。コードユニットの最初の実行について性能面で留意しておいてください。私の経験では、2回目の実行では少なくとも2倍、5〜6回目の実行では10倍の速度になることも珍しくありません。

4. データ収集とデータ処理機能を分ける

性能を重視するサーバーサイドコードというのは、たいてい大量のデータを処理します。そもそもサーバサイドで処理しようとするのは、大量データ処理を処理したいからです。データ取得機能をデータ処理機能と分離するというのは、データ取得はIOバウンドで、データ処理はCPUに制約を受けるからです。
Groovyの性能についての批判と議論をどう思おうとも、GroovyとC++(アセンブラでもいいですが)の間の性能の比率というのは、データ取得がI/Oで律速されている限り、重要ではないのです。

5. JITが効くようにしよう、JITが助けてくれるようにJITを助けよう

Groovyは、JITが効くようになるにつれて、Javaの性能に近いところまで追いつけるようになります。TIPS #4に従ってデータ取得関数をデータ処理関数と分けておくならば、あなたは、数週間か数ヶ月間、マシンをリブートするまでの間に、1000回実行されるコードブロックを持つことになるでしょう(もちろん、Linux で走らせてるとしてね)。データ処理コードに関して興味深いのは、JITが本当に良く効くようになるのはコード断片が1000回以上実行されてからだということです。Groovyのダックタインピングコードに対して、コードブロックが型AあるいはBの引数を受け入れるときでも、JITが効く可能性があります。しかし、実用上は、Aで連続して1000回呼ばれた後でBで呼ばれた場合、という傾向があるのです。

GroovyコードがJavaに負けないの性能を発揮する場合もあって、私が気づいた一例としては、大量数値データのコレクションのために、演算子オーバーロード定義を使用しているケースです。このユースケースでは、Javaの実装はコレクション上のデータをループ処理し、複雑な式を評価します。Groovyの実装はコレクションクラスの操作をオーバーロード定義していて、操作全体に対する式の結果を含む新しいコレクションを返します。この方式には性能上の利点が二つあります。

まず、Java版で、それぞれの繰り返しにおいて変更を行わないけれども、再計算を行う部分式です。これは式をvolatileな操作とvolatileではない操作に分解することで避けることができます。Groovyのソリューションでは式をクロージャで与えます。


他のTIPSもあります。「常に-server VMフラグを使用せよ」など。これはjavaでも意味がありますが、Groovyではもっと意味があります。Javaに関する記事で詳しく説明されていますが、Groovyコードの高速化と言う意味に置いて、たいへん助けとなりました。

すんません、#5のところ、訳に自信が無いです。誰か添削してほしいです。