メソッド名やその引数が可変な状況でのリフレクションは非常にパフォーマンスが悪いです。その処理をネイティブでの呼び出しと同レベルまで引き上げるのが今回のお題です。
リフレクションを使う場合、大まかに状況は以下のように分類できると思います。
- クラス名とメソッド名が固定
- クラス名が可変、メソッド名は固定
- クラス名は固定、メソッド名は可変
- クラス名とメソッド名が可変
1番、2番に関してはインターフェースを噛ませればネイティブでの呼び出しと大差ない呼び出し時間になります。 クラス名の取得はJARファイルはZIP形式ですので、展開してファイルを列挙すれば分かります。ここではそちらには触れません。
ここでは3番、4番のメソッド呼び出しのパフォーマンス改善を行います。
結論から言いますと、関数を中継して呼び出すクラスのソースコードを出力し内部でコンパイル、そのクラスをインターフェース越しに呼び出すという形になりました。
前出のバイトコードの出力よりはメンテナンスが容易ですが、この高速化手法を行うにはJavaをJDKで実行している必要があります。不特定多数に使われるアプリケーションの場合は使えないので、その場合には冗談で書いた前回のようにプログラム中でバイトコードを生成する必要があります。
実験の為、まずは普通に関数を実行する以下のコードを実行します。
インライン最適化の仕様をそれほど把握できていないので、明らかに無駄なコードがありましたら教えてください。。。
【共通部】
1 2 3 4 5 6 7 8 9 |
package com.gumulab.refrection; import com.gumulab.refrection.IPlugin; public class Plugin implements IPlugin{ public int sum(int a,int b){ return a+b; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.gumulab.refrection; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class JARUtils { public static Class getPluginClass() throws ClassNotFoundException, MalformedURLException { File f = new File("Include.jar"); URLClassLoader loader = new URLClassLoader(new URL[] { f.toURL() }); Class c = loader.loadClass("com.gumulab.refrection.Plugin"); return c; } } |
【コード1】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package com.gumulab.refrection; public class CallByNative { public static void main(String[] args){ Plugin plugin = new Plugin(); System.out.print("最適化前"); long time = System.currentTimeMillis(); long l1 = call(plugin); System.out.println(System.currentTimeMillis()-time); System.out.println("最適化中"); System.out.println("<========>"); for(int i = 0; i < 100; i++ ){ if(i%10==0) System.out.print("+"); call(plugin); } System.out.println(); System.out.print("最適化後"); time = System.currentTimeMillis(); long l2 = call(plugin); System.out.println(System.currentTimeMillis()-time); System.out.println(l1+l2); } public static long call(Plugin p){ long n = 0; for(int i = 0; i < 1000000000; i++){ n+=p.sum(i, i); } return n; } } |
【コード2】
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.gumulab.refrection; public class CallByNativeInterface { /*コード1と同一なのでmain関数を省略*/ public static long call(IPlugin p){ long n = 0; for(int i = 0; i < 1000000000; i++){ n+=p.sum(i, i); } return n; } } |
【コード3】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.gumulab.refrection; import java.net.MalformedURLException; public class CallByInterface { public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException{ Class c = JARUtils.getPluginClass(); IPlugin plugin = (IPlugin)c.newInstance(); /* 以下省略 */ } public static long call(IPlugin p){ long n = 0; for(int i = 0; i < 1000000000; i++){ n+=p.sum(i, i); } return n; } } |
【コード4】(遅いので、ループ回数を1/10にして実行しています)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.gumulab.refrection; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.MalformedURLException; public class CallByInvoke { public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException{ Class c = JARUtils.getPluginClass(); Object obj = c.newInstance(); Method m = c.getMethod("sum", int.class, int.class); System.out.print("最適化前"); long time = System.currentTimeMillis(); long l1 = call(obj,m); System.out.println(System.currentTimeMillis()-time); /*以下省略*/ } public static long call(Object obj,Method m) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException{ long n = 0; for(int i = 0; i < 100000000; i++){ n += (int)m.invoke(obj, i ,i); } return n; } } |
それぞれの秒数(単位はms)を3回計測すると以下のようになりました。時間がかかっていたため、コード4はループ回数を1/10にして表示された速度に10倍しています。
[コード1] 生成したインスタンスのメソッドを叩く 331,329,331
[コード2] 生成したインスタンスをインターフェース越しに叩く 331,329,330
[コード3] リフレクションで生成したインスタンスをインターフェース越しに叩く 327,334,337
[コード4] リフレクションで生成したインスタンスをリフレクションで叩く 6460,6510,6320
Method.Invokeを使うとかなり遅くなる事が分かります。 反面、リフレクションで動的にインスタンスを生成してもインターフェース越しに呼び出すコストは殆どないようです。
つまり、リフレクションで呼び出したいクラスを呼び出すためのクラスを動的に作成し、それをリフレクションでインターフェース越しに呼べば高速化になる、はずです。
【アプリケーション】 |
(リフレクションで生成、インターフェースで操作) |
【動的に作成した呼び出し用クラス】 |
(渡されたものをそのまま操作) |
【呼び出したいクラス】 |
前回はバイトコードを出力しましたが、難解ですし実用的ではありません。そのため、ソースコードを出力してコンパイルはJDKに丸投げします。
まず呼び出し用クラスの為のインターフェースを作成します。
1 2 3 4 5 6 |
package com.gumulab.refrection; public interface IPluginCaller { public void setObj(Object obj); int sum(int a, int b); } |
次に呼び出すクラスを作成します。
1 2 3 4 5 6 7 |
package com.gumulab.refrection; public class Plugin2 { public int sum2(int a,int b){ return a+b; } } |
以下のコードでコンパイル、実行することができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 |
package com.gumulab.refrection; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.tools.Diagnostic; import javax.tools.DiagnosticCollector; import javax.tools.FileObject; import javax.tools.ForwardingJavaFileManager; import javax.tools.JavaCompiler; import javax.tools.JavaFileManager; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; public class CallByCreateNative { /* 呼び出し用クラスのソースコードのテンプレート */ private static final String callCode = "package com.gumulab.refrection.temp;\n" + "import com.gumulab.refrection.IPluginCaller;\n" + "public class Caller implements IPluginCaller{" + " %class_name% c = null;" + "public void setObj(Object obj){" + " c = (%class_name%) obj;" + "}" + "public int sum(int x,int y){" + " return c.%func_name%(x,y);" + "}" + "}"; /* IPluginCallerのソースコード */ private static final String pluginCallerInterfaceCode = "package com.gumulab.refrection;\n" + "public interface IPluginCaller {" + "public void setObj(Object obj);" + "public int sum(int x,int y);" + "}"; /* 呼び出し用クラスのクラス名を指定 */ private static final String PluginCallerName = "com.gumulab.refrection.temp.Caller"; /* IPluginのインターフェース名 */ private static final String PluginCallerInterfaceName = "com.gumulab.refrection.IPluginCaller"; /* 呼び出し用クラスの作成を行う */ private IPluginCaller setupCaller(ClassLoader parentLoader,String jarName, String className, String methodName) throws Exception { /* クラスローダーの設定 */ URLClassLoader loader = null; if(parentLoader == null){ loader = new URLClassLoader(new URL[] { new File(jarName).toURI().toURL() }); }else{ loader = new URLClassLoader(new URL[] { new File(jarName).toURI().toURL() }, parentLoader); } /* 呼び出したいオブジェクトを取得 */ Object target = loader.loadClass(className).newInstance(); /* Javaコンパイラを取得 */ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); if (compiler == null) { /* JDKで実行されていない場合はここでNULLになる */ System.out.println("Compiler = null"); throw new Exception(); } /* コンパイルが終了したクラスファイルを追加するためのArrayList */ final ArrayList<LoadClassFileObject> compiledObject = new ArrayList<LoadClassFileObject>(); /* 実際にファイルが保存されないよう、ファイルの出力を監視する */ StandardJavaFileManager stdFileManager = compiler.getStandardFileManager(null, null, null); JavaFileManager fileManager = new ForwardingJavaFileManager<JavaFileManager>(stdFileManager) { @Override public JavaFileObject getJavaFileForOutput(Location location, String name, JavaFileObject.Kind kind, FileObject sibling) throws IOException { /* 書き出すクラスの名称が目当ての物か */ if (name.equals(PluginCallerName)) { /* コンパイル済みクラスファイルに追加する */ LoadClassFileObject l = new LoadClassFileObject(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind); l.setClassName(name); compiledObject.add(l); return l; } /* ダミーの何もしない出力クラスを返す */ return new NullOutputFileObject(URI.create("string:///dummy"), kind); } }; /* エラー情報を取得する受け皿 */ DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>(); /* Javaソースを構築 */ String codeCaller = callCode.replace("%class_name%", className).replace("%func_name%", methodName); String codeInterface = pluginCallerInterfaceCode; /* コンパイルするソースをリストにまとめる */ List<JavaFileObject> fileobjs = new ArrayList<JavaFileObject>(); fileobjs.add(JavaCode.getFileObject(PluginCallerName, codeCaller)); fileobjs.add(JavaCode.getFileObject(PluginCallerInterfaceName, codeInterface)); /* 呼び出したいクラスの格納されたJARファイルをクラスパスに追加 */ List<String> options = Arrays.asList(new String[] { "-classpath", jarName }); /* コンパイル */ JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, fileobjs); if (!task.call()) { /* コンパイルに失敗した場合、エラー情報を出力して例外を投げる */ for (Diagnostic<?> diagnostic : diagnostics.getDiagnostics()) { System.out.println(diagnostic.getCode()); System.out.println(diagnostic.getKind()); System.out.println(diagnostic.getPosition()); System.out.println(diagnostic.getStartPosition()); System.out.println(diagnostic.getEndPosition()); System.out.println(diagnostic.getSource()); System.out.println(diagnostic.getMessage(null)); } throw new Exception(); } /*今回はコンパイルしたクラスは1つであるため、最初の物をロード、インスタンスを返す*/ Class<?> c = compiledObject.get(0).loadClass(loader); IPluginCaller caller = (IPluginCaller) c.newInstance(); /*呼び出し先を設定*/ caller.setObj(target); return caller; } /* なにも出力しないダミーのオブジェクト */ public class NullOutputFileObject extends SimpleJavaFileObject { protected NullOutputFileObject(URI uri, Kind kind) { super(uri, kind); } public OutputStream openOutputStream() throws IOException { return new ByteArrayOutputStream(); } } /* ロードするクラスを格納するためのオブジェクト */ public class LoadClassFileObject extends SimpleJavaFileObject { protected LoadClassFileObject(URI uri, Kind kind) { super(uri, kind); } /* クラスファイルのバイトコード */ private ByteArrayOutputStream baos; /* クラスファイルを出力するためのOutputStreamを書き換え */ @Override public OutputStream openOutputStream() throws IOException { return (baos = new ByteArrayOutputStream()); } /* 保持するクラス名 */ private String className = null; public void setClassName(String name) { className = name; } /* ロードしたクラスの保持 */ private Class<?> clazz = null; private ClassLoader loader = null; /* クラスのロード */ public Class<?> loadClass(ClassLoader parentLoader) throws ClassNotFoundException { final byte[] b = baos.toByteArray(); if (loader == null) { loader = new ClassLoader(parentLoader) { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { if (clazz == null && className.equals(name)){ clazz = super.defineClass(className, b, 0, b.length); return clazz; } return super.findClass(name); } }; } return loader.loadClass(className); } } /* 速度の計測 */ private void process() { try { /*呼び出し用クラスの生成*/ IPluginCaller caller = setupCaller( null ,"Include.jar","com.gumulab.refrection.Plugin2", "sum2"); System.out.print("最適化前"); long time = System.currentTimeMillis(); long l1 = call(caller); System.out.println(System.currentTimeMillis() - time); System.out.println("最適化中"); System.out.println("<========>"); for (int i = 0; i < 100; i++) { if (i % 10 == 0) System.out.print("+"); call(caller); } System.out.println(); System.out.print("最適化後"); time = System.currentTimeMillis(); long l2 = call(caller); System.out.println(System.currentTimeMillis() - time); System.out.println(l1 + l2); } catch (Exception e) { e.printStackTrace(); } } public static long call(IPluginCaller p) { long n = 0; for (int i = 0; i < 1000000000; i++) { n += p.sum(i, i); } return n; } public static void main(String[] args) { new CallByCreateNative().process(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
package com.gumulab.refrection; import java.net.URI; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; public class JavaCode extends SimpleJavaFileObject { private String code; private JavaCode(String className, String code) { super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); this.code = code; } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return code; } public static JavaFileObject getFileObject(String className, String code) { return new JavaCode(className, code); } } |
実行時間は332,328,329と十分なパフォーマンスが出ています。
これでもバイトコードを出力した際よりも長いコードですが、あそこまで難解ではないし意味の分かりやすいコードなのでメンテナンスはしやすいはずです。
Compiler APIの他のサンプルを見る限り、ほぼ全てJavaFileManagerを継承して新しいクラスを作ったりしていましたが実装が長くなるのが辛かったので無理に押し込んでしまいました。
実装にあたってはクラスローダーが曲者でした。 LoadClassFileObject内部のfindClassの実装は間違っているかもしれません。
ピンバック: 【Java】リフレクションを限界まで高速化する | Gumu-Lab.