结构字段的加载值指令与加载地址指令的效率

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里面的BA 内),加载地址指令比加载值指令更有效。

考虑到这一点,考虑更改示例代码:

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 的信息),确切地知道 BA 中的位置,其中 [=15] =] 在 B 里面,B.Ca.

里面

在 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]).