英文原文:

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;// 每次调用该函数时都会触发此Ifif (!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);// 访问 Pifloat 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::Xfloat ___X_0;// System.Single MyVector3Class::Yfloat ___Y_1;// System.Single MyVector3Class::Zfloat ___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::Xfloat ___X_0;// System.Single MyVector3::Yfloat ___Y_1;// System.Single MyVector3::Zfloat ___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++。您可能会惊讶于您的发现!

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

  1. 读取文件并输出,输出时将小写字母转为大写

    1.获取文件属性(检查文件):stat() struct stat buf_stat; stat("temp.txt", &buf_stat);// #include &l ...

  2. 读取文件并输出,输出时将小写字母转为大写 [ 2 ]

    1 /*2 * FILE: p48_file2lower.c3 * DATE: 201801064 * --------------5 * DESCRIPTION: 读取文件内容,输出时将小写字母转为 ...

  3. python读取csv文件并修改指定内容-pandas读取CSV文件时查看修改各列的数据类型格式...

    下面给大家介绍下pandas读取CSV文件时查看修改各列的数据类型格式,具体内容如下所述: 我们在调bug的时候会经常查看.修改pandas列数据的数据类型,今天就总结一下: 1.查看: Numpy和 ...

  4. MFC匿名管道原理详解、函数总结、调用实例(用MFC的匿名管道读取CMD输出内容)(C++语言)

    本博客主要总结MFC中匿名管道的原理和具体调用实例,以及调用匿名管道三个核心函数各个参数用法详解,具体的如下所述. 博主在做项目时,遇到一个问题.用程序调用一个进程,然后读取进程输出信息.但是,博主用 ...

  5. MFC程序提示 0xC0000005: 读取位置 0x00000020 时发生访问冲突。

    这个bug困扰我两天了,我在win7下开发的MFC程序,在win7下一只运行良好,放到同事的win10机子上就出问题了,在点击按钮弹出子窗口时必崩,在win7下仔细调试才发现:调用DoModal()后 ...

  6. pythoncsv数据类型_pandas读取CSV文件时查看修改各列的数据类型格式

    下面给大家介绍下pandas读取CSV文件时查看修改各列的数据类型格式,具体内容如下所述: 我们在调bug的时候会经常查看.修改pandas列数据的数据类型,今天就总结一下: 1.查看: Numpy和 ...

  7. RDKit|一站式搞定分子读取、输出、可视化

    文章目录 一.简介 二.读取分子 2.1.读SMILES/SMARTS 2.1.1.直接读字符串 2.1.2.文件批量读取 2.1.3.文本批量读取 2.1.4.DataFrame批量读取 2.2.读 ...

  8. python读取tif图片时保留其16bit的编码格式

    python读取tif图片时保留其16bit的编码格式 tif图片的编码格式一般是16bit的,在使用python-opencv读取tif文件时,为了保留其编码格式,我们需要用以下的方式: impor ...

  9. python修改csv文件中列的数据类型_pandas读取CSV文件时查看修改各列的数据类型格式...

    下面给大家介绍下pandas读取CSV文件时查看修改各列的数据类型格式,具体内容如下所述: 我们在调bug的时候会经常查看.修改pandas列数据的数据类型,今天就总结一下: 1.查看: Numpy和 ...

最新文章

  1. 加载多瓦片地图_手把手教 | 网络时空大数据爬取与分析DAS系统(瓦片地图获取)...
  2. Windows下用FFmpeg+nginx+rtmp搭建直播环境 实现推流、拉流(超简单教程)
  3. 【学习笔记】【Design idea】一、Java异常的设计思想、性能相关、笔记
  4. 通讯实例 modbus_实例讲解PLC实现modbus通讯
  5. iOS之地理位置及定位系统 -- 入门笔记(用Swift)
  6. MySQL故障检测_检测MySQL的表的故障的方法
  7. 数字图像处理 中值滤波 MATLAB实验
  8. Android填坑系列:Android JSONObject 中对key-value为null的特殊处理
  9. gensim实现TF-IDF和LDA模型、sklearn实现聚类
  10. 短视频去水印接口支持全网解析源码/自定义API接口
  11. 如何在Windows 10中扫描文档
  12. 微信连WiFi(sign有误)
  13. 绿色计算产业峰会,易捷行云新一代ARM云平台推动绿色计算产业发展
  14. 锐龙R3-3300X和i5-9400f哪个好?
  15. Android 应用广告过滤几种方式
  16. 如何解决IT公司代码混乱的问题
  17. cgroup driver: cgroupfs还是systemd
  18. VS报出的C2134,C4430,C2238错误
  19. leetcode 笨阶乘
  20. 代理模式 静态代理、JDK动态代理、Cglib动态代理

热门文章

  1. sqlmaphttrack
  2. 历年软考案例分析背景中的明显错误总结(六)
  3. 数据库基础知识---主键和索引的关系
  4. 计算机分组交换的优点缺点,分组交换的优缺点分析
  5. 5G路测速率低优化分析
  6. 计算机导论实践报告带图,计算机导论实践报告.doc
  7. Java实习生面试题分享
  8. 自己开水果店步骤,开水果店的流程
  9. sigmoid函数和tanh函数和relu函数和leaky_relu函数
  10. 深入理解PHP中的count函数