倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Pure Java より速いぞ Groovy!

前回ContinuedFraction クラス(連分数)を使って円周率 π の評価をしましが、Groovy だとパフォーマンス的になかなか悲しい結果になりました。 と、そう言えば最近 id:uehaj 殿が「速いぞGroovy!」って記事を書いてらしたなぁと思い出したので、ちょっと試してみようかと。

参考 URL

バイトコードを書く!?


正直、バイトコードの素養は全くないのでどうしたものかと思ってたのですが、実はゼロからバイトコードを書く必要はないようです。 そのカラクリは「ASM bytecode outline plugin for IntelliJ IDEA」。 これは Javaソースコードをコンパイルして生成されるバイトコードをアウトラインに表示してくれる IntelliJ Plugin。 これを使えば、普通の Java コードを書くだけで必要なバイトコードを作ることができます。 大雑把な手順はこんな感じ:

「ASM bytecode outline plugin for IntelliJ IDEA」の使い方はダウンロードしたページにある程度載ってるのでそちらを参照のこと。 バイトコードの修正は大雑把かと。 下記のサンプルコードを見てください。

Groovy コードをバイトコードにすると言いつつ、Java コードを書いてるのがナカナカ本末転倒感たっぷりだけど、まぁ使い方次第ってことで。

パフォーマンス


さて、コードは Groovy コード内での無名クラスで実装しましょう。 ジャジャーン!

def fracBytecode = new ContinuedFraction(){
    @Override @ast.Bytecode
    protected double getA(int n, double x) {
        l0
            iload 1
            ifne l1
            ldc 3.0
            _goto l2
        l1
            frame same
            ldc 6.0
        l2
            frame same1, 'D'
            dreturn
    }

    @Override @ast.Bytecode
    protected double getB(int n, double x) {
        l0
            iconst_2
            iload 1
            imul
            iconst_1
            isub
            i2d
            dstore 4
        l1
            dload 4
            dload 4
            dmul
            dreturn
    }
}

perform(fracBytecode, 'Groovy Bytecode')

たぶん、Fibonacci 数列のサンプルより簡単ダネ(笑) ゼロからはとても書けないけど。 で、これを実行してみると(前回の実行結果も再掲)

Pure Java : 125 ms
Groovy Closure : 2906 ms
Groovy Anonymous : 1000 ms
Groovy Bytecode : 78 ms

おぉ〜! Pure Java よりも速ぇ〜!! よほどの事がない限り使わない方法だろうけど、よほどの事態には使える方法かと!

インフラ・コード


最後にバイトコードを生成するために必要なアノテーションとクラスを書いておきます。 実はバイトコードよりもこっちの方が込み入ってるんですけど:

Bytecode アノテーションは「Yes, Fibonacci in Groovy can be as fast as Java !」で書かれているもののコピペで構いません(パッケージ注意):

package ast

import java.lang.annotation.*
import org.codehaus.groovy.transform.GroovyASTTransformationClass

@Retention(RetentionPolicy.SOURCE)
@Target([ElementType.METHOD])
@GroovyASTTransformationClass(["ast.BytecodeASTTransformation"])
public @interface Bytecode {}

一方、BytecodeASTTransformation は上記リンク先にあるコードのコピペでは動きません。 というのは、バイトコードの命令セットが網羅されていないためです。 サンプルの Fibonacci 数列のコードが動くほぼ最低限の命令セットしか処理できねーようになってるッ! 苦心の末なんとか動くように修正できました:

package ast

import groovyjarjarasm.asm.Label
import groovyjarjarasm.asm.MethodVisitor
import groovyjarjarasm.asm.Opcodes
import org.codehaus.groovy.ast.ASTNode
import org.codehaus.groovy.ast.expr.ArgumentListExpression
import org.codehaus.groovy.ast.expr.MethodCallExpression
import org.codehaus.groovy.ast.expr.VariableExpression
import org.codehaus.groovy.ast.stmt.ExpressionStatement
import org.codehaus.groovy.classgen.BytecodeInstruction
import org.codehaus.groovy.classgen.BytecodeSequence
import org.codehaus.groovy.control.CompilePhase
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.transform.ASTTransformation
import org.codehaus.groovy.transform.GroovyASTTransformation

@GroovyASTTransformation(phase = CompilePhase.SEMANTIC_ANALYSIS)
class BytecodeASTTransformation implements ASTTransformation, Opcodes {
        void visit(ASTNode[] nodes, SourceUnit source) {
                def meth = nodes[1]
                def instructions = meth.code.statements
                meth.code = new BytecodeSequence(new BytecodeInstruction() {
                        @Override
                        void visit(MethodVisitor mv) {
                                def labels = [:]
                                // perform first visit to collect labels
                                instructions.each { ExpressionStatement stmt ->
                                        def expression = stmt.expression
                                        if (expression instanceof VariableExpression) {
                                                def text = expression.text
                                                if (text ==~ /l[0-9]+/) {
                                                        labels.put(text, new Label())
                                                }
                                        }
                                }
                                instructions.each { ExpressionStatement stmt ->
                                        def expression = stmt.expression
                                        if (expression instanceof VariableExpression) {
                                                def text = expression.text
                                                if (text ==~ /l[0-9]+/) {
                                                        mv.visitLabel(labels[text])
                                                } else if (text =~ /[aild]const|[aild]sub|[aild]add|[aild]mul|[aild]return|[aild]2[aild]/) {
                                                        mv.visitInsn(Opcodes."${text.toUpperCase()}")
                                                } else {
                                                        throw new IllegalArgumentException("Bytecode operation unsupported : "+text);
                                                }
                                        } else if (expression instanceof MethodCallExpression) {
                                                if (expression.objectExpression instanceof VariableExpression 
                                                    && expression.arguments instanceof ArgumentListExpression) {
                                                        if (expression.objectExpression.text=="this") {
                                                                def opcode = expression.methodAsString.toUpperCase()
                                                                ArgumentListExpression args = expression.arguments
                                                                switch (opcode) {
                                                                        case '_GOTO':
                                                                                mv.visitJumpInsn(GOTO, labels[args.expressions[0].text])
                                                                                break;
                                                                        case 'IFNE':
                                                                                mv.visitJumpInsn(Opcodes."${opcode}",
                                                                                                        labels[args.expressions[0].text])
                                                                                break;
                                                                        case 'ILOAD':
                                                                        case 'DLOAD':
                                                                        case 'DSTORE':
                                                                                mv.visitVarInsn(Opcodes."${opcode}",
                                                                                                      args.expressions[0].text as int)
                                                                                break;
                                                                        case 'LDC':
                                                                                mv.visitLdcInsn(args.expressions[0].text as double)
                                                                                break;
                                                                        case 'FRAME':
                                                                                def frameId = args.expressions[0].text.toUpperCase()
                                                                                if ('SAME'==frameId) {
                                                                                        mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
                                                                                        break;
                                                                                } else if ('SAME1'==frameId) {
                                                                                        if (args.expressions[1].text=='I') {
                                                                                                mv.visitFrame(Opcodes.F_SAME1,
                                                                                                                    0, null, 1,
                                                                                                                    [Opcodes.INTEGER] as Object[]);
                                                                                                break;
                                                                                        }else if (args.expressions[1].text=='D') {
                                                                                                mv.visitFrame(Opcodes.F_SAME1,
                                                                                                                    0, null, 1,
                                                                                                                    [Opcodes.DOUBLE] as Object[]);
                                                                                                break;
                                                                                        }
                                                                                }
                                                                        default:
                                                                                throw new IllegalArgumentException(
                                                                                    "Bytecode operation unsupported : "+expression);
                                                                }
                                                        } else {
                                                                throw new IllegalArgumentException(
                                                                     "Bytecode operation unsupported : "+expression);
                                                        }
                                                } else {
                                                        throw new IllegalArgumentException(
                                                            "Bytecode operation unsupported : "+expression);
                                                }
                                        } else {
                                                throw new IllegalArgumentException(
                                                    "Bytecode operation unsupported : "+expression);
                                        }
                                }
                        }
                })
        }
}

他の命令が必要になったら、もう知らん! つーか、Groovy 1.8 になってプリミティブ型がサクサク動くようになったら、この苦労は無駄になるのかなぁ・・・ 無常なり。

Java仮想マシン仕様 (The Java series)

Java仮想マシン仕様 (The Java series)

  • 作者: ティムリンドホルム,フランクイェリン,Tim Lindholm,Frank Yellin,村上雅章
  • 出版社/メーカー: ピアソンエデュケーション
  • 発売日: 2001/05
  • メディア: 単行本
  • 購入: 5人 クリック: 98回
  • この商品を含むブログ (33件) を見る

*1:小文字にしたい箇所を選択して [メニューバー] → Edit → Toggle Case