返回介绍

博客

帮助文档

函数注入策略

发布于 2024-08-03 14:42:56 字数 23290 浏览 0 评论 0 收藏 0

为了避免脏函数传染,默认会在所有函数头部注入一小段检查跳转代码。这个注入代码对短函数性能和最终生成的代码长度的影响较为显著(增加30%左右代码)。 虽然绝大多数情况下注入代码对整体性能影响微不足道,但罕见的特殊场合下,会观察到这个性能下降现象。 自v4.5.9版本起,允许自定义配置这个注入行为。

脏函数传染

我们称变化的函数为脏函数。如果未对il2cpp生成的原始代码作任何修改,对于非虚函数调用,存在脏函数链式传染的问题。例如:A函数调用B函数, B函数调用C函数,如果C函数发生变化,则A,B,C都被会标记为脏函数。在实践中,一些常用的基础函数发生变化,有可能导致巨量的代码被标记为脏函数, 这显然不是我们期望的。

 class Foo
 {

    public static void A()
    {
        B();
    }

    public static void B()
    {
        C();
    }

    public static void C()
    {
        // 旧代码为 new object();
        // 修改后,导致A、B、C都被标记为脏函数
        new List<int>();
    }
 }

间接函数优化技术

我们使用间接函数优化的技术来克服这个问题。il2cpp生成代码时,在DHE函数的头部插入一段检查代码,如果函数未发生变化则继续执行,否则跳转到解释函数执行。

以下面csharp代码为例:

    public class IndirectChangedNotInjectMethod
    {
        public static int ChangeMethod10(int x)
        {
            return ChangeMethod0(x);
        }

        public static int ChangeMethod100(int x)
        {
            return ChangeMethod10(x);
        }
    }

ChangeMethod100函数生成的原始il2cpp代码如下:

 IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR int32_t IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A (int32_t ___0_x, const RuntimeMethod* method) 
{
    {
        // return ChangeMethod10(x);
        int32_t L_0 = ___0_x;
        int32_t L_1;
        L_1 = IndirectChangedNotInjectMethod_ChangeMethod10_m1CFE86C6F8D9E11116BA0F8CACB72A31D4F8401E(L_0, NULL);
        return L_1;
    }
}

插入检查跳转代码后,变成:


 IL2CPP_EXTERN_C IL2CPP_METHOD_ATTR int32_t IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A (int32_t ___0_x, const RuntimeMethod* method) 
{
    static bool s_Il2CppMethodInitialized;
    if (!s_Il2CppMethodInitialized)
    {
        il2cpp_codegen_initialize_runtime_metadata((uintptr_t*)&IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A_RuntimeMethod_var);
        s_Il2CppMethodInitialized = true;
    }
    method = IndirectChangedNotInjectMethod_ChangeMethod100_mFE65234D8ACE343677581C1D96E05E9DFC7C2D1A_RuntimeMethod_var;
    if (method->isInterpterImpl)
    {
        typedef int32_t (*RedirectFunc)(int32_t, const RuntimeMethod*);
        return ((RedirectFunc)method->methodPointerCallByInterp)(___0_x, method);
    }
    {
        // return ChangeMethod10(x);
        int32_t L_0 = ___0_x;
        int32_t L_1;
        L_1 = IndirectChangedNotInjectMethod_ChangeMethod10_m1CFE86C6F8D9E11116BA0F8CACB72A31D4F8401E(L_0, NULL);
        return L_1;
    }
}

注入代码包含以下内容:

  • 头部的元数据初始块增加了当前函数对应的元数据的初始化代码。如果函数原来没有任何需要初始化的元数据,则新增整个元数据初始代码块
  • 新增一个分支检查代码。如果当前函数被替换为解释执行,则跳转到解释执行

对于大多数情况,注入代码只多了一次额外检查if (method->isInterpterImpl),对整体性能影响可忽略不计。但对于短函数(如 int GetValue() { return value; }), 由于短函数本身代码简短,往往没有需要初始化的元数据,导致引入了两次额外检查,并有可能阻止了函数inline,造成可观测的性能下降(10%甚至更多)和显著的代码膨胀(增加了两块代码)。

即使不是短函数,注入代码导致DHE程序集生成的代码文件整体大小增加了30%,这个对包体影响不可忽视。

其实很多短函数并不会发生变化,注入代码是不必要的,避免注入可以显著提升它们的性能,也能一定程度减少最终生成的cpp代码大小。为此我们引入了注入策略文件来配置这个行为。

注入策略文件

我们通过配置部分或者全部函数(慎用,不推荐!)不注入来优化间接函数优化带来的性能下降和代码膨胀问题。函数注入策略(InjectRules)文件用于 实现这个目的。

:::tip 即使某个函数被标记为不注入,后续热更新中修改了此函数,并不会导致运行出错或者执行了旧逻辑,只会导致脏函数传染问题,即所有直接调用了此函数的函数都会被标记为脏函数。 :::

HybridCLR Settings设置

HybridCLR SettingsInjectRuleFiles字段中填写注入策略文件路径,文件的相对路径为项目根目录(如Assets/InjectRules/DefaultInjectRules.xml)。

允许提供0-N个配置策略文件。如果没有任何配置策略文件,则默认对所有DHE程序集的函数注入。

配置规则

配置语法与link.xml非常相似。对于某个函数,如果匹配了多个规则,则以最后一条规则为准。

一个典型的注入策略文件如下:

<rules>
    <assembly fullname="*">
        <type fullname="*">
            <property name="*"/> 所有属性都不注入
        </type>
    </assembly>
    <assembly fullname="AssemblyA">
        <type fullname="Type1">
            <method name="*"/>
        </type>
        <type fullname="Type2">
            <property name="Age*"/>
            <property name="Age_3" mode="proxy"/>
            <property name="Count" mode="none"/>
            <property signature="System.String Name"/>
            <method name="Run*"/>
            <method name="Run_3" mode="proxy"/>
            <method name="Foo"/>
            <method signature="System.Int32 Sum(System.Int32,System.Int32)"/>
            <method signature="System.Int32 Sum2(System.Int32,System.Int32)"/>
            <event name="OnEvent*"/>
            <event name="OnEvent_3" mode="proxy"/>
            <event name="OnHello"/>
        </type>
    </assembly>
    <assembly fullname="AssemblyB">
        <type fullname="*">
            <method name="*"/>
        </type>
    </assembly>
</rules>

rules

最顶层tag为rules,rules下可以包含0-n个assembly规则。

名称类型可空描述
assembly子元素程序集规则

assembly

配置针对某个或者一类程序集的规则。

名称类型可空描述
fullname属性程序集名称,不含'.dll'后缀。支持通配符,如''、'Unity.'、'MyCustom*'之类
type子元素类型规则。可以包含0-N个

type

配置针对某个或某一类类型的注入规则。注意,支持泛型原始类型的注入规则,但不支持配置泛型实例类的注入规则。例如可以配置 List`1 的注入规则, 但不能配置List<int>的注入规则。

  • 如果某个函数满足多条规则,则以最后一条规则为准
  • property被当成 get_{name}set_{name}两条函数,因此int Count也能被&lt;method name="get_Count"&gt;匹配
名称类型可空描述
fullname属性类型全名称。支持通配符,如''、'Unity.'、'MyCustom.*.TestType'之类
method子元素函数规则
property子元素属性规则
event子元素事件规则

method

配置函数注入规则。

名称类型可空描述
name属性函数名。支持通配符,如''、'Run'之类
signature属性函数签名。支持通配符,如''、'System.Int32 (System.Int32)'
mode子元素注入类型,有效值为'none'或'proxy'。如果不填或者为空则取'none'

property

配置属性注入规则。注意,属性被当成 get_{name}set_{name}两条函数,因此int Count的getter函数get_Count也能被&lt;method name="get_Count"&gt;匹配。

名称类型可空描述
name属性函数名。支持通配符,如''、'Run'之类
signature属性函数签名。支持通配符,如''、'System.Int32 \'
mode子元素注入类型,有效值为'none'或'proxy'。如果不填或者为空则取'none'

event

配置事件注入规则。注意,事件被当成add_{name}remove_{name}两条函数,因此Action OnDone的add函数add_OnDone也能被&lt;method name="add_OnDone"&gt;匹配。

名称类型可空描述
name属性函数名。支持通配符,如''、'Run'之类
signature属性函数签名。支持通配符,如''、'Action<System.Int32> On\'
mode子元素注入类型,有效值为'none'或'proxy'。如果不填或者为空则取'none'

代码生成注入规则

手动添加注入规则有可能是一件比较繁琐的事情,当按名字通配不能满足需求时,例如当你想对指令数小于10的短函数不注入时,代码生成相应的注入规则可以极大简化 构造注入规则的工作量。

代码实现生成注入规则不复杂,大致就是遍历每个DHE程序集,如果函数满足某个规则,则添加相应的注入规则。示例代码如下:


public static void GenerateInjectRule(List<string> dheAssemblyNames, string outputInjectRuleFile)
{
    int minInjectMethodInstructions = 10;

    foreach (string dheDllPath in dheAssemblyNames)
    {
        using (var dheMod = ModuleDefMD.Load(dheDllPath))
        {
            // 添加注入规则  <assembly fullname="{dheMod.Assembly.Name}" />
            for (uint i = 1, n = dheMod.Metadata.TablesStream.MethodTable.Rows; i <= n; i++)
            {
                MethodDef methodDef = dheMod.ResolveMethod(i);
                if (methodDef.HasBody && methodDef.Body.Instructions.Count < minInjectMethodInstructions)
                {
                    // 添加注入规则
                    // <type name="{methodDef.DeclaringType.Name}">
                    // <method name="{methodDef.Name}" />
                    // </type>
                }
            }
        }
    }
}

构建工作流相关

注入策略文件需要与构建的主包一致,既每个独立发布的主包都必须备份当时所用的注入策略文件。就如每次生成dhao文件时需要使用构建主包时生成的AOT dll, 生成dhao文件时必须使用构建主包时备份的注入策略文件。如果使用了错误注入策略文件,会导致生成错误dhao文件,这有可能会导致运行了错误的逻辑甚至崩溃!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文