From 2e0731b01c518ae4378dcf43b849ad311e80682b Mon Sep 17 00:00:00 2001 From: walon Date: Wed, 3 Jan 2024 22:33:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=B3=A8=E5=85=A5=E7=AD=96?= =?UTF-8?q?=E7=95=A5=E7=9B=B8=E5=85=B3=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/business/accesspolicy.md | 8 +- docs/business/ultimate/injectrules.md | 227 ++++++++++++++++++ docs/business/ultimate/manual.md | 15 ++ .../current/business/ultimate/injectrules.md | 227 ++++++++++++++++++ .../current/business/ultimate/intro.md | 2 +- .../current/business/ultimate/manual.md | 16 +- sidebars.js | 1 + 7 files changed, 490 insertions(+), 6 deletions(-) create mode 100644 docs/business/ultimate/injectrules.md create mode 100644 i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/injectrules.md diff --git a/docs/business/accesspolicy.md b/docs/business/accesspolicy.md index 354dd36cc..d371ad594 100644 --- a/docs/business/accesspolicy.md +++ b/docs/business/accesspolicy.md @@ -47,7 +47,7 @@ 每个Rule包含多个程序集的访问控制规则。每个Rule会计算出一个限制访问的类型集合,最终的限制访问的集合为所有Rule的并集,即只要某个Rule限制访问某个类型, 则最终不允许访问这个类型。 -|属性或元素|类型|可空|描述| +|名称|类型|可空|描述| |-|-|-|-| |id|属性|否|Rule的id。字符串类型,不可为空,必须全局唯一| |assembly|子元素||针对单个程序集的限制集合,Rule下可以包含0-N个assembly,同一个Rule下不可有同名assembly,但不同Rule间可以有同名程序集| @@ -56,7 +56,7 @@ 针对单个程序集的限制规则集合,配置了禁止访问该程序集的哪些类型。 -|属性或元素|类型|可空|描述| +|名称|类型|可空|描述| |-|-|-|-| |fullname|属性|否|程序集名。字符串类型,不可为空。程序集名不包含'.dll'(例如 mscorlib)| |type|子元素||针对单个或者一组类型的限制规则。可以有0-N个元素| @@ -77,7 +77,7 @@ 针对一个或者一组类型的限制规则。 -|属性或元素|类型|可空|描述| +|名称|类型|可空|描述| |-|-|-|-| |fullname|属性|否|类型全名或者类型通配符。如果为类型通配符时,只支持`*`全匹配或者`xx.yy.zz.*`命名空间通配,不支持`xx.yy*`这种前缀通配或者`xx.*.yy`这种任意通配。如果想匹配所有命名空间为空的类型,使用`.*`| |access|属性|是|是否可访问,默认为false。当取 `true, yes, 1` 时为true,取`false, no, 0`时为false| @@ -87,7 +87,7 @@ Target配置了程序集中代码被施加的访问限制规则。 -|属性或元素|类型|可空|描述| +|名称|类型|可空|描述| |-|-|-|-| |assembly|属性|否|访问限制的作用目标程序集,即该程序集中代码访问的函数必须满足rules中指定的访问限制规则| |accessAssemblyNotInRules|属性|是|是否可以访问rules中未涉及的程序集。默认为false,当取 `true, yes, 1` 时为true,取`false, no, 0`时为false| diff --git a/docs/business/ultimate/injectrules.md b/docs/business/ultimate/injectrules.md new file mode 100644 index 000000000..740469e76 --- /dev/null +++ b/docs/business/ultimate/injectrules.md @@ -0,0 +1,227 @@ +# 函数注入策略 + + 为了避免脏函数传染,默认会在所有函数头部注入一小段检查跳转代码。这个注入代码对短函数性能和最终生成的代码长度的影响较为显著(增加30%左右代码)。 + 虽然绝大多数情况下注入代码对整体性能影响微不足道,但罕见的特殊场合下,会观察到这个性能下降现象。 + 自v4.5.9版本起,允许自定义配置这个注入行为。 + + ## 脏函数传染 + + 我们称变化的函数为脏函数。如果未对il2cpp生成的原始代码作任何修改,对于非虚函数调用,存在脏函数链式传染的问题。例如:A函数调用B函数, + B函数调用C函数,如果C函数发生变化,则A,B,C都被会标记为脏函数。在实践中,一些常用的基础函数发生变化,有可能导致巨量的代码被标记为脏函数, + 这显然不是我们期望的。 + + ```csharp + class Foo + { + + public static void A() + { + B(); + } + + public static void B() + { + C(); + } + + public static void C() + { + // 旧代码为 new object(); + // 修改后,导致A、B、C都被标记为脏函数 + new List(); + } + } + + ``` + + ## 间接函数优化技术 + + 我们使用间接函数优化的技术来克服这个问题。il2cpp生成代码时,在DHE函数的头部插入一段检查代码,如果函数未发生变化则继续执行,否则跳转到解释函数执行。 + + +以下面csharp代码为例: + +```csharp + public class IndirectChangedNotInjectMethod + { + public static int ChangeMethod10(int x) + { + return ChangeMethod0(x); + } + + public static int ChangeMethod100(int x) + { + return ChangeMethod10(x); + } + } + +``` + +`ChangeMethod100`函数生成的原始il2cpp代码如下: + +```cpp + 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; + } +} +``` + +插入检查跳转代码后,变成: + + ```cpp + + 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)文件用于 +实现这个目的。 + +### HybridCLR Settings设置 + +在 `HybridCLR Settings`中`InjectRuleFiles`字段中填写注入策略文件路径,文件的相对路径为项目根目录(如`Assets/InjectRules/DefaultInjectRules.xml`)。 + +允许提供0-N个配置策略文件。如果没有任何配置策略文件,则默认对所有DHE程序集的函数注入。 + +### 配置规则 + +配置语法与link.xml非常相似。对于某个函数,如果匹配了多个规则,则以最后一条规则为准。 + +一个典型的注入策略文件如下: + +```xml + + + + 所有属性都不注入 + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 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`也能被`<method name="get_Count">`匹配 + +|名称|类型|可空|描述| +|-|-|-|-| +|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`也能被`<method name="get_Count">`匹配。 + +|名称|类型|可空|描述| +|-|-|-|-| +|name|属性|否|函数名。支持通配符,如'*'、'Run*'之类| +|signature|属性|是|函数签名。支持通配符,如'*'、'System.Int32 \*'| +|mode|子元素|是|注入类型,有效值为'none'或'proxy'。如果不填或者为空则取'none'| + +#### event + +配置事件注入规则。注意,事件被当成`add_{name}`和`remove_{name}`两条函数,因此`Action OnDone`的add函数`add_OnDone`也能被`<method name="add_OnDone">`匹配。 + +|名称|类型|可空|描述| +|-|-|-|-| +|name|属性|否|函数名。支持通配符,如'*'、'Run*'之类| +|signature|属性|是|函数签名。支持通配符,如'*'、'Action<System.Int32> On\*'| +|mode|子元素|是|注入类型,有效值为'none'或'proxy'。如果不填或者为空则取'none'| diff --git a/docs/business/ultimate/manual.md b/docs/business/ultimate/manual.md index ee30d5d5e..8ec843f7a 100644 --- a/docs/business/ultimate/manual.md +++ b/docs/business/ultimate/manual.md @@ -126,6 +126,21 @@ void LoadDifferentialHybridAssembly(string assemblyName, string originalDllMd5) } ``` +## 配置函数注入策略 + +:::tip + +在绝大多数项目中,默认的全注入策略对性能影响微乎其微,只要没有性能问题,不需要也不应该关心此项配置。 + +::: + + 为了避免间接脏函数传染(即A函数调用了B函数,如果B改变了,A也会被标记为改变),默认会在所有函数头部注入一小段检查跳转代码。虽然是 + 非常简单的`if (method->isInterpterImpl)`语句,但对于`int Age {get; set;}`这样的短函数,这种插入可能会产生可观察的性能下降(甚至能达到10%)。 + +函数注入策略用于优化这种情况.对于不会变化的短函数,配置为不注入可以提升性能。详细请见[InjectRules](./injectrules)文档。 + +在 `HybridCLR Settings`中`InjectRuleFiles`字段中填写注入策略文件路径,文件的相对路径为项目根目录(如`Assets/InjectRules/DefaultInjectRules.xml`)。 + ## 打包 diff --git a/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/injectrules.md b/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/injectrules.md new file mode 100644 index 000000000..f85a9987f --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/injectrules.md @@ -0,0 +1,227 @@ +# Function Injection Rules + + In order to avoid dirty function contagion, a small piece of check jump code is injected into the header of all functions by default. This injected code has a significant impact on the performance of short functions and the length of the final generated code (increased code by about 30%). + Although in most cases the impact of injected code on overall performance is negligible, on rare special occasions this performance degradation will be observed. + Since version v4.5.9, custom configuration of this injection behavior is allowed. + + ## Dirty function contagion + + We call changing functions dirty functions. If no modifications are made to the original code generated by il2cpp, there will be a problem of dirty function chain infection for non-virtual function calls. For example: function A calls function B, + Function B calls function C. If function C changes, A, B, and C will all be marked as dirty functions. In practice, changes in some commonly used basic functions may lead to a huge amount of code being marked as dirty functions. + This is obviously not what we expected. + + ```csharp + classFoo + { + + public static void A() + { + B(); + } + + public static void B() + { + C(); + } + + public static void C() + { + //The old code is new object(); + // After modification, A, B, and C are all marked as dirty functions. + new List(); + } + } + + ``` + + ## Indirect function optimization technology + + We use the technique of indirect function optimization to overcome this problem. When il2cpp generates code, it inserts a check code at the head of the DHE function. If the function does not change, execution continues, otherwise it jumps to the interpretation function for execution. + + +Take the following csharp code as an example: + +```csharp + public class IndirectChangedNotInjectMethod + { + public static int ChangeMethod10(int x) + { + return ChangeMethod0(x); + } + + public static int ChangeMethod100(int x) + { + return ChangeMethod10(x); + } + } + +``` + +The original il2cpp code generated by the `ChangeMethod100` function is as follows: + +```cpp + 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; + } +} +``` + +After inserting the check and redirect invoking code, it becomes: + + ```cpp + + 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; + } +} + + ``` + +The injected code contains the following: + + - The metadata initialization block in the header adds the initialization code of the metadata corresponding to the current function. If the function originally does not have any metadata that needs to be initialized, add the entire metadata initialization code block. + - Added a new branch to check the code. If the current function is replaced by interpreted execution, jump to interpreted execution + +For most cases, the injected code only has one additional check `if (method->isInterpterImpl)`, and the impact on overall performance is negligible. But for short functions (such as `int GetValue() { return value; }`), +Since the code of the short function itself is short, there is often no metadata that needs to be initialized, which leads to the introduction of two additional checks and may prevent the function from being inline, resulting in observable performance degradation (10% or more) and significant code bloat. (Added two blocks of code). + +Even if it is not a short function, the injected code causes the overall size of the code file generated by the DHE assembly to increase by 30%. This impact on the package body cannot be ignored. + +In fact, many short functions will not change. Injecting code is unnecessary. Avoiding injection can significantly improve their performance and reduce the size of the final generated cpp code to a certain extent. For this purpose we introduced an injection policy file to configure this behavior. + +## Inject policy file + +We optimize the performance degradation and code bloat caused by indirect function optimization by configuring some or all functions (use with caution, not recommended!) not to inject. Function injection rules (InjectRules) file is used to +achieve this purpose. + +### HybridCLR Settings settings + +Fill in the injection policy file path in the `InjectRuleFiles` field in `HybridCLR Settings`. The relative path of the file is the project root directory (e.g `Assets/InjectRules/DefaultInjectRules.xml`). + +Allows 0-N configuration policy files to be provided. If there is no configuration policy file, function injection for all DHE assemblies is performed by default. + +### Configuration rules + +The configuration syntax is very similar to link.xml. For a function, if multiple rules match, the last rule takes precedence. + +A typical injection policy file is as follows: + +```xml + + + + All properties are not injected + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### rules + +The top-level tag is rules, and rules can contain 0-n assembly rules. + +|Name|Type|Nullable|Description| +|-|-|-|-| +|assembly|child element|no|assembly rules| + +#### assembly + +Configure rules for a certain assembly or type of assembly. + +|Name|Type|Nullable|Description| +|-|-|-|-| +|fullname|property|no|The assembly name without the '.dll' suffix. Support wildcard characters, such as '*', 'Unity.*', 'MyCustom*' and so on | +|type|child elements|are |type rules. Can contain 0-N | + +#### type + +Configure injection rules for a certain type or type. Note that injection rules for generic primitive types are supported, but injection rules for configuring generic instance classes are not supported. For example, you can configure the injection rules of **List\`1**, +But the injection rules of **List<int>** cannot be configured. + +- If a function satisfies multiple rules, the last rule will prevail +- Property is regarded as two functions: `get_{name}` and `set_{name}`, so `int Count` can also be matched by `<method name="get_Count">` + +|Name|Type|Nullable|Description| +|-|-|-|-| +|fullname|Property|No|The full name of the type. Support wildcard characters, such as '*', 'Unity.*', 'MyCustom.*.TestType' and so on | +|method|child element|is|function rule| +|property|child element|is|property rule| +|event|child element|is|event rule| + +#### method + +Configure function injection rules. + +|Name|Type|Nullable|Description| +|-|-|-|-| +|name|property|no|function name. Support wildcard characters, such as '*', 'Run*' and so on | +|signature|property|is|a function signature. Supports wildcard characters, such as '*', 'System.Int32 *(System.Int32)'| +|mode|child elements| are |injection types, valid values ​​are 'none' or 'proxy'. If not filled in or empty, take 'none' | + +#### property + +Configuration property injection rulesbut. Note that the attribute is treated as two functions: `get_{name}` and `set_{name}`, so the getter function `get_Count` of `int Count` can also be matched by `<method name="get_Count">`. + +|Name|Type|Nullable|Description| +|-|-|-|-| +|name|property|no|function name. Support wildcard characters, such as '*', 'Run*' and so on | +|signature|property|is|a function signature. Supports wildcard characters, such as '*', 'System.Int32 \*'| +|mode|child elements| are |injection types, valid values are 'none' or 'proxy'. If not filled in or empty, take 'none' | + +#### event + +Configure event injection rules. Note that the event is treated as two functions: `add_{name}` and `remove_{name}`, so the add function `add_OnDone` of `Action OnDone` can also be matched by `<method name="add_OnDone">`. + +|Name|Type|Nullable|Description| +|-|-|-|-| +|name|property|no|function name. Support wildcard characters, such as '*', 'Run*' and so on | +|signature|property|is|a function signature. Supports wildcard characters, such as '*', 'Action<System.Int32> On\*'| +|mode|child elements| are |injection types, valid values are 'none' or 'proxy'. If not filled in or empty, take 'none' | \ No newline at end of file diff --git a/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/intro.md b/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/intro.md index 6b8f7b013..d75ef5b1a 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/intro.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/intro.md @@ -1,4 +1,4 @@ -# introduce +# Introduce The Ultimate Edition is mainly for projects with strict performance requirements. Compared with the community version, the performance of the flagship version has been greatly improved, basically reaching the native performance level, and at the same time, it has better optimization in terms of security and memory. diff --git a/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/manual.md b/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/manual.md index 1e6dd16f0..21acc4a89 100644 --- a/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/manual.md +++ b/i18n/en/docusaurus-plugin-content-docs/current/business/ultimate/manual.md @@ -1,4 +1,4 @@ -# manual +# Manual ## Install @@ -126,6 +126,20 @@ void LoadDifferentialHybridAssembly(string assemblyName, string originalDllMd5) } ``` +## Configure function injection rules + +:::tip + +In the vast majority of projects, the default full injection rules has minimal impact on performance. As long as there are no performance issues, you do not need and should not care about this configuration. + +::: + + In order to avoid indirect dirty function contagion (that is, function A calls function B, if B changes, A will also be marked as changed), a small piece of check jump code is injected into the header of all functions by default. Although it is + Very simple `if (method->isInterpterImpl)` statement, but for short functions like `int Age {get; set;}`, this insertion may produce an observable performance degradation (even up to 10%). + +The function injection rules is used to optimize this situation. For short functions that do not change, configuring not to inject can improve performance. See the [InjectRules](./injectrules) document for details. + +Fill in the injection policy file path in the `InjectRuleFiles` field in `HybridCLR Settings`. The relative path of the file is the project root directory (such as `Assets/InjectRules/DefaultInjectRules.xml`). ## Pack diff --git a/sidebars.js b/sidebars.js index 938be5c89..ba8eb2ab4 100644 --- a/sidebars.js +++ b/sidebars.js @@ -104,6 +104,7 @@ const sidebars = { 'business/ultimate/intro', 'business/ultimate/quickstart', 'business/ultimate/manual', + 'business/ultimate/injectrules', 'business/ultimate/commonerrors', ], },