Sunday, December 18, 2011

メールからエンジニアの活動パターンを調査する in Groovy

これは Ariel Advent Calendar 2011 の記事です。

僕が以前所属していたアリエル・ネットワークではほとんどの情報がメールで共有されていました。面倒なミーティングに招待されたことや、誰かが typo まみれの恥ずかしいコードをコミットしたことも全てメールで届きます。それはログであり、誰が、何時、何をしているかがそこからわかります。

エンジニアの活動について知りたいならば最も適切な材料になるのは ITS (Issue Tracking System) から届くメールです。問題の報告、仕様の記述、進捗や調査内容の共有、質問と回答などなど、事あるごとにメールが飛びます。今回はそれらのメールの送信時間から何がわかるのか見てみたいと思います。使うのは手元に残っている 2007 - 2009 の12月のメールです。mbox で保存されている各月ごとのメールと次の Groovy スクリプトを使って、時間帯ごとの送信回数をグラフで表示してみます(DateヘッダがGMTだったので実際はJSTに変える処理が必要だったけどそこは省略):
def mbox = mboxName as File
mbox.withReader { r ->
    def acts = new int[24]
    while (r.ready()) {
        (r.readLine() =~ /^Date:.+\s(\d{2}):\d{2}:\d{2}\s/).with { m ->
            if (m) acts[m[0][1] as int]++ 
        }
    }
    acts.eachWithIndex { c, h ->
        println(String.format("%2d %s(%d)", h, '*'*(c/10), c))
    }
}

Dec 2007:
 
 0 **(27)
 1 **(20)
 2 (5)
 3 (5)
 4 (2)
 5 (0)
 6 (0)
 7 (0)
 8 (0)
 9 (1)
10 *************(132)
11 *********************************(337)
12 **************************************(384)
13 ***************(158)
14 ****************************(286)
15 *******************************(312)
16 *******************************(314)
17 *************************************(374)
18 ***********************************(351)
19 *******************************(315)
20 **********************(228)
21 ********************(203)
22 ****************(166)
23 *****(59)
 

Dec 2008:
 
 0 (5)
 1 (7)
 2 (0)
 3 (0)
 4 (1)
 5 (0)
 6 (0)
 7 (0)
 8 (0)
 9 *(17)
10 ***********(119)
11 ******************************(304)
12 ************************(245)
13 ***********(119)
14 ****************************(288)
15 *******************************(311)
16 *************************(250)
17 ***************************(275)
18 **********************(226)
19 *****************(171)
20 *********(99)
21 *****(55)
22 *******(77)
23 *(19)
 

Dec 2009:
 
 0 ******(68)
 1 **(26)
 2 **(29)
 3 **(21)
 4 (7)
 5 (8)
 6 (5)
 7 (2)
 8 (5)
 9 *******(75)
10 **********************************(349)
11 **************************************************************************(749)
12 ****************************************************************************(766)
13 **************************************************(502)
14 ***********************************************************(598)
15 *************************************************************(615)
16 ****************************************************************(642)
17 *********************************************************************(698)
18 ***************************************************************(634)
19 **********************************************************(584)
20 **********************************(343)
21 ***********************(235)
22 *************(136)
23 *************(139)
 

このグラフから次のことが見て取れます:

1. 10時 - 24時までが主な活動時間: 今は朝7時とかに出社するよくわからないグループもいるみたいですが。

2. 10時台になると一気に活発になる: 多くの人は 10時 - 11時に出社するようです。そう、アリエルは11時までに出社すればOKです。ギリギリに来る人が多かったですね。

3. ランチは13時台: 自分が早く出社してお腹が空いているからといって早めに誘ってしまうと、12時台のピークを奪うことになりますから止めましょう。

4. 午後の活動のピークは17時前後: ランチ後の睡眠欲に打ち勝ち(あるいは充足し)、ノッている時間帯です。この時間帯にミーティングが入ってしまうと生産性が非常に落ちます。

5. 20時 - 22時 にかけて人が帰宅していく: ただし、アリエルは15時以降ならいつでも帰ってよいことになってるし、帰りづらい雰囲気は皆無です。僕も明るいうちに会社出て遊びに行ったりしてました。逆に忙しい時は自宅で朝っぱらから夜中までとかもやってましたが。自由を制する力が求められます。

6. 2008年と比較すると2009年は倍増: 会社が急激に成長したようです。グラフの形があまり変わっていないのは仕事と一緒に人もどんどん増えた結果でしょうか。コアチームのメンバーはほとんど増えなかったですけど。

見落としていることもあるかもしれません。でも少なくともメールの送信時間だけでこれだけのことはわかるわけです。ここに人やissueの属性が加わればもっといろんなことがわかります。

このようにログを取っていてそれを利用できる会社なら、無駄な報告作業が減るかもしれませんね。

Tuesday, December 13, 2011

G5、それはGroovy使いの為のスライドショーツール

これは G* Advent Calendar の 12/13 (13日目) の記事です。

タイトルにあるとおり、 Groovy でスライドショーできるツール、 G5 をリリースします(おそらくクリスマスまでには!)。インスパイアされたのが HTML/JavaScript/CSS でスライドショーする S5Clojure でスライドショーする L5 なので、 G5 と名づけました。ちなみに L5 の作者の深町さん("はてな"の人)はアリエル時代の同僚で、S5 より L5 を先に知りました。このツール、作ろうかなと思ってから1年くらい経っていて、今回の Advent Calendar のネタとしてようやく手を付けたので、まだ数時間しか費やせてません。完成度は察してください。

まずはシンプルな例です:
new g5.SlideBuilder()({s{b("Look Ma, one line!")}})()

これで次のようにスライドショーが開始されます。今のところ Apache FOP 純正の AWT renderer 使っているので見た目が懐かしい感じですが、それはそれ。

ファイルに出力したい時はこうします:
new g5.SlideBuilder()({s{b("Look Ma, one line!")}})('application/pdf','one-line.pdf')

こんな仕上がり:

もう少しまともな例として G5 の紹介用スライドを G5 で書きました(途中まで)。こちらはまずは出来上がりから:


このスライドは次のコードで動いています:
import g5.SlideBuilder

def slideBuilder = new SlideBuilder()

def name = { 
    def attrs = [color:'#3283A1']
    slideBuilder.inline(it ? attrs + it : attrs, "G5") 
}
def title = { slideBuilder.block 'font-size':'48pt', 'margin-bottom':'18pt', it }
def properNoun = { slideBuilder.inline 'font-style':'italic', "\"$it\"" }

def slides = slideBuilder 'font-family':'Verdana', {
    slide {
        title { inline "Introduction to "; name('font-size':'64pt') }
        block "Nagai Masato" 
        block 'font-size':'16pt', color:'gray', {
            block "Twitter: @nagai_masato"
            block "Blog: http://nagaimasato.blogspot.com/"
        }
    }
    slide {
        title { inline "What is "; name(); inline "?" }
        list 'font-size':'24pt', {
            listItem { 
                image 'http://groovy.codehaus.org/images/groovy-logo-medium.png' 
                inline "Slideshow System" 
            }
            listItem {
                inline "Allows you to create and show slides using "; properNoun "Groovy"
            }
            listItem {
                inline "Uses "; properNoun "XSL-FO"; inline " and "
                properNoun "Apache FOP"; inline " for formatting slides"
            }
        }
    }    
} 
slides.show()

別名を使えば短く書けます。お好みでどうぞ。
import g5.SlideBuilder
 
def sb = new SlideBuilder()

def nm = { def attrs = [color:'#3283A1']; sb.i(it ? attrs + it : attrs, "G5") }
def tt = { sb.b 'font-size':'48pt', 'margin-bottom':'18pt', it }
def pn = { sb.i 'font-style':'italic', "\"$it\"" }

def ss = sb 'font-family':'Verdana', {
    s {
        tt { i "Introduction to "; nm('font-size':'64pt') }
        b "Nagai Masato" 
        b 'font-size':'16pt', color:'gray', {
            b "Twitter: @nagai_masato"
            b "Blog: http://nagaimasato.blogspot.com/"
        }
    }
    s {
        tt { i "What is "; nm(); i "?" }
        ls 'font-size':'24pt', {
            li { 
                im 'http://groovy.codehaus.org/images/groovy-logo-medium.png' 
                i "Slideshow System" 
            }
            li { i "Allows you to create and show slides using "; pn "Groovy" }
            li { 
                i "Uses "; pn "XSL-FO"; i " and "; pn "Apache FOP"
                i " for formatting slides" 
            }
        }
    }    
} 
ss()

G5 は こちら で公開しています。まだ開発ブランチにソースがあるだけですけど。Groovy 使いのみなさん、リリースされたら勉強会とかで使ってください ;)

時間がないので今日はこのへんで。

Wednesday, November 2, 2011

グリーに入社しました

11/1付けでグリーに入社しました。今後は当面グリープラットフォームの開発を担当します。

グリーでは相当なハードワークになりそうですが、いいきっかけなので"闘うプログラマー"を地で行ってみます。僕が入ったことでグリーに良い変化を与えられたらと思います。

これからもよろしくお願いします。

Sunday, October 23, 2011

アリエル・ネットワークを退職しました

私事ですが、先週でアリエル・ネットワーク(以下アリエル)を退職しました。正確には最終出社を終え、今はまだ有給消化期間です。

"美しいプログラムを書けるプログラマ"の言葉に共感し入社したのが2007年2月、そこから4年9ヶ月をアリエルで過ごしてきました。その間いろいろな変化がありました。20名だった社員は100名に増え、オフィスは2回移転、初期から関わってきた製品は成長して大きくなり、そして僕の立場はマネージャになっていました。

マネージャにならないかと打診されたのは1年ちょっと前でした。不安はありましたが、プロジェクトやチームを動かせる機会なんてそうそうないですし、エンジニアと兼務してもいいと言われたのでやらせてもらうことにしました。一応誤解のないように補足しておくと、アリエルではエンジニアの先にマネージャがあるわけではありません。いようと思えばずっとエンジニアでいれます。

マネージャになってからは、開発プロセスの改善や、チーム作りなど、望んでいたとおりそこでしか得られない貴重な体験ができました。しかしマネージメント上の課題や問題(半分くらいは開発とは直接関係のないこと)が頭の中を占める割合が増え、エンジニアの方に思うように力を入れらなくなると、果たしてこれが今自分がやりたいことなのだろうか、という疑問が出始めました。仕事だからと割り切り、生きがいを個人でのエンジニア活動に限定することもできなくなはいでしょうが、日々の大半は仕事です。僕はマネージャを辞め1エンジニアとして再出発することを決断しました。

再出発には別の道を進むことを条件に付け加えました。"不得手なことをする"と言い換えてもいいです。入社以来1つの製品をひたすら開発し続ける中、得意とするフィールドができあがり、そこでしか勝負しない自分に嫌気がさしていました。その思いはプロジェクトに余裕がない状態でマネージャが考えることではない、と自分に言い訳しながら無視してきましましたが、こうなればそんな言い訳はできません。

当初、アリエルに残る道も考えましたが、選んだのは退職でした。しがらみを乗り越えるより、不便になること覚悟で何もかも捨てて身軽になる方に魅力を感じました。とにかくシンプルにしたかった。

アリエルではほんとにいろんな人にお世話になりました。特に開発の3アミーゴスである、井上さん大谷さん中山さん、そしてチームの皆には最後まで迷惑をかけ、いろいろと助けてもらいました。この場を借りて改めて感謝の言葉を伝えてたいと思います。本当にありがとうございました。

11月からは新しい場所で出直しです。これからもよろしくお願いします。

Thursday, October 13, 2011

Groovy の AST を楽に書く方法

AST を書くのは大変

Groovy は AST 変換の実装用に AST を生成する DSL を提供しています。ところがこの DSL は複雑で習得が困難です。次のような GString の AST を生成したいとします:
"Hi, $name."
その場合、DSL はこうなります。こんなもの構文と同じ数覚えられません:
gString 'Hi, $name.', {
    strings {
        constant 'Hi, '
        constant '.'
    }
    values {
        variable 'name'
    }
}

ソースコードから直接生成することもできますが、コンパイラによるチェックの恩恵を受けられないですし (これについては Joachim Baumann が説明しています)、DSL よりパフォーマンスが悪いです。次のベンチマークを見てください:
@Grab('com.googlecode.gbench:gbench:0.2.2')
import gbench.BenchmarkBuilder
import org.codehaus.groovy.ast.builder.AstBuilder

def benchmarks = new BenchmarkBuilder().run {
    'DSL to AST' {
        new AstBuilder().buildFromSpec {
            gString 'Hi, $name.', {
                strings {
                    constant 'Hi, '
                    constant '.'
                }
                values {
                    variable 'name'
                }
            }
        }
    }
    'Code to AST' {
        new AstBuilder().buildFromString('"Hi, $name"')
    }
}
benchmarks.prettyPrint()

            user system cpu    real

DSL to AST     0      0   0  339918
Code to AST    0      0   0 2076590
というわけで、DSL を実装コードやテストコードを見ながら書く羽目になるのですが、これが大変な作業になることは簡単に想像してもらえるはずです。

どうすれば楽に書けるのか

この問題を解決する為に DSL をコードから自動生成してくれるライブラリを作りました。 AstSpecBuilder と名付けたこのライブラリはここで公開しています。使い方はとても簡単、build メソッドに馴染みのあるコードを文字列で渡すだけです:
import astspecbuilder.*

def spec = new AstSpecBuilder().build('"Hi, $name."')

def expectedSpec =  '''\
block {
    returnStatement {
        gString 'Hi, $name.', {
            strings {
                constant 'Hi, '
                constant '.'
            }
            values {
                variable 'name'
            }
        }
    }
}
'''
assert expectedSpec == spec
AST からも生成できます。というより実は上のメソッドは次のコードのショートカットに過ぎません:
import astspecbuilder.*

def ast = new AstBuilder.buildFromString('"Hi, $name."')
def spec = new AstSpecBuilder().build(ast)

インデントはデフォルトではスペース4つですが、変更するオプションが付いてます。次のようにすると DSL がタブ1つでインデントされます:
def spec = new AstSpecBuilder(indent: '\t').build('"foo"')

def expectedSpec = '''\
block {
\treturnStatement {
\t\tconstant 'foo'
\t}
}
'''
assert expectedSpec == spec

Sunday, July 24, 2011

GBench 0.2.0 がリリース

GBench 0.2.0 をリリースしました。GBench は Groovy 向けのベンチマークフレームワークです。このフレームワークは2つの強力な機能、 AST 変換と builder によって簡単にベンチマークを取れるようにしてくれます。

What's New
  • CPU時間のサポートを追加
  • builder API の改良
    • ベンチマーク対象のコードブロックを追加する為のより良い (Groovierな) 構文の追加。
                ----
                run {
              with 'label', {
              }
          }
          ----
                ->
                ----
                run {
              label {
              }
          }
          ----
    • run() へのオプションを追加。
      • "average", "idle", "trim"。使い方は javadoc を参照。Yasuharu Nakano さん(GroovyServ の作者) へソースコードの提供を感謝します。
      • "repeat"。このオプションは "time" オプションの代わりとなるものですが、 "time" オプションも後方互換性の為に残ります。
    • 新しい API の追加。
      • sum(), average(), prettyPrint()。使い方はそれぞれの javadoc を参照。
  • バージョンスキームの変更。
        YY.MM.DD
        ->
        Major.Minor.Micro

Resolved Issues
  • @Benchmark のデフォルトハンドリングを指定するシステムプロパティの名前に古いドメインが含まれている。
        "groovybenchmark.sf.net.defaulthandle"
        ->
        "gbench.defaulthandle"

Examples
  • AST transformation Example:

  • Builder Example:

ぜひ試し、フィードバックを下さい (このブログのコメント、プロジェクトの issue tracking system、またはTwitter:@nagai_masato 経由で)。 あなたのフィードバックが GBench を改善を継続する力になります。

Sunday, July 3, 2011

Groovy Quiz: Elvisは何人いる?

Elvisは何人いる?:

[]?.size?:[].@size?:[]*.size?:[].&size?':[].size?':[]

Saturday, July 2, 2011

Poison for Groovy Part 2

残念ながら今日Groovy用の新たな毒を生成することに成功しました (既に別の毒も見つけています。以前のポストを読んでください)。

生成には3つの工程があります:

1. GroovyがインストールされているWindowsマシンを手に入れます。これは大変難しいミッションです。
2. ダブルクォートされ、スペースを含んだ文字列をJAVA_OPTS環境変数へセットします:
----
> set JAVA_OPTS=-Daprop="a value"
----
3. groovyを実行します:
----
> groovy -h
----

すると次のエラーが起こります:
----
value"" was unexpected at this time.
----

既にこのバグに関連する2つの問題を報告しパッチも提供しています (GROOVY-4910GANT-126を参照)。これらの問題がすぐに解決することを望みます、Windows固有の問題はパッチがあってもいつも未解決のままですけどね。 

Groovyは仮想マシン上で動くクロスプラットフォーム言語、でしょ?

Saturday, June 25, 2011

GBench = @Benchmark Annotation + BenchmarkBuilder

Groovy 用のベンチマーク・フレームワーク、 GBench をリリースしました。GBench は2つの機能、 @Benchmark Annotation と BenchmarkBuilder を持っています @Benchmark Annotation はプロダクトのコードをいじらずに実行時間を計測することを可能にするアノテーションで、既に公開されています。より詳しい情報については以前のポストを読んでください。 BenchmarkBuilder はベンチマーク・コードを簡単に書くための便利なビルダで、今回初めて登場します。

次のコードは文字列連結を1000回繰り返してベンチマークを採るものです:
----
def strings = ['GBench', ' = ', '@Benchmark Annotation', ' + ', 'BenchmarkBuilder']
def benchmark = new BenchmarkBuilder()
benchmark.run times: 1000, {
    with '+=', { 
        def s = ''
        for (string in strings) {
            s += string    
        }
    }
    with 'concat', { 
        def s = ''
        for (string in strings) {
            s.concat string    
        }
    }
    with 'string builder', {
        def sb = new StringBuilder()    
        for (string in strings) {
            sb << string    
        }
        sb.toString() 
    }
    with 'join', {
        strings.join()
    }
}
println benchmark
----

出力はこのようになります:
----
                   time
+=             18197705
concat          7669621
string builder  9420539
join            5248348
----

もちろん結果はソートできます:
----
println benchmark.sort({ lhs, rhs -> lhs.time <=> rhs.time })
----

----
                   time
join            5248348
concat          7669621
string builder  9420539
+=             18197705
----

結果を好きなように処理することもできます:
----
new File('benchmark.csv').withWriter { file ->
    file.writeLine 'label,time(ms)'
    benchmark.sort().each { bm ->
        file.writeLine "${bm.label},${bm.time / 1000000}"
    }
}
----

----
> cat benchmark.csv
label,time(ms)
join,5.248348
concat,7.669621
string builder,9.420539
+=,18.197705
----

いまのところ、GBench は wall-clock time しか計測できませんが、将来のリリースでは CPU time や user time もサポートする予定です。

GBench はこちらからダウンロードできます。ぜひ試してフィードバックしてください!

Saturday, June 11, 2011

@Benchmark annotation for Groovy が大幅に更新!

@Benchmark annotation for Groovy v11.06.11 をリリースしました。このリリースはいくつかの大きな変更と大きなバグの修正を含んでいます。

- 変更
  - ベンチマーク結果へのクラス/メソッド情報の追加
  - ベンチマーク結果処理をカスタマイズする新オプション

  - クラスアノテーションのサポート

- バグ修正
  - クラス内のメソッドで動かない問題を修正


ベンチマーク結果へのクラス/メソッド情報の追加

ベンチマーク結果として、ベンチマークされたメソッドとそのクラス上方を取得できます。例えば次のコードでは、
----
package foo

class Foo {
    @Benchmark
    def foo() {
    }
}
----

出力はこうなります:
----
foo.Foo java.lang.Object foo(): xxx ns
----


ベンチマーク結果処理をカスタマイズする新オプション

貧弱なオプション、"prefix" と "suffix" の代わりに ベンチマーク結果の処理をカスタマイズする "value" オプションが追加されました。値の設定方法は3つあります:

- ハンドラクラスを使う
- クロージャを使う
- システムプロパティを使う

ハンドラクラスを使う場合、Benchmark.BenchmarkHandler インターフェイスを実装したクラスを作り2つのメソッド、 handle() と getInstance() をそれらへ追加します:
----
class MyHandler implements Benchmark.BenchmarkHandler {
    static def instance = new MyHandler()
    static MyHandler getInstance() {
        instance    
    }
    void handle(klass, method, time) {
        println("${method} of ${klass}: ${(time/1000000) as long} ms")
    }    
}
----

そうですね、上の例のようなシングルトンクラスは @Singleton アノテーションを使えば短く書けます :-)
----
@Singleton
class MyHandler implements Benchmark.BenchmarkHandler {
    void handle(klass, method, time) {
        println("${method} of ${klass}: ${(time/1000000) as long} ms")
    }
}
----

最後にハンドラクラスを @Benchmark に設定します:
----
@Benchmark(MyHandler.class)
def foo() {
}
----

Groovy 1.8 からはハンドラクラスの代わりにクロージャも使えます。クロージャであれば、
ベンチマーク結果を処理するクロージャを設定するだけです:
----
@Benchmark({println("${method} of ${klass}: ${(time/1000000) as long} ms")})
def foo() {
}
----

またシステムプロパティ、 "groovybenchmark.sf.net.defaulthandle" でデフォルトの処理を置き換えられます:
----
> groovy -cp groovybenchmark-11.06.11.jar 
-Dgroovybenchmark.sf.net.defaulthandle="println(method + ' of ' + klass + ': ' + ((time/1000000) as long) + ' ms')" foo\Foo.groovy
----

これらの例の場合、出力はこうなります:
----
java.lang.Object foo() of foo.Foo: xxx ms
----


クラスアノテーションのサポート

クラスにアノテートすることで、そのクラスの全てのメソッドのベンチマーク結果を取得できます:
----
package foo

@Benchmark
class Foo {
    def foo() {
    }
    def bar() {
    }
}
----

この例の場合、 foo() と bar() メソッドのベンチマーク結果を取得できます:
----
foo.Foo java.lang.Object foo(): xxxx ns
foo.Foo java.lang.Object bar(): xxxx ns
----

そしてこれはつまり、あなたがコードの変更やプロファイリングツールなしにプログラムの全てのメソッドをベンチマークできる力を手に入れたことを意味します。なぜなら Groovy 1.8 は全てのクラスにアノテーションを適用する compilation customizer を提供しているからです:
----
// BenchmarkGroovyc.groovy
import groovybenchmark.sf.net.Benchmark

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.tools.FileSystemCompiler

def cc = new CompilerConfiguration()
cc.addCompilationCustomizers(new ASTTransformationCustomizer(Benchmark))
new FileSystemCompiler(cc).commandLineCompile(args)
----

----
> groovy -cp groovybenchmark-11.06.11.jar BenchmarkGroovyc.groovy MyApp.groovy
> java -cp .;%GROOVY_HOME%\embeddable\groovy-all-1.8.0.jar;groovybenchmark-11.06.11.jar MyApp 
----

----
Xxx xxxx foo(): xxx ns
Xxx xxxx bar(): xxx ns
Xxx xxxx baz(): xxx ns
MyApp java.lang.Object main(java.lang.Object): xxx ns
----

ぜひ試してフィードバックしてください!

Tuesday, May 31, 2011

@Benchmark annotation for Groovy がバグ修正の為に更新されています

今日以下の(恥ずかしい)バグの修正の為に @Benchmark annotation for Groovy を更新しました。

- Groovy 1.7 で java.lang.VerifyError が起きる
- JVM 1.5 で java.lang.UnsupportedClassVersionError が起きる

現在 @Benchmark が Groovy 1.7/1.8 と JVM 1.5/1.6 で動作することは確認しています。
@Benchmark は初めてという方は前回のポストを読んでください。

Saturday, May 28, 2011

@Benchmark annotation for Groovyをリリース!

@Benchmarkアノテーションはメソッドの実行時間をコードの追加なしで計測できるようにしてくれます。次の例は@Benchmarkの基本的な使用例です:
----
@Benchmark
def method() {
    // an operation takes 1 ms
}

method()
----
この場合標準出力に"1000000"が出力されます. @Benchmarkは時間をナノ秒で表します。

出力形式に手を加える為のオプションが2つあります:
----
@Benchmark(prefix = 'execution time of method(): ', suffix = ' ns')
def method() {
    // an operation takes 1 ms
}

method()
----
こうすると出力は"execution time of method(): 1000000 ns"となります。

@Benchmarkは出力にthis.println(value)を使います。なので目的に沿ったwriterを"out"プロパティにセットすることで出力をリダイレクトできます:
----
def results = new StringBuffer()
this.setProperty("out", new StringBufferWriter(results))

// method calls

this.setProperty("out", null)

results.toString().eachLine { result ->
    println "${result.asType(int.class) / 1000000} ms"
}
----

ダウンロードはこちらから。

Sunday, May 22, 2011

Exploring Groovy 1.8 Part 1 - そのコード、本当に最適化されてますか?

Groovy目下の課題であるパフォーマンス向上の為、1.8では整数演算とメソッド直接呼び出しへの最適化が行われるようになりました。どちらの最適化も考え方は同じで、簡単にいってしまえば"出来る限り書かれた通りにコンパイルする"というものです。これはこれまでGroovyコンパイラが構文を正しく解析してくれなかった、という意味ではありません。Groovyでは柔軟なメタプログラミングを可能にする為にコンパイル時に劇的な変換が施されるのです(このあたりは以前のポスト、インサイドGDKでも触れています)。問題は例外がなかったことです。この後の話の都合上、フィボナッチ数の計算メソッドを例としてあげます:
----
int fib(int n) {
    if (n < 2) return n
    return fib(n - 1) + fib(n - 2)
}
----


このコードは全くメタプログラミングの恩恵を受ける必要のないコードですが、v1.7以前では次のように変換されます(実際はGroovyのAPIが登場しますし行っていることももう少し複雑なのですが、今回の話では重要ではない為省略します)。全ての演算/メソッド呼び出しはリフレクションへ置き換えられ、整数に対してせわしなくボクシングとアンボクシングが繰り返されます:
----
int fib(int n) {
    // Get the Method objects
    if (lessThanMethod.invoke(Integer.valueOf(n), Integer.valueOf(2))) 
        return Integer.valueOf(n).intValue();
    return 
        ((Integer) plusMethod.invoke(
            fibMethod.invoke(
                minusMethod.invoke(Integer.valueOf(n), Integer.valueOf(1)
            ),
            fibMethod.invoke(
                minusMethod.invoke(Integer.valueOf(n), Integer.valueOf(2)
            )
        )).intValue();
    );
----


これがv1.8では次のように最適化されます。これが冒頭での言った"出来る限り書かれた通りにコンパイルする"の意味です:
----
int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
----


さて、話はこれで終わりません。というのも、どうやらこの最適化の恩恵を受ける条件は我々が想像しているよりずっと厳しいものだということです。リリースノートに書かれている次の注意事項を忠実に守るだけでは充分ではありません:
1. 整数の最適化を求めるのであれば1つの式の中でint型とInteger型を混ぜないこと
2. メソッド呼び出しの最適化を求めるのであれば"this"上で実行でき、引数の型がprimitiveかfinalであること


次の表を見てください。これはフィボナッチ数を求めるメソッドをいくつかの方法で実装し、それらの実行速度をv1.8.0とv1.7.10で比較したものです:
フィボナッチ(30)の実行時間(単位はナノ秒)
ランク
アルゴリズム / スタイル
v1.8
v1.7
改善
1
反復 + Groovyスタイル for loop
49720
46276
-6.93%
Down :-(
2
反復 + Java/Cスタイル for loop
129360
77824
-39.84%
Down :-(
3
再帰 + if + return
45080820
548522821
+1116.75%
Up :-)
4
再帰+ 三項演算子
120489259
538476119
+346.91%
Up :-)
5
再帰 + if-else
259945537
546381516
+110.19%
Up :-)

この結果は次の3つのことを示しています:
1. 反復版はパフォーマンスが少し劣化した
2. 再帰版はパフォーマンスが大幅に向上した
3. コーディングスタイルによるパフォーマンスの差が広がった

理由は簡単です。結果が芳しくないものは両方、あるいはいずれかの最適化に失敗しているのです。この表で言えば、先に例にあげた再帰 + if + return版以外は全て何らかの形で最適化に失敗しています。例えば再帰を使った中で最も悪い結果となった再帰 + if  + else版はこうです:

----
int fib(int n) {
    if (n < 2) n
    else fib(n - 1) + fib(n - 2)
}
----

これは次のように変換されます(繰り返しになりますが正確には実際のものとは違います):
----
int fib(int n) {
    if (n < 2) return n;
    else return 
        ((Integer) plusMethod.invoke(
            fibMethod.invoke(Integer.valueOf(n - 1)),
            fibMethod.invoke(Integer.valueOf(n - 2))
        )).intValue();
    );
}
----

たったこれだけのスタイルの違いが最適化が成功するかを左右し、パフォーマンスに5倍の差を作るのです。こういった問題はこれから改善されていくでしょうが、少なくとも現時点では諸手を上げて喜べる段階ではないようです。


さてさて、そのコード、本当に最適化されてますか?




(今回計測に使ったコード一覧)


反復 + Groovyスタイル for loop:
----
int fib(int n) {

    int a = 0
    int b = 1
    int c
    for (int i in 2..n) {
        c = a + b
        a = b
        b = c
    }
    b
}
----


反復 + Java/Cスタイル for loop:
----
int fib(int n) {
    int a = 0
    int b = 1
    int c
    for (int i = 2; i <= n; i++) {
        c = a + b
        a = b
        b = c        
    }
    b
}
----


再帰 + if + return:
----

int fib(int n) {
    if (n < 2) return n
    return fib(n - 1) + fib(n - 2)
}
----


再帰 + 三項演算子:
----
int fib(int n) {
    n < 2 ? n : fib(n - 1) + fib(n - 2)  
}
----


再帰 + if-else:
----
int fib(int n) {
    if (n < 2) n
    else fib(n - 1) + fib(n - 2)
}
----