IL2CPPでもnew T()を高速化したい
本記事はUnity Advent Calendar 2023 シリーズ3 21日目の記事です。遅れてすみません、、
急ぎ足で端折っててるので、分かりにくいかもしれません。
こちらのライブラリの部分解説になります。
IL2CPPの制限
まずUnity IL2CPPはAOT(Ahead of time)であり、動的コード生成はできません、
すでにNativeなコードを動かすのでILをJITコンパイルすることもできません。
(Interpreterを実装すれば実行は可能)
ということでリフレクションをExpressionTreeやDynamicMethodでの高速化はできません。
しかし、一部は高速化可能です。
MethodInfoは簡単
MethodInfoはDelegate.CreateDelegateを用いてDelegateに変換可能で、得られたDelegateはIL2CPPでも高速に実行できます。
ConstructorInfoは簡単には行かない
しかし、ConstructorInfo
はDelegate.CreateDelegate
できません。コンストラクタはインスタンスメソッドであり、new ()
とは異なり、アロケートされたインスタンスのコンストラクトのみを行うため、扱いづらいせいなのかDelegate化関数が提供されていません。
アロケートされただけでコンストラクタの呼ばれていないオブジェクトは
FormatterServices.GetUninitializedObject(Type)
で得られるのであとは
コンストラクタのデリゲートさえあれば、new T()と同等なので、惜しい、、。
でも実はConstructorInfoをDelegate化する方法はあります。
internalなメソッドとUnsafeクラスです。
mscorlibとCoreCLRで異なるので、環境依存にはなりますが、現時点(2023/12/26)でのUnityが用いているmscorlibではDelegate.CreateDelegate_internal
という型チェックを行わないextern関数が存在します。そしてMethodInfo
もConstructorInfo
もデリゲート化時に用いられるRuntimeMethodHandle
の配置が同じです。
ということで手順を説明します。
-
internal static extern Delegate CreateDelegate_internal(Type type,object target,MethodInfo info, bool throwOnBindFailure)
をリフレクションで取得する。 -
Delegate.CreateDelegate
でデリゲート化Func<Type,object,MethodInfo,bool,Delegate>
-
Unsafe.As
でConstructorInfo
をMethodInfo
としてコンパイラをだます。 - Delegateの型はターゲットの型が参照型の場合は
void Action(T instance, /*引数*/)
、値型の場合はvoid Action(ref T instance, /*引数*/)
となるようにする。 - 先ほどのデリゲートを呼び出す。
createDelegate_internal(delegateType, null, construnctorInfo, true)
以上の手順でConstructorInfo
をデリゲート化できました。
new T(args)
の代わりとする場合
//参照型
Func<TArg,T> create=(arg)=>{
var instance=(T)FormatterServices.GetUninitializedObject(type);
constructorDelegate(instance,arg);
return instance;
}
//値型
Func<TArg,T> create=(arg)=>{
var instance=default(T);
constructorDelegate(ref instance,arg);
return instance;
}
IL2CPPで高速化
これでめでたしめでたし、、、、でもいいのですが、CreateDelegate_internalにリフレクションで呼び出すとかはやっぱり嫌ですし、オーバーヘッドももっと減らしたいです。
ということでさらにLow Levelに行きます。
FormatterServices.GetUninitializedObject
は内部では、internal externなActivationServices
のAllocateUninitializedClassInstance
を呼び出しており、そこから先はRuntime依存です。
il2cppではTypeからil2cpp_class_from_type()
でIL2CPPClass*
に変換し、Class::New(IL2CPPClass*)
を行っています。
実は通常のnew でもClass::New(IL2CPPClass*)
を用いており、これを使えれば、オーバーヘッドはなくなります。
でもその関数はC++の関数でアクセスできないと思われるでしょう?
実はできるのです。
NativePlugin開発でよく見る[DllImport("__Internal")]
、
これでil2cppのapiにアクセスできるのです!!!
[DllImport("__Internal", CallingConvention = CallingConvention.Cdecl)]
public static extern Il2CppClass* il2cpp_class_from_type(Il2CppType* type);
[DllImport("__Internal", CallingConvention = CallingConvention.Cdecl)]
public static extern Il2CppObject* il2cpp_object_new(Il2CppClass* klass);
リフレクションに必要な関数は大体揃っていて、私のライブラリのUniReflection.IL2CPP.Il2cppApiでで見ることができます。GCやコマンドライン、icallによるextern methodsを検索、追加できるなど特殊なこともできます。
ということで、アロケーションは最大限高速化できました。
次はConstructor
ですね。
先ほどちらっと言いましたが、ConstructorInfo
にはRuntimeMethodHandle
が入っており、
そこでGetFunctionPointer
できると思った人はなかなかですね。さらにIL2CPPではこの関数がNotSupportedなことを知っていたら、もっとすごい。実はIL2CPPに限りRuntimeMethodHandle
が関数ポインタそのものなことを知っていたらもっともっとすごい。
関数ポインタならCreateDelegate
いらないし、パフォーマンスはDelegateよりちょっと速いくらいです。
ということで、最終的に出来上がったものを見ましょう。
var isValueType = targetType.IsValueType;
var klass = targetType.GetClassHandle();
Func<TArg,T> createInstance;
if (isValueType)
{
var p = (delegate*<ref TInstance, TArg, void>)constructor.MethodHandle.Value;
createInstance = (t0) =>
{
var t = default(TInstance);
p(ref t, t0);
return t;
};
}
else
{
//Il2CppObjectHandleはil2cppでのobjectの表現
var p = (delegate*<Il2CppObjectHandle, TArg, void>)constructor.MethodHandle.Value;
createInstance = (t0) =>
{
var o = Il2CppApi.il2cpp_object_new(klass);
p(o, t0);
return Unsafe.As<Il2CppObjectHandle, TInstance>(ref o);
};
}
最後にもう一度パフォーマンスを見てみましょう。
ばっちりですね!!
最後に
SourceGeneratorによりリフレクション高速化の需要はそんなにはないかもしれませんが、ライブラリとして公開しでいます。
汎用SerializerやDIには使えるかも?
Discussion