IL2CPP:一般的な実装

IL2CPPシリーズの以前の記事では 、生成されたC ++コードのメソッド呼び出しを調べました。 次に、IL2CPPコードの最も重要な機能の1つ、つまり実行可能なIL2CPPのサイズを大幅に削減できるメソッドの一般的な実装について説明します。 Monoおよび.NETランタイムでも汎用実装が使用されていることに注意してください。 IL2CPPでは、当初サポートされておらず、時間の経過とともに追加されました。







そこで、参照型と値型の一般化されたメソッドの実装と、一般化されたパラメーターの制限による影響を分析します。 この記事で生成されたコードは、Unityの将来のバージョンで変更される可能性があることを忘れないでください。 ただし、原則として、リリース直後にこのような変更について説明します。



汎用実装とは何ですか?



C#でListクラスの実装を記述する必要があると想像してください。 この実装はT型に依存しますか? リスト文字列とリストオブジェクトの両方にAddメソッドの実装を使用できますか? List DateTimeはどうですか?



実際、一般化のプラスは、C#での実装が共有に適していることです。つまり、一般化されたListクラスは任意のタイプTに適しています。しかし、C#から実行可能な何か、たとえば、Monoはどのように実行しますか、IL2CPPの場合はC ++コードですか? その後、Addメソッドの同じ実装を使用できますか?



ほとんどの場合、はい。 この記事で後述するように、一般化された実装の可能性は、T型のサイズにほぼ完全に依存しています。参照型(文字列またはオブジェクト)の場合、そのサイズは常にポインターのサイズに等しくなります。 Tが値型(intまたはDateTime)の場合、そのサイズは変化する可能性があり、これにより状況が少し複雑になります。 最終的に、共通の実装を持つメソッドが多いほど、実行可能コードのサイズは小さくなります。



Monoで汎用実装を実装した開発者であるMark Probstは、これについていくつかの興味深い記事を書いています。 概念自体を深く掘り下げるのではなく、IL2CPPでどのように、どのような条件下で使用されるかについて説明します。 この情報により、プロジェクトの実行可能ファイルのサイズをより完全に把握できると思います。



IL2CPPの一般化された実装の機能



IL2CPPは、Tが参照型(文字列、オブジェクト、またはカスタムクラス)、整数型、または列挙型の場合、SomeGenericType型のジェネリックメソッド実装をサポートします。 値型の場合、フィールドのサイズによってサイズが異なる場合があるため、一般的な実装はサポートされていません。



つまり、SomeGenericType(Tは参照型)を追加しても、実行可能ファイルのサイズにはほとんど影響しません。 一方、Tが値型の場合、結果はより具体的になります。 MonoとIL2CPPでは、これは同じように機能します。 しかし、実装の詳細に直接行きましょう。



仕事の準備



WindowsでUnityバージョン5.0.2p1を使用して、WebGLでプロジェクトをビルドします。 そうすることで、Development Playerオプションを有効にし、Enable Exceptionsの値をNoneに設定します。 最初に、ジェネリック型のインスタンスを作成するドライバーメソッドを作成します。



public void DemonstrateGenericSharing() { var usesAString = new GenericType<string>(); var usesAClass = new GenericType<AnyClass>(); var usesAValueType = new GenericType<DateTime>(); var interfaceConstrainedType = new InterfaceConstrainedGenericType<ExperimentWithInterface>(); }
      
      





次に、このメソッドで使用されるタイプを定義します。



 class GenericType<T> { public T UsesGenericParameter(T value) { return value; } public void DoesNotUseGenericParameter() {} public U UsesDifferentGenericParameter<U>(U value) { return value; } } class AnyClass {} interface AnswerFinderInterface { int ComputeAnswer(); } class ExperimentWithInterface : AnswerFinderInterface { public int ComputeAnswer() { return 42; } } class InterfaceConstrainedGenericType<T> where T : AnswerFinderInterface { public int FindTheAnswer(T experiment) { return experiment.ComputeAnswer(); } }
      
      





すべてのコードは、MonoBehaviourから派生したHelloWorldというクラスにネストされています。 また、このシリーズの最初の記事のように、 il2cpp.exeコマンドラインに-enable-generic-sharingオプションが含まれていないことに気付くかもしれません。 ただし、一般的な実装は発生しますが、現在は自動的に実行されます。



参照型の一般的な実装



まず、最も一般的なケースである参照型を検討してください。 マネージコードでは、これらの型はSystem.Objectから派生し、生成コードではObject_tから派生します。 したがって、C ++コードでそれらを表すには、Object_t *プレースホルダーを使用できます。



DemonstrateGenericSharingメソッドの生成されたバージョンを見つけましょう。 私のプロジェクトでは、HelloWorld_DemonstrateGenericSharing_m4と呼ばれています。 GenericTypeクラスの4つのメソッドの定義に興味があります。 Ctagsを使用して、GenericType_1__ctor_m8(GenericTypeコンストラクター)のメソッドの宣言に進むことができます。 このメソッド宣言は、このメソッドをGenericType_1__ctor_m10447_gsharedメソッドにマップする#defineステートメントであることに注意してください。



次に、GenericType型のメソッド宣言を見つけましょう。 興味深いことに、GenericType_1__ctor_m9コンストラクターの宣言は、同じ関数-GenericType_1__ctor_m10447_gsharedに関連付けられた#define演算子でもあります!

GenericType_1__ctor_m10447_gshared定義コードのコメントは、このメソッドがHelloWorld / GenericType`1 <System.Object> ::。Ctor()マネージメソッドの名前と一致することを示しています。 これは、GenericTypeオブジェクトタイプのコンストラクターであり、完全に一般化されています。GenericTypeタイプを使用する場合、すべての参照タイプTに対して、すべてのメソッドの実装はTがオブジェクトであるバージョンを使用します。



生成されたコードのコンストラクターのすぐ下に、UsesGenericParameterメソッドがあります。



 extern "C" Object_t * GenericType_1_UsesGenericParameter_m10449_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method) { { Object_t * L_0 = ___value; return L_0; } }
      
      





どちらの場合でも、一般化されたパラメーターT(戻り値の型と個々の引数の型)がある場合、生成されたコードではObject_t *型が使用されます。 そして、そのようなコード内のすべての参照型がObject_t *を介して表現できる場合、このメソッドの実装は、参照型である任意のTに対して呼び出すことができます。



このシリーズの2番目の記事では、C ++のメソッド定義はすべて無料の関数であると述べました。 il2cpp.exeユーティリティは、C ++継承を使用してオーバーライドされたC#メソッドを生成するのではなく、型に使用します。 検索に「AnyClass_t」と入力すると、C ++でC#AnyClass型がどのように見えるかを確認できます。



 struct AnyClass_t1 : public Object_t { };
      
      





AnyClass_t1はObject_tから派生しているため、GenericType_1_UsesGenericParameter_m10449_gshared関数への引数としてポインターを渡すだけで済みます。



しかし、戻り値はどうでしょうか? 派生物へのポインタが想定されている基本クラスへのポインタを返すことはできませんか? GenericType :: UsesGenericParameterメソッドの宣言を見てください。



 #define GenericType_1_UsesGenericParameter_m10452(__this, ___value, method) (( AnyClass_t1 * (*) (GenericType_1_t6 *, AnyClass_t1 *, MethodInfo*))GenericType_1_UsesGenericParameter_m10449_gshared)(__this, ___value, method)
      
      







生成されたコードでは、戻り値(Object_t *型)は実際には派生型AnyClass_t1 *になります。 IL2CPPは、C ++型システムを回避するためにC ++コンパイラーをだまします。



制限付きの一般化された実装



T型のオブジェクトでいくつかのメソッドを呼び出すことを許可する必要があるが、Object_t *の使用はこれを妨げないでしょうか? しかし、最初に、一般化された制約を使用してこの考えをC#コンパイラに伝える必要があります。



スクリプトコード、つまりInterfaceConstrainedGenericTypeをもう一度見てください。 このジェネリック型はwhere句を使用して、型TがAnswerFinderInterfaceインターフェイスから派生するようにして、ComputeAnswerメソッドの呼び出しを許可します。 前の記事で、インターフェイスメソッドを呼び出す方法について説明しましたが、vtableでの検索が必要です。 FindTheAnswerメソッドは、制限された型T(Object_t *で表される)のインスタンスに対して直接関数呼び出しを行うため、完全に一般化された実装をC ++コードで使用できます。



HelloWorld_DemonstrateGenericSharing_m4関数の実装からInterfaceConstrainedGenericType_1__ctor_m11関数の定義に移ると、このメソッドはInterfaceConstrainedGenericType_1__ctor_m10456_gshared関数に関連付けられた#define演算子でもあることがわかります。 以下は、InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared関数の実装です。この関数は、Object_t *引数を取り、完全に一般化されています。 関数呼び出しInterfaceFuncInvoker0 :: Invokeを使用すると、マネージメソッドComputeAnswerを呼び出すことができます。



 extern "C" int32_t InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared (InterfaceConstrainedGenericType_1_t2160 * __this, Object_t * ___experiment, MethodInfo* method) { static bool s_Il2CppMethodIntialized; if (!s_Il2CppMethodIntialized) { AnswerFinderInterface_t11_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&AnswerFinderInterface_t11_0_0_0); s_Il2CppMethodIntialized = true; } { int32_t L_0 = (int32_t)InterfaceFuncInvoker0<int32_t>::Invoke(0 /* System.Int32 HelloWorld/AnswerFinderInterface::ComputeAnswer() */, AnswerFinderInterface_t11_il2cpp_TypeInfo_var, (Object_t *)(*(&amp;___experiment))); return L_0; } }
      
      





IL2CPPはすべての管理対象インターフェイスをSystem.Objectとして扱うことを覚えておくことが重要です。 このルールは、il2cpp.exeユーティリティによって生成されたコードに適しています。



基本クラスの制約



インターフェイスの制限に加えて、C#は基本クラスの制限を許可します。 しかし、IL2CPPが基本クラスをSystem.Objectと見なさない場合、汎用実装はどのように機能しますか?



基本クラスは常に参照型であるため、IL2CPPは完全に一般的なメソッドを使用します。 フィールドを使用するか、制限された型のメソッドを呼び出すコードでは、C ++で型キャストが行われます。 繰り返しになりますが、C#コンパイラーはジェネリック制約の正しい実装を提供し、型に関するC ++コンパイラーをごまかしています。



汎用値タイプの実装



HelloWorld_DemonstrateGenericSharing_m4関数に戻って、GenericTypeの実装を見てみましょう。 DateTime型は参照であるため、GenericTypeはジェネリックではありません。 このタイプのコンストラクター、GenericType_1__ctor_m10の宣言に移りましょう。 ここでは、他の場合と同様に#defineが表示されますが、1つのクラス(GenericType)のみが使用するGenericType_1__ctor_m10_gshared関数に関連付けられています。



一般的な実装の概念的な理解



一般化された実装の概念を理解することは非常に困難です。 サブジェクト領域には、病的なケースがたくさんあります(同じ再帰パターン)。 したがって、ここではいくつかの基本原則を強調する必要があります。





ジェネリック型の場合、il2cpp.exeは常に完全にジェネリックなメソッド実装を生成します。 他の実装は、必要な場合にのみ生成されます。



一般化された方法



ジェネリック実装は、ジェネリック型だけでなく、ジェネリックメソッドにも使用されます。 ソースコードでは、UsesDifferentGenericParameterメソッドはGenericTypeクラスとは異なるタイプのパラメーターを使用することに注意してください。 しかし、GenericTypeクラスの汎用実装を検討する際、このメソッドは見当たりませんでした。 「UsesDifferentGenericParameter」を検索すると、このメソッドの実装がGenericMethods0.cppファイルにあることがわかります。



 extern "C" Object_t * GenericType_1_UsesDifferentGenericParameter_TisObject_t_m15243_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method) { { Object_t * L_0 = ___value; return L_0; } }
      
      





これは、Object_t *型をとる完全に一般化された実装です。 そして、このメソッドは一般化されたタイプですが、動作は一般化されていないものと同じです。 il2cpp.exeは、一般化されたパラメーターを使用してメソッドを実装するために、常に最小限のコードを生成しようとすると主張できます。



おわりに



一般化された実装は、リリース以来、IL2CPPの最も重要な改善点の1つです。これにより、同じ動作を行うメソッドの実装のC ++コードのサイズを大幅に削減できます。 バイナリファイルのサイズを削減するソリューションを探し続け、一般化された実装のより多くの利点と機能を使用しようとします。



次の記事では、p / invokeラッパーの生成と、マネージコードとアンマネージコード間の型のマーシャリングについて説明します。



All Articles