在不将结构设为只读的情况下避免将 `in` 与结构一起使用会降低性能吗?

Aus*_*yan 5 c# optimization struct c#-7.2 in-parameters

C# 7.2 添加了两个新特性:

  1. 输入参数

    使用in的参数让我们通过引用传递,但随后阻止我们将值分配给它。然而,性能实际上会变得更糟,因为它创建了结构的“防御性副本”,复制了整个内容

  2. 只读结构

    解决此问题的一种方法是使用readonlyfor a struct。当您将它传递给一个in参数时,编译器会看到它readonly并且不会创建防御性副本,从而使其成为更好的性能替代方案。

这一切都很棒,但该领域的每个领域struct都必须如此readonly。这不起作用:

public readonly struct Coord
{
    public int X, Y;    // Error: instance fields of readonly structs must be read only
}
Run Code Online (Sandbox Code Playgroud)

自动属性也必须是readonly

有没有办法获得in参数的好处(编译时检查以强制参数不被更改,通过引用传递)同时仍然能够修改 的字段struct,而不会in因创建防御副本?

svi*_*ick 5

当您将 [a readonly struct]传递给in 参数时,编译器会看到它是只读的并且不会创建防御性副本。

我想你误会了。编译器创建了一个只读变量的防守副本,包含struct(这可能是一个in参数,也是一个readonly字段),当你调用该方法struct

考虑以下代码

struct S
{
    int x, y;

    public void M() {}
}

class C
{
    static void Foo()
    {
        S s = new S();
        Bar(s);
    }

    static void Bar(in S s)
    {
        s.M();
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以检查为上述代码生成的 IL,以了解实际会发生什么。

对于Foo,IL 是:

ldloca.s 0 // load address of the local s to the stack
initobj S  // initialize struct S at the address on the stack
ldloca.s 0 // load address of the local s to the stack again
call void C::Bar(valuetype S&) // call Bar
ret        // return
Run Code Online (Sandbox Code Playgroud)

请注意,没有复制:本地s被初始化,然后该本地的地址直接传递给Bar

IL 为Bar

ldarg.0     // load argument s (which is an address) to the stack
ldobj S     // copy the value from the address on the stack to the stack
stloc.0     // store the value from the stack to an unnamed local variable
ldloca.s 0  // load the address of the unnamed local variable to the stack
call instance void S::M() // call M
ret         // return
Run Code Online (Sandbox Code Playgroud)

在这里,ldobjstloc指令创建了防御性副本,以确保如果M发生变异struct,s不会发生变异(因为它是只读的)。

如果你更改代码,使S一个readonly struct,那么IL为Foo保持不变,但Bar其改变为:

ldarg.0 // load argument s (which is an address) to the stack
call instance void S::M() // call M
ret     // return
Run Code Online (Sandbox Code Playgroud)

请注意,这里不再复制。

这是将您标记structreadonly避免的防御性副本。但是如果你不在结构上调用任何实例方法,就不会有任何防御性副本。

还要注意,语言规定当代码执行时,它必须表现得好像防御性副本就在那里。如果 JIT 可以确定副本实际上不是必需的,则允许避免它。

  • 为了完整起见:从 c# 8 开始,我们能够将 struct 中的各个方法标记为只读。因此,我们现在可以选择通过仅将 M() 方法标记为只读来避免 Bar() 方法中的防御性复制,而不使整个结构只读 (2认同)