英文原文:
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 开销来帮助提高性能。