[IL2CPP] 我在读取 IL2CPP 输出时遇到的三个惊喜

英文原文:

https://www.jacksondunstan.com/articles/3916

  我们使用 C# 编写代码,但这只是一个起点。我们的 C# 代码被编译为 DLL,然后转换为 C++,然后再次编译为机器代码。好消息是,这不是一个黑匣子!我最近一直在阅读 IL2CPP 输出的 C++ 代码并学到了很多东西。今天的文章是关于我遇到的一些惊喜以及如何更改 C# 代码以避免一些讨厌的陷阱。

静态变量

  假设我们编写了一个使用静态变量的静态函数。这是基本圆几何:

static class CircleFunctions
{
	private static readonly float Pi = 3.14f;
 
	public static float Area(float radius)
	{
		return Pi * radius * radius;
	}
}

  请注意,Pi是一个静态变量,而不是一个常数。通常你会把它变成一个常数,但在这个例子中我们将使用一个静态变量。有很多时候你都不能使用常数。

  现在我们来看看IL2CPP为Area生成的C++代码。我在其中加入了内联注释和一些间距,解释了正在发生的事情。

extern "C"  float CircleFunctions_Area_m4188038 (Il2CppObject * __this /* static, unused */, float ___radius0, const MethodInfo* method)
{
	// 仅限于此函数的静态变量
	// 这用于对方法进行一次性初始化
	static bool s_Il2CppMethodInitialized;
 
	// 每次调用该函数时都会触发此If
	if (!s_Il2CppMethodInitialized)
	{
		il2cpp_codegen_initialize_method (CircleFunctions_Area_m4188038_MetadataUsageId);
		s_Il2CppMethodInitialized = true;
	}
	{
		// This macro expands to this:
		//   do {
		//     if((klass)->has_cctor && !(klass)->cctor_finished)
		//       il2cpp::vm::Runtime::ClassInit ((klass));
		//   } while (0)
		// 每次调用函数时都会发生这种情况
		IL2CPP_RUNTIME_CLASS_INIT(CircleFunctions_t532702825_il2cpp_TypeInfo_var);
 
		// 访问 Pi
		float L_0 = ((CircleFunctions_t532702825_StaticFields*)CircleFunctions_t532702825_il2cpp_TypeInfo_var->static_fields)->get_Pi_0();
 
		// 实际的工作
		float L_1 = ___radius0;
		float L_2 = ___radius0;
		return ((float)((float)((float)((float)L_0*(float)L_1))*(float)L_2));
	}
}

  这应该是一个简单的功能,但变成了相当复杂的东西。 IL2CPP 增加了很多开销。现在让我们看一个调用它的函数:

static void TestStaticFunctionUsingStaticVariable()
{
	float area = CircleFunctions.Area(3.0f);
}

  这是来自 IL2CPP 的 C++:

extern "C"  void TestScript_TestStaticFunctionUsingStaticVariable_m953893846 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
	// 用于一次性初始化的更多静态布尔值
	static bool s_Il2CppMethodInitialized;
	if (!s_Il2CppMethodInitialized)
	{
		il2cpp_codegen_initialize_method (TestScript_TestStaticFunctionUsingStaticVariable_m953893846_MetadataUsageId);
		s_Il2CppMethodInitialized = true;
	}
	float V_0 = 0.0f;
	{
		// 更多类初始化 (same macro as above)
		IL2CPP_RUNTIME_CLASS_INIT(CircleFunctions_t532702825_il2cpp_TypeInfo_var);
 
		// 正真的工作:
		float L_0 = CircleFunctions_Area_m4188038(NULL /*static, unused*/, (3.0f), /*hidden argument*/NULL);
		V_0 = L_0;
		return;
	}
}

  所以即使是这个函数的调用者也需要为静态变量付出代价。哎哟。

  现在让我们看看如果你避免使用静态变量会发生什么。在这种情况下,很容易只使用 const:

static class CircleFunctionsConst
{
	private const float Pi = 3.14f;
 
	public static float Area(float radius)
	{
		return Pi * radius * radius;
	}
}

这是 C++:

extern "C"  float CircleFunctionsConst_Area_m2838794717 (Il2CppObject * __this /* static, unused */, float ___radius0, const MethodInfo* method)
{
	{
		float L_0 = ___radius0;
		float L_1 = ___radius0;
		return ((float)((float)((float)((float)(3.14f)*(float)L_0))*(float)L_1));
	}
}

开销没了!只剩下实际的工作。那么调用者呢?

static void TestStaticFunctionNotUsingStaticVariable()
{
	float area = CircleFunctionsConst.Area(3.0f);
}
extern "C"  void TestScript_TestStaticFunctionNotUsingStaticVariable_m2848480467 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
	float V_0 = 0.0f;
	{
		float L_0 = CircleFunctionsConst_Area_m2838794717(NULL /*static, unused*/, (3.0f), /*hidden argument*/NULL);
		V_0 = L_0;
		return;
	}
}

  所有令人讨厌的开销都完全消失了。

建议:考虑使用常量和参数而不是静态变量。

结构初始化

  现在让我们做一个简单的struct:

struct MyVector3
{
	public float X;
	public float Y;
	public float Z;
 
	public MyVector3(float x, float y, float z)
	{
		X = x;
		Y = y;
		Z = z;
	}
}

  让我们用默认构造函数初始化它,然后设置它的字段:

static void TestDefaultStructConstructor()
{
	MyVector3 vec = new MyVector3();
	vec.X = 1;
	vec.Y = 2;
	vec.Z = 3;
}

这是在 C++ 中的样子:

extern "C"  void TestScript_TestDefaultStructConstructor_m1260349596 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
	// The static variable overhead is back!
	static bool s_Il2CppMethodInitialized;
	if (!s_Il2CppMethodInitialized)
	{
		il2cpp_codegen_initialize_method (TestScript_TestDefaultStructConstructor_m1260349596_MetadataUsageId);
		s_Il2CppMethodInitialized = true;
	}
 
	MyVector3_t770449606  V_0;
 
	// 该struct 默认为全零
	memset(&V_0, 0, sizeof(V_0));
	{
		// Initobj 看起来像这样:
		//   inline void Initobj(Il2CppClass* type, void* data)
		//   {
		//       if (type->valuetype)
		//           memset(data, 0, type->instance_size - sizeof(Il2CppObject));
		//       else
		//           *static_cast<Il2CppObject**>(data) = NULL;
		//   }
		// 尽管我们知道这是一个struct,但还有一个“If”的开销
		// 然后struct被再次清零
		Initobj (MyVector3_t770449606_il2cpp_TypeInfo_var, (&V_0));
 
		// 设置字段。 这些“set_*”函数是微不足道的传递。
		(&V_0)->set_X_0((1.0f));
		(&V_0)->set_Y_1((2.0f));
		(&V_0)->set_Z_2((3.0f));
		return;
	}
}

  为什么那个静态开销又回来了?我们能摆脱它吗?让我们尝试一个对象初始化器语法:

static void TestStructInitializer()
{
	MyVector3 vec = new MyVector3 { X = 1, Y = 2, Z = 3 };
}
extern "C"  void TestScript_TestStructInitializer_m3484430381 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
	// 相同的静态变量开销
	static bool s_Il2CppMethodInitialized;
	if (!s_Il2CppMethodInitialized)
	{
		il2cpp_codegen_initialize_method (TestScript_TestStructInitializer_m3484430381_MetadataUsageId);
		s_Il2CppMethodInitialized = true;
	}
 
	// 相同的静态变量开销
	MyVector3_t770449606  V_0;
	memset(&V_0, 0, sizeof(V_0));
 
	// 声明另一个结构?为什么?
	// 同时,将其清除为零。
	MyVector3_t770449606  V_1;
	memset(&V_1, 0, sizeof(V_1));
	{
		// _再次_将其中一个结构清零
		Initobj (MyVector3_t770449606_il2cpp_TypeInfo_var, (&V_1));
 
		// 设置所有字段
		(&V_1)->set_X_0((1.0f));
		(&V_1)->set_Y_1((2.0f));
		(&V_1)->set_Z_2((3.0f));
 
		// 将一个结构复制到另一个结构
		MyVector3_t770449606  L_0 = V_1;
		V_0 = L_0;
		return;
	}
}

  字段初始化器增加了更多开销!现在在静态变量初始化代码之上有两个结构、一个副本和三个不必要的清零。

好的,让我们尝试一个自定义构造函数:

static void TestCustomStructConstructor()
{
	MyVector3 vec = new MyVector3(1, 2, 3);
}
extern "C"  void TestScript_TestCustomStructConstructor_m3485483736 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
	MyVector3_t770449606  V_0;
	memset(&V_0, 0, sizeof(V_0));
	{
		MyVector3__ctor_m3460461338((&V_0), (1.0f), (2.0f), (3.0f), /*hidden argument*/NULL);
		return;
	}
}
 
extern "C"  void MyVector3__ctor_m3460461338 (MyVector3_t770449606 * __this, float ___x0, float ___y1, float ___z2, const MethodInfo* method)
{
	{
		float L_0 = ___x0;
		__this->set_X_0(L_0);
		float L_1 = ___y1;
		__this->set_Y_1(L_1);
		float L_2 = ___z2;
		__this->set_Z_2(L_2);
		return;
	}
}

  自定义构造函数摆脱了所有静态变量开销和额外的结构。它全部替换为设置字段的函数(构造函数)。不幸的是,即使 C# 语言要求构造函数设置所有字段并且在设置它们之前不访问它们,IL2CPP 仍然在调用构造函数以将结构清零之前生成了 memset 调用。这是我们将获得的最低开销。

建议:考虑使用自定义构造函数而不是默认构造函数和对象初始值设定项。

类的开销

最后,让我们制作上述结构的类版本:

class MyVector3Class
{
	public float X;
	public float Y;
	public float Z;
 
	public MyVector3Class(float x, float y, float z)
	{
		X = x;
		Y = y;
		Z = z;
	}
}

以下是它在 C++ IL2CPP 生成中的样子:

struct  MyVector3Class_t1350799278  : public Il2CppObject
{
public:
	// System.Single MyVector3Class::X
	float ___X_0;
	// System.Single MyVector3Class::Y
	float ___Y_1;
	// System.Single MyVector3Class::Z
	float ___Z_2;
 
public:
	inline static int32_t get_offset_of_X_0() { return static_cast<int32_t>(offsetof(MyVector3Class_t1350799278, ___X_0)); }
	inline float get_X_0() const { return ___X_0; }
	inline float* get_address_of_X_0() { return &___X_0; }
	inline void set_X_0(float value)
	{
		___X_0 = value;
	}
 
	inline static int32_t get_offset_of_Y_1() { return static_cast<int32_t>(offsetof(MyVector3Class_t1350799278, ___Y_1)); }
	inline float get_Y_1() const { return ___Y_1; }
	inline float* get_address_of_Y_1() { return &___Y_1; }
	inline void set_Y_1(float value)
	{
		___Y_1 = value;
	}
 
	inline static int32_t get_offset_of_Z_2() { return static_cast<int32_t>(offsetof(MyVector3Class_t1350799278, ___Z_2)); }
	inline float get_Z_2() const { return ___Z_2; }
	inline float* get_address_of_Z_2() { return &___Z_2; }
	inline void set_Z_2(float value)
	{
		___Z_2 = value;
	}
};

  有很多样板的“get”和“set”函数,但是这个类大多是我们所期望的。它具有三个浮点字段,并且它派生自 System.Object(又名对象),这是您未显式声明基类时的默认设置。这就是众所周知的 Il2CppObject,所以让我们看一下:

struct Il2CppObject
{
    Il2CppClass *klass;
    MonitorData *monitor;
};

  这意味着我们类实例的大小不仅仅是三个浮点变量,还有两个指针的大小。在 64 位平台上,需要额外的 16 字节存储空间。因此,我们的向量实例实际上需要 40 个字节,而不是需要 24 个字节,增加了 66%。这是一个固定的开销,因此对于较大的类来说并不重要,但对于您有很多的较小的类,绝对要注意一些事情。

为了比较,让我们看一下 struct 版本的 C++:

struct  MyVector3_t770449606 
{
public:
	// System.Single MyVector3::X
	float ___X_0;
	// System.Single MyVector3::Y
	float ___Y_1;
	// System.Single MyVector3::Z
	float ___Z_2;
 
public:
	inline static int32_t get_offset_of_X_0() { return static_cast<int32_t>(offsetof(MyVector3_t770449606, ___X_0)); }
	inline float get_X_0() const { return ___X_0; }
	inline float* get_address_of_X_0() { return &___X_0; }
	inline void set_X_0(float value)
	{
		___X_0 = value;
	}
 
	inline static int32_t get_offset_of_Y_1() { return static_cast<int32_t>(offsetof(MyVector3_t770449606, ___Y_1)); }
	inline float get_Y_1() const { return ___Y_1; }
	inline float* get_address_of_Y_1() { return &___Y_1; }
	inline void set_Y_1(float value)
	{
		___Y_1 = value;
	}
 
	inline static int32_t get_offset_of_Z_2() { return static_cast<int32_t>(offsetof(MyVector3_t770449606, ___Z_2)); }
	inline float get_Z_2() const { return ___Z_2; }
	inline float* get_address_of_Z_2() { return &___Z_2; }
	inline void set_Z_2(float value)
	{
		___Z_2 = value;
	}
};

这个版本几乎相同,只是它没有这两个指针的开销。

建议:当您需要节省每个实例的内存时,考虑使用结构而不是类。

总结

  IL2CPP 生成的 C++ 代码充满了惊喜。花一些时间检查一下游戏中的已知热点。您可以简单地搜索“MyClass::MyFunction”并轻松找到与您的 C# 代码等效的 C++。您可能会惊讶于您的发现!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值