可收集的动态程序集中的静态字段访问缺乏性能
Static field access in collectible dynamic assemblies lacks performance
对于动态二进制翻译模拟器,我需要使用访问静态字段的 classes 生成可收集的 .NET 程序集。但是,在可收集程序集中使用静态字段时,执行性能比不可收集程序集低 2-3 倍。这种现象在
不使用静态字段的可收集程序集。
在下面的代码中,抽象 class AbstrTest
的方法 MyMethod
由可收集和不可收集的动态程序集实现。使用 CreateTypeConst
MyMethod
将 ulong 参数值乘以常数值二,而使用 CreateTypeField
第二个因子取自
构造函数初始化的静态字段 MyField
.
为了获得真实的结果,MyMethod
结果在 for 循环中累积。
测量结果如下(.NET CLR 4.5/4.6):
Testing non-collectible const multiply:
Elapsed: 8721.2867 ms
Testing collectible const multiply:
Elapsed: 8696.8124 ms
Testing non-collectible field multiply:
Elapsed: 10151.6921 ms
Testing collectible field multiply:
Elapsed: 33404.4878 ms
这是我的复制代码:
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;
public abstract class AbstrTest {
public abstract ulong MyMethod(ulong x);
}
public class DerivedClassBuilder {
private static Type CreateTypeConst(string name, bool collect) {
// Create an assembly.
AssemblyName myAssemblyName = new AssemblyName();
myAssemblyName.Name = name;
AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);
// Create a dynamic module in Dynamic Assembly.
ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name);
// Define a public class named "MyClass" in the assembly.
TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest));
// Create the MyMethod method.
MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod",
MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeof(ulong), new Type [] { typeof(ulong) });
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
methodIL.Emit(OpCodes.Ldarg_1);
methodIL.Emit(OpCodes.Ldc_I4_2);
methodIL.Emit(OpCodes.Conv_U8);
methodIL.Emit(OpCodes.Mul);
methodIL.Emit(OpCodes.Ret);
return myTypeBuilder.CreateType();
}
private static Type CreateTypeField(string name, bool collect) {
// Create an assembly.
AssemblyName myAssemblyName = new AssemblyName();
myAssemblyName.Name = name;
AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);
// Create a dynamic module in Dynamic Assembly.
ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name);
// Define a public class named "MyClass" in the assembly.
TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest));
// Define a private String field named "MyField" in the type.
FieldBuilder myFieldBuilder = myTypeBuilder.DefineField("MyField",
typeof(ulong), FieldAttributes.Private | FieldAttributes.Static);
// Create the constructor.
ConstructorBuilder constructor = myTypeBuilder.DefineConstructor(
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig,
CallingConventions.Standard, Type.EmptyTypes);
ConstructorInfo superConstructor = typeof(AbstrTest).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance,
null, Type.EmptyTypes, null);
ILGenerator constructorIL = constructor.GetILGenerator();
constructorIL.Emit(OpCodes.Ldarg_0);
constructorIL.Emit(OpCodes.Call, superConstructor);
constructorIL.Emit(OpCodes.Ldc_I4_2);
constructorIL.Emit(OpCodes.Conv_U8);
constructorIL.Emit(OpCodes.Stsfld, myFieldBuilder);
constructorIL.Emit(OpCodes.Ret);
// Create the MyMethod method.
MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod",
MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeof(ulong), new Type [] { typeof(ulong) });
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
methodIL.Emit(OpCodes.Ldarg_1);
methodIL.Emit(OpCodes.Ldsfld, myFieldBuilder);
methodIL.Emit(OpCodes.Mul);
methodIL.Emit(OpCodes.Ret);
return myTypeBuilder.CreateType();
}
public static void Main() {
ulong accu;
Stopwatch stopwatch;
try {
Console.WriteLine("Testing non-collectible const multiply:");
AbstrTest i0 = (AbstrTest)Activator.CreateInstance(
CreateTypeConst("MyClassModule0", false));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i0.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
Console.WriteLine("Testing collectible const multiply:");
AbstrTest i1 = (AbstrTest)Activator.CreateInstance(
CreateTypeConst("MyClassModule1", true));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i1.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
Console.WriteLine("Testing non-collectible field multiply:");
AbstrTest i2 = (AbstrTest)Activator.CreateInstance(
CreateTypeField("MyClassModule2", false));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i2.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
Console.WriteLine("Testing collectible field multiply:");
AbstrTest i3 = (AbstrTest)Activator.CreateInstance(
CreateTypeField("MyClassModule3", true));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i3.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
}
catch (Exception e) {
Console.WriteLine("Exception Caught " + e.Message);
}
}
}
所以我的问题是:为什么它变慢了?
是的,这是静态变量分配方式的必然结果。我将首先描述如何将 "visual" 放回 Visual Studio,只有当您可以查看抖动生成的机器代码时,您才有机会诊断像这样的性能问题。
这对 Reflection.Emit 代码来说很棘手,您无法单步执行委托调用,也无法找到代码的确切生成位置。您要做的是注入对 Debugger.Break() 的调用,以便调试器停在正确的位置。所以:
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
var brk = typeof(Debugger).GetMethod("Break");
methodIL.Emit(OpCodes.Call, brk);
methodIL.Emit(OpCodes.Ldarg_1);
// etc..
将循环重复次数更改为 1。工具 > 选项 > 调试 > 常规。取消勾选 "Just My Code" 和 "Suppress JIT optimization"。调试选项卡 > 勾选 "Enable native code debugging"。切换到发布版本。我将 post 32 位代码,它更有趣,因为 x64 抖动可以做得更好。
"Testing non-collectible field multiply" 测试的机器代码如下所示:
01410E70 push dword ptr [ebp+0Ch] ; Ldarg_1, high 32-bits
01410E73 push dword ptr [ebp+8] ; Ldarg_1, low 32-bits
01410E76 push dword ptr ds:[13A6528h] ; myFieldBuilder, high 32-bits
01410E7C push dword ptr ds:[13A6524h] ; myFieldBuilder, low 32-bits
01410E82 call @JIT_LMul@16 (73AE1C20h) ; 64 bit multiply
没什么大不了的,它调用 CLR 辅助方法来执行 64 位乘法。 x64 抖动可以通过一条 IMUL 指令完成。请注意对静态 myFieldBuilder
变量的访问,它具有硬编码地址 0x13A6524。在你的机器上会有所不同。这样效率很高。
现在是令人失望的:
059F0480 push dword ptr [ebp+0Ch] ; Ldarg_1, high 32-bits
059F0483 push dword ptr [ebp+8] ; Ldarg_1, low 32-bits
059F0486 mov ecx,59FC8A0h ; arg2 = DynamicClassDomainId
059F048B xor edx,edx ; arg1 = DomainId
059F048D call JIT_GetSharedNonGCStaticBaseDynamicClass (73E0A6C7h)
059F0492 push dword ptr [eax+8] ; @myFieldBuilder, high 32-bits
059F0495 push dword ptr [eax+4] ; @myFieldBuilder, low 32-bits
059F0498 call @JIT_LMul@16 (73AE1C20h) ; 64-bit multiply
你可以看出为什么它在半英里外变慢了,有一个额外的呼叫 JIT_GetSharedNonGCStaticBaseDynamicClass。它是 CLR 中的一个辅助函数,专门设计用于处理 Reflection.Emit 代码中使用的静态变量,这些代码是用 AssemblyBuilderAccess.RunAndCollect 构建的。今天可以看到源码了,is here。让每个人的眼睛都流血,但它是将 AppDomain 标识符和动态 class 标识符(也称为类型句柄)映射到存储静态变量的已分配内存的函数。
在"non-collectible"版本中抖动知道静态变量存放的具体地址。当它从与 AppDomain 关联的名为 "loader heap" 的内部结构中编译代码时,它分配了变量。知道变量的确切地址,它可以直接发出机器代码中变量的地址。当然非常有效,没有办法更快地做到这一点。
但这在 "collectible" 版本中不起作用,它不仅需要垃圾收集机器代码,而且 还 静态变量。这只能在动态分配存储时起作用。所以它可以动态释放。额外的间接寻址,将其与字典进行比较,是使代码变慢的原因。
您现在可能会明白除非卸载 AppDomain 才能卸载 .NET 程序集(和代码)的原因。这是一个非常非常重要的性能优化。
不确定您想要哪种类型的推荐。一种是自己处理静态变量存储,一个带有实例字段的 class。收集这些没问题。仍然不会那么快,它需要额外的间接访问,但绝对比让 CLR 处理它更快。
对于动态二进制翻译模拟器,我需要使用访问静态字段的 classes 生成可收集的 .NET 程序集。但是,在可收集程序集中使用静态字段时,执行性能比不可收集程序集低 2-3 倍。这种现象在 不使用静态字段的可收集程序集。
在下面的代码中,抽象 class AbstrTest
的方法 MyMethod
由可收集和不可收集的动态程序集实现。使用 CreateTypeConst
MyMethod
将 ulong 参数值乘以常数值二,而使用 CreateTypeField
第二个因子取自
构造函数初始化的静态字段 MyField
.
为了获得真实的结果,MyMethod
结果在 for 循环中累积。
测量结果如下(.NET CLR 4.5/4.6):
Testing non-collectible const multiply:
Elapsed: 8721.2867 ms
Testing collectible const multiply:
Elapsed: 8696.8124 ms
Testing non-collectible field multiply:
Elapsed: 10151.6921 ms
Testing collectible field multiply:
Elapsed: 33404.4878 ms
这是我的复制代码:
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;
public abstract class AbstrTest {
public abstract ulong MyMethod(ulong x);
}
public class DerivedClassBuilder {
private static Type CreateTypeConst(string name, bool collect) {
// Create an assembly.
AssemblyName myAssemblyName = new AssemblyName();
myAssemblyName.Name = name;
AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);
// Create a dynamic module in Dynamic Assembly.
ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name);
// Define a public class named "MyClass" in the assembly.
TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest));
// Create the MyMethod method.
MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod",
MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeof(ulong), new Type [] { typeof(ulong) });
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
methodIL.Emit(OpCodes.Ldarg_1);
methodIL.Emit(OpCodes.Ldc_I4_2);
methodIL.Emit(OpCodes.Conv_U8);
methodIL.Emit(OpCodes.Mul);
methodIL.Emit(OpCodes.Ret);
return myTypeBuilder.CreateType();
}
private static Type CreateTypeField(string name, bool collect) {
// Create an assembly.
AssemblyName myAssemblyName = new AssemblyName();
myAssemblyName.Name = name;
AssemblyBuilder myAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
myAssemblyName, collect ? AssemblyBuilderAccess.RunAndCollect : AssemblyBuilderAccess.Run);
// Create a dynamic module in Dynamic Assembly.
ModuleBuilder myModuleBuilder = myAssembly.DefineDynamicModule(name);
// Define a public class named "MyClass" in the assembly.
TypeBuilder myTypeBuilder = myModuleBuilder.DefineType("MyClass", TypeAttributes.Public, typeof(AbstrTest));
// Define a private String field named "MyField" in the type.
FieldBuilder myFieldBuilder = myTypeBuilder.DefineField("MyField",
typeof(ulong), FieldAttributes.Private | FieldAttributes.Static);
// Create the constructor.
ConstructorBuilder constructor = myTypeBuilder.DefineConstructor(
MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig,
CallingConventions.Standard, Type.EmptyTypes);
ConstructorInfo superConstructor = typeof(AbstrTest).GetConstructor(
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance,
null, Type.EmptyTypes, null);
ILGenerator constructorIL = constructor.GetILGenerator();
constructorIL.Emit(OpCodes.Ldarg_0);
constructorIL.Emit(OpCodes.Call, superConstructor);
constructorIL.Emit(OpCodes.Ldc_I4_2);
constructorIL.Emit(OpCodes.Conv_U8);
constructorIL.Emit(OpCodes.Stsfld, myFieldBuilder);
constructorIL.Emit(OpCodes.Ret);
// Create the MyMethod method.
MethodBuilder myMethodBuilder = myTypeBuilder.DefineMethod("MyMethod",
MethodAttributes.Public | MethodAttributes.ReuseSlot | MethodAttributes.Virtual | MethodAttributes.HideBySig,
typeof(ulong), new Type [] { typeof(ulong) });
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
methodIL.Emit(OpCodes.Ldarg_1);
methodIL.Emit(OpCodes.Ldsfld, myFieldBuilder);
methodIL.Emit(OpCodes.Mul);
methodIL.Emit(OpCodes.Ret);
return myTypeBuilder.CreateType();
}
public static void Main() {
ulong accu;
Stopwatch stopwatch;
try {
Console.WriteLine("Testing non-collectible const multiply:");
AbstrTest i0 = (AbstrTest)Activator.CreateInstance(
CreateTypeConst("MyClassModule0", false));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i0.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
Console.WriteLine("Testing collectible const multiply:");
AbstrTest i1 = (AbstrTest)Activator.CreateInstance(
CreateTypeConst("MyClassModule1", true));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i1.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
Console.WriteLine("Testing non-collectible field multiply:");
AbstrTest i2 = (AbstrTest)Activator.CreateInstance(
CreateTypeField("MyClassModule2", false));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i2.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
Console.WriteLine("Testing collectible field multiply:");
AbstrTest i3 = (AbstrTest)Activator.CreateInstance(
CreateTypeField("MyClassModule3", true));
stopwatch = Stopwatch.StartNew();
accu = 0;
for (uint i = 0; i < 0xffffffff; i++)
accu += i3.MyMethod(i);
stopwatch.Stop();
Console.WriteLine("Elapsed: " + stopwatch.Elapsed.TotalMilliseconds + " ms");
}
catch (Exception e) {
Console.WriteLine("Exception Caught " + e.Message);
}
}
}
所以我的问题是:为什么它变慢了?
是的,这是静态变量分配方式的必然结果。我将首先描述如何将 "visual" 放回 Visual Studio,只有当您可以查看抖动生成的机器代码时,您才有机会诊断像这样的性能问题。
这对 Reflection.Emit 代码来说很棘手,您无法单步执行委托调用,也无法找到代码的确切生成位置。您要做的是注入对 Debugger.Break() 的调用,以便调试器停在正确的位置。所以:
ILGenerator methodIL = myMethodBuilder.GetILGenerator();
var brk = typeof(Debugger).GetMethod("Break");
methodIL.Emit(OpCodes.Call, brk);
methodIL.Emit(OpCodes.Ldarg_1);
// etc..
将循环重复次数更改为 1。工具 > 选项 > 调试 > 常规。取消勾选 "Just My Code" 和 "Suppress JIT optimization"。调试选项卡 > 勾选 "Enable native code debugging"。切换到发布版本。我将 post 32 位代码,它更有趣,因为 x64 抖动可以做得更好。
"Testing non-collectible field multiply" 测试的机器代码如下所示:
01410E70 push dword ptr [ebp+0Ch] ; Ldarg_1, high 32-bits
01410E73 push dword ptr [ebp+8] ; Ldarg_1, low 32-bits
01410E76 push dword ptr ds:[13A6528h] ; myFieldBuilder, high 32-bits
01410E7C push dword ptr ds:[13A6524h] ; myFieldBuilder, low 32-bits
01410E82 call @JIT_LMul@16 (73AE1C20h) ; 64 bit multiply
没什么大不了的,它调用 CLR 辅助方法来执行 64 位乘法。 x64 抖动可以通过一条 IMUL 指令完成。请注意对静态 myFieldBuilder
变量的访问,它具有硬编码地址 0x13A6524。在你的机器上会有所不同。这样效率很高。
现在是令人失望的:
059F0480 push dword ptr [ebp+0Ch] ; Ldarg_1, high 32-bits
059F0483 push dword ptr [ebp+8] ; Ldarg_1, low 32-bits
059F0486 mov ecx,59FC8A0h ; arg2 = DynamicClassDomainId
059F048B xor edx,edx ; arg1 = DomainId
059F048D call JIT_GetSharedNonGCStaticBaseDynamicClass (73E0A6C7h)
059F0492 push dword ptr [eax+8] ; @myFieldBuilder, high 32-bits
059F0495 push dword ptr [eax+4] ; @myFieldBuilder, low 32-bits
059F0498 call @JIT_LMul@16 (73AE1C20h) ; 64-bit multiply
你可以看出为什么它在半英里外变慢了,有一个额外的呼叫 JIT_GetSharedNonGCStaticBaseDynamicClass。它是 CLR 中的一个辅助函数,专门设计用于处理 Reflection.Emit 代码中使用的静态变量,这些代码是用 AssemblyBuilderAccess.RunAndCollect 构建的。今天可以看到源码了,is here。让每个人的眼睛都流血,但它是将 AppDomain 标识符和动态 class 标识符(也称为类型句柄)映射到存储静态变量的已分配内存的函数。
在"non-collectible"版本中抖动知道静态变量存放的具体地址。当它从与 AppDomain 关联的名为 "loader heap" 的内部结构中编译代码时,它分配了变量。知道变量的确切地址,它可以直接发出机器代码中变量的地址。当然非常有效,没有办法更快地做到这一点。
但这在 "collectible" 版本中不起作用,它不仅需要垃圾收集机器代码,而且 还 静态变量。这只能在动态分配存储时起作用。所以它可以动态释放。额外的间接寻址,将其与字典进行比较,是使代码变慢的原因。
您现在可能会明白除非卸载 AppDomain 才能卸载 .NET 程序集(和代码)的原因。这是一个非常非常重要的性能优化。
不确定您想要哪种类型的推荐。一种是自己处理静态变量存储,一个带有实例字段的 class。收集这些没问题。仍然不会那么快,它需要额外的间接访问,但绝对比让 CLR 处理它更快。