结构字段的加载值指令与加载地址指令的效率
Efficiency of load-value instructions versus load-address instructions for fields of structs
考虑以下 C# 结构定义:
public struct A
{
public B B;
}
public struct B
{
public int C;
}
同时考虑下面的静态方法:
public static int Method(A a) => a.B.C;
调用此方法将生成结构类型 A
的副本。例如,在下面的代码中:
A a = default;
Method(a);
对 Method
的调用将编译为如下所示的 IL:
IL_0008: ldloc.0 // V_0
IL_0009: call int32 Class::Method(valuetype A)
ldloc
会将局部变量 a
(V_0
) 的值复制到计算堆栈中,该值将在 Method
中使用。如果 A
(或 B
)是一个大结构,这个副本可能会很昂贵。 Method
的 IL 也会导致加载值指令:
IL_0000: ldarg.0 // a
IL_0001: ldfld valuetype B A::B
IL_0006: ldfld int32 B::C
IL_000b: ret
最新版本的 C# 包含有助于提高结构处理效率的功能。 C# 7.2 在参数上引入了 in
修饰符,当编译器可以验证参数不会被调用的方法修改时,可以通过引用传递值类型。例如,将 in
修饰符应用于参数 a
:
public static int Method(in A a) => a.B.C;
将在调用站点生成以下已编译 IL:
IL_0008: ldloca.s a
IL_000a: call int32 Class::Method(valuetype A&)
并在执行中 Method
:
IL_0000: ldarg.0 // a
IL_0001: ldflda valuetype B A::B
IL_0006: ldfld int32 B::C
IL_000b: ret
注意加载地址指令。我的假设(如果我错了请纠正我)是对于深场读取(例如读取C
里面的B
在 A
内),加载地址指令比加载值指令更有效。
考虑到这一点,考虑更改示例代码:
A a = default;
var c = a.B.C;
第二行然后编译为:
IL_0008: ldloc.1 // V_1
IL_0009: ldfld valuetype B A::B
IL_000e: ldfld int32 B::C
IL_0013: stloc.0 // c
在这种情况下,为什么编译器也不喜欢使用加载地址指令?是否仅仅因为 a
是局部变量而不是方法参数而导致效率差异,还是我在这里缺少其他东西?
这绝对与 a
是局部变量还是方法参数无关。至少从效率的角度来看不是。
首先要了解的是,C# 中的结构直接位于(在内存中)声明它们的位置 - 对于局部变量来说,直接位于堆栈上。更重要的是 - 嵌套结构的行为相同。 JIT 有可能在运行时的任何时候(不总是在编译期间,阅读更多关于 StructLayoutAttribute 的信息),确切地知道 B
在 A
中的位置,其中 [=15] =] 在 B
里面,B.C
在 a
.
里面
在 JIT 编译方法后查看汇编代码时(在 Release 中编译很重要 - 调试版本不会以相同的方式进行优化。确保编译器也不会优化变量),你会发现无论你在哪里输入 a.B.C 它总是来自内存的直接赋值(相对于 A 在内存中的位置)。
在我的例子中,我在 A 中添加了另一个变量 int a1
以稍微移动内存 - 这是结果代码:
A a = 默认值;
xor ecx,ecx
mov qword ptr [rbp-30h],rcx
var c = a.B.C;
mov esi,dword ptr [rbp-2Ch]
其中 esi 是 var c
的临时寄存器,[rbp-30h]
是 a
在堆栈中的位置。 B
有一个整数位于偏移量 0,A
有一个整数位于偏移量 0,B
有一个整数位于偏移量 4,所以 a.B.C
的最终地址总是 a+ 4 ([rbp-2Ch]
).
考虑以下 C# 结构定义:
public struct A
{
public B B;
}
public struct B
{
public int C;
}
同时考虑下面的静态方法:
public static int Method(A a) => a.B.C;
调用此方法将生成结构类型 A
的副本。例如,在下面的代码中:
A a = default;
Method(a);
对 Method
的调用将编译为如下所示的 IL:
IL_0008: ldloc.0 // V_0
IL_0009: call int32 Class::Method(valuetype A)
ldloc
会将局部变量 a
(V_0
) 的值复制到计算堆栈中,该值将在 Method
中使用。如果 A
(或 B
)是一个大结构,这个副本可能会很昂贵。 Method
的 IL 也会导致加载值指令:
IL_0000: ldarg.0 // a
IL_0001: ldfld valuetype B A::B
IL_0006: ldfld int32 B::C
IL_000b: ret
最新版本的 C# 包含有助于提高结构处理效率的功能。 C# 7.2 在参数上引入了 in
修饰符,当编译器可以验证参数不会被调用的方法修改时,可以通过引用传递值类型。例如,将 in
修饰符应用于参数 a
:
public static int Method(in A a) => a.B.C;
将在调用站点生成以下已编译 IL:
IL_0008: ldloca.s a
IL_000a: call int32 Class::Method(valuetype A&)
并在执行中 Method
:
IL_0000: ldarg.0 // a
IL_0001: ldflda valuetype B A::B
IL_0006: ldfld int32 B::C
IL_000b: ret
注意加载地址指令。我的假设(如果我错了请纠正我)是对于深场读取(例如读取C
里面的B
在 A
内),加载地址指令比加载值指令更有效。
考虑到这一点,考虑更改示例代码:
A a = default;
var c = a.B.C;
第二行然后编译为:
IL_0008: ldloc.1 // V_1
IL_0009: ldfld valuetype B A::B
IL_000e: ldfld int32 B::C
IL_0013: stloc.0 // c
在这种情况下,为什么编译器也不喜欢使用加载地址指令?是否仅仅因为 a
是局部变量而不是方法参数而导致效率差异,还是我在这里缺少其他东西?
这绝对与 a
是局部变量还是方法参数无关。至少从效率的角度来看不是。
首先要了解的是,C# 中的结构直接位于(在内存中)声明它们的位置 - 对于局部变量来说,直接位于堆栈上。更重要的是 - 嵌套结构的行为相同。 JIT 有可能在运行时的任何时候(不总是在编译期间,阅读更多关于 StructLayoutAttribute 的信息),确切地知道 B
在 A
中的位置,其中 [=15] =] 在 B
里面,B.C
在 a
.
在 JIT 编译方法后查看汇编代码时(在 Release 中编译很重要 - 调试版本不会以相同的方式进行优化。确保编译器也不会优化变量),你会发现无论你在哪里输入 a.B.C 它总是来自内存的直接赋值(相对于 A 在内存中的位置)。
在我的例子中,我在 A 中添加了另一个变量 int a1
以稍微移动内存 - 这是结果代码:
A a = 默认值;
xor ecx,ecx
mov qword ptr [rbp-30h],rcx
var c = a.B.C;
mov esi,dword ptr [rbp-2Ch]
其中 esi 是 var c
的临时寄存器,[rbp-30h]
是 a
在堆栈中的位置。 B
有一个整数位于偏移量 0,A
有一个整数位于偏移量 0,B
有一个整数位于偏移量 4,所以 a.B.C
的最终地址总是 a+ 4 ([rbp-2Ch]
).