[IL2CPP] 在编译时获取struct的大小

英文原文:

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

  通过阅读 IL2CPP 输出的 C++ 代码,我继续学到很多东西。就像阅读反编译的代码一样,它可以让我们深入了解 Unity 的构建过程如何使用我们提供的 C#。本周我了解到 sizeof(MyStruct) 不像在 C++ 中那样是编译时常量。因此,IL2CPP 每次使用时都会生成一些不太理想的 C++ 代码。今天的文章展示了我解决该问题的过程,并最终得到了一些代码,您可以将其放入项目中以避免该问题。

  能够使用 sizeof(MyStruct) 是在非托管内存中使用它们的基础。这也是我们跳过重重障碍的原因,所以我们可以做一些事情,比如分配一个数组:

static Vector3* AllocOneThousandVectors()
{
	return (Vector3*)Marshal.AllocHGlobal(sizeof(Vector3) * 1000);
}

  由于 Vector3 仅包含三个浮点字段,因此可以合理地假设它的大小在编译时是已知的。毕竟, sizeof(float) 是一个编译时常量。我们知道这一点是因为我们可以使用 const 关键字:

const int sizeofFloat = sizeof(float);

  但是如果我们尝试在 Vector3 中使用 const 关键字,我们会得到一个编译器错误:

const int sizeofVector3 = sizeof(Vector3);

error CS0133: The expression being assigned to `sizeofVector3’ must be constant

  这大概是因为运行 C# 代码的脚本环境可以保持其选项处于打开状态。它可能会选择对齐 Vector3 的字段,以使它们不会在内存中紧密地打包。

  那么 IL2CPP 输出什么而不是仅仅用 12 替换 sizeof(Vector3) 呢?只需进行 iOS 构建并搜索“AllocOneThousandVectors”,您就会看到:(由我注释和格式化)

extern "C"  Vector3_t2243707580 * TestScript_AllocOneThousandVectors_m2221219875 (Il2CppObject * __this /* static, unused */, const MethodInfo* method)
{
	// 每次调用都会检查的静态布尔值
	// C++11 要求这是线程安全的
	static bool s_Il2CppMethodInitialized;
	if (!s_Il2CppMethodInitialized)
	{
		// il2cpp_codegen_initialize_method 看起来像这样:
		//   il2cpp::vm::MetadataCache::InitializeMethodMetadata(index);
		// InitializeMethodMetadata 没有来源
		il2cpp_codegen_initialize_method (TestScript_AllocOneThousandVectors_m2221219875_MetadataUsageId);
		s_Il2CppMethodInitialized = true;
	}
	{
		// 请参阅下面的内容...
		uint32_t L_0 = il2cpp_codegen_sizeof(Vector3_t2243707580_il2cpp_TypeInfo_var);
 
		// 请参阅下面的内容...
		IL2CPP_RUNTIME_CLASS_INIT(Marshal_t785896760_il2cpp_TypeInfo_var);
 
		// 实际的工作
		IntPtr_t L_1 = Marshal_AllocHGlobal_m4258042074(NULL /*static, unused*/, ((int32_t)((int32_t)L_0*(int32_t)((int32_t)1000))), /*hidden argument*/NULL);
		void* L_2 = IntPtr_op_Explicit_m1073656736(NULL /*static, unused*/, L_1, /*hidden argument*/NULL);
		return (Vector3_t2243707580 *)(L_2);
	}
}
 
inline uint32_t il2cpp_codegen_sizeof(Il2CppClass* klass)
{
	// 它是一个结构,它是一个值类型。
	// 无论如何,这仍然每次都会被检查
	if (!klass->valuetype)
	{
		return sizeof(void*);
	}
 
	// 无论如何,这仍然每次都会被检查
	return il2cpp::vm::Class::GetInstanceSize(klass) - sizeof(Il2CppObject);
}
 
#define IL2CPP_RUNTIME_CLASS_INIT(klass) \
	// 这个 do-while 技巧(可悲的是)在 C/C++ 宏中是正常的。
	// 别担心——它并不会真正产生循环。
	do { \
		// 另一个“If”来检查一些关于该类是否的标志
		// 有一个静态构造函数,它完成了
		if((klass)->has_cctor && !(klass)->cctor_finished) \
			// 有一个静态构造函数,它完成了
			il2cpp::vm::Runtime::ClassInit ((klass)); \
	} while (0)

  所以这是相当多的开销,而不是应该只是 12!有一个静态变量、几个 if 语句和几个函数调用。这在做一些一次性工作时很好,比如分配一堆向量,但是如果你使用 sizeof 进行日常工作,那么开销可能会超过你正在尝试做的实际工作。请记住,当索引到非托管结构数组(如此处的 Vector3* 数组)时,会隐式使用 sizeof。

  我们知道我们的游戏将在 x86 或 ARM 处理器上运行,并且 x、y 和 z 字段将是连续的。每个浮点字段为 4 个字节,因此向量的大小为 12 个字节。

  这导致了解决方法 - 只需使用一个常量:

const int sizeofVector3 = 12;

  不幸的是,这并不总是那么容易。一方面,有些结构在编译时我们无法知道其大小。有两个原因。

  第一个原因是结构包含非值类型,例如字符串。应该将结构转换为使用对象句柄,但在那之前我们根本无法知道它的大小。如果我们尝试,我们会得到一个错误:

error CS0208: Cannot take the address of, get the size of, or declare a pointer to a managed type `SomeStruct’

  第二个原因是该结构包含一个指针。指针的大小取决于 CPU 是 32 位还是 64 位。许多游戏需要同时支持两者,尤其是在移动设备上。不幸的是,我们的 C# 代码编译为 DLL,可用于 32 位和 64 位处理器。根本没有适用于两者的常数。在这种情况下,我们不得不使用 sizeof 并承担开销。

  这种解决方法适用于像 Vector3 这样从不改变的东西,但我们自己的结构会经常更改它们的字段。手动重新计算尺寸,考虑到一些复杂的对齐规则,既容易出错又耗时。那么为什么不利用一些代码生成来为我们制作这些常量呢?

  为了演示,我制作了一个简单的编辑器脚本,它生成了一个名为 TypeSizes.cs 的文件。它包含一个带有静态字段的静态类,这些静态字段指示游戏中没有任何对象字段的所有结构的大小。当没有指针字段时,它使用 const。当有指针字段时,它使用静态只读和 sizeof。这是一个示例生成的文件的样子:


// Autogenerated code! Do not modify! //

 
unsafe public static class TypeSizes
{
	public const uint MyVector3 = 12;
	public static readonly uint Player = (uint)sizeof(Player);
 
#if UNITY_EDITOR
	static TypeSizes()
	{
		if (
			Entity != sizeof(Entity) ||
			EntityEntry != sizeof(EntityEntry) ||
			PositionComponent != sizeof(PositionComponent) ||
			VelocityComponent != sizeof(VelocityComponent))
		{
			UnityEngine.Debug.LogError("TypeSizes is stale. Re-run code generator.");
			UnityEditor.EditorApplication.isPlaying = false;
		}
	}
#endif
}

  除了实际大小之外,还包括一个静态构造函数。在编辑器中运行时,它会检查以确保所有 const 大小都是正确的。因此,如果您更改结构的字段,您将收到一条错误消息,警告您重新运行代码生成器。您还将退出播放模式,因为继续操作不安全。

生成它的编辑器脚本非常小:大约 150 行,包括注释。

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using UnityEditor;
using UnityEngine;
 
/// <summary>
/// 用于生成 C# 文件的编辑器脚本,该文件包含具有值类型大小的静态类
/// in the runtime project. 
/// </summary>
/// <author>JacksonDunstan.com/articles/3921</author>
/// <license>MIT</license>
public class GenerateTypeSizes
{
	[MenuItem("Code Generators/Type Sizes")]
	public static void Generate()
	{
		// 使用运行时代码查找程序集
		Assembly runtimeAssembly = null;
		foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
		{
			if (assembly.FullName.StartsWith("Assembly-CSharp,"))
			{
				runtimeAssembly = assembly;
				break;
			}
		}
		if (runtimeAssembly == null)
		{
			Debug.LogError("Couldn't find runtime assembly. No generating code.");
			return;
		}
 
		// 覆盖输出文件
		const string outputClassName = "TypeSizes";
		string path = Path.Combine(Application.dataPath, outputClassName) + ".cs";
		using (FileStream stream = File.OpenWrite(path))
		{
			// 清除现有文件
			stream.SetLength(0);
			stream.Flush();
 
			// 写下大小
			// 还要列出常量类型
			List<Type> constTypes = new List<Type>(1024);
			StreamWriter writer = new StreamWriter(stream);
			writer.Write("\n");
			writer.Write("// Autogenerated code! Do not modify! //\n");
			writer.Write("\n");
			writer.Write('\n');
			writer.Write("unsafe public static class ");
			writer.Write(outputClassName);
			writer.Write('\n');
			writer.Write("{\n");
			foreach (Type type in runtimeAssembly.GetTypes())
			{
				// 仅输出值类型
				// 跳过输出类型本身
				// 跳过枚举
				if (type.IsValueType && !type.IsEnum && type.Name != outputClassName)
				{
					// 确保它没有除指针之外的任何非值字段类型
					bool hasPointers = false;
					foreach (FieldInfo field in type.GetFields())
					{
						bool isPointer = field.FieldType.IsPointer;
						if (isPointer)
						{
							hasPointers = true;
						}
						if (!field.FieldType.IsValueType && !isPointer)
						{
							Debug.LogWarningFormat(
								"{0}.{1} is a {2}, which is not a value type. Not outputting size.",
								type.Name,
								field.Name,
								field.FieldType);
							goto continueOuterLoop;
						}
					}
 
					writer.Write("\tpublic ");
 
					// 指针的大小因 CPU 架构而异(例如 32 位与 64 位)
					// 我们被迫在运行时使用 sizeof(MyStruct) 来确定它
					if (hasPointers)
					{
						writer.Write("static readonly uint ");
						writer.Write(type.Name);
						writer.Write(" = (uint)sizeof(");
						writer.Write(type.Name);
						writer.Write(')');
					}
					// 没有指针而只有值类型的结构的大小可以是
					// 由 Marshal.SizeOf 确定。
					else
					{
						writer.Write("const uint ");
						writer.Write(type.Name);
						writer.Write(" = ");
						writer.Write(Marshal.SizeOf(type));
						constTypes.Add(type);
					}
 
					writer.Write(";\n");
				}
				continueOuterLoop:;
			}
 
			// 输出静态构造函数以检查常量在编辑器中运行时是否有效
			writer.Write("\t\n");
			writer.Write("#if UNITY_EDITOR\n");
			writer.Write("\tstatic ");
			writer.Write(outputClassName);
			writer.Write("()\n");
			writer.Write("\t{\n");
			writer.Write("\t\tif (\n");
			for (int i = 0, len = constTypes.Count; i < len; ++i)
			{
				Type type = constTypes[i];
				writer.Write("\t\t\t");
				writer.Write(type.Name);
				writer.Write(" != sizeof(");
				writer.Write(type.Name);
				writer.Write(")");
				if (i != len - 1)
				{
					writer.Write(" ||\n");
				}
				else
				{
					writer.Write(")\n");
				}
			}
			writer.Write("\t\t{\n");
			writer.Write(
				"\t\t\tUnityEngine.Debug.LogError(\"TypeSizes is stale. Re-run code generator.\");\n");
			writer.Write("\t\t\tUnityEditor.EditorApplication.isPlaying = false;\n");
			writer.Write("\t\t}\n");
			writer.Write("\t}\n");
			writer.Write("#endif\n");
 
			// End the file
			writer.Write("}\n");
			writer.Flush();
		}
 
		// Done
		AssetDatabase.Refresh();
		Debug.LogFormat("Successfully generated {0}.cs", outputClassName);
	}
}

  只需将其放入项目中的 Editor 文件夹,然后单击 Code Generators -> Type Sizes 菜单项。它运行得非常快,所以很容易重新生成类型大小。

  我希望您发现此解决方法和此代码生成应用程序很有用。它当然可以通过减少一些 IL2CPP 开销来帮助提高性能。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值