使用带有结构的`in`避免性能损失而不使结构只读?

时间:2018-05-26 05:52:53

标签: c# optimization struct c#-7.2 in-parameters

C#7.2增加了两个新功能:

  1. 参数

    使用in作为参数让我们通过引用传递,但是后来阻止我们为它赋值。然而,性能实际上可能变得更糟,因为它创建了结构的“防御性副本”,复制整个事物

  2. Readonly Structs

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

  3. 这一切都很棒,但struct中的每个字段都必须为readonly。这不起作用:

    public readonly struct Coord
    {
        public int X, Y;    // Error: instance fields of readonly structs must be read only
    }
    

    自动属性也必须为readonly

    是否有办法获得in参数的好处(编译时检查以强制参数未被更改,通过引用传递),同时仍然能够修改{{1的字段没有因创建防御性副本而导致的struct显着性能下降?

2 个答案:

答案 0 :(得分:1)

  

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

我觉得你误解了。编译器创建一个只读变量的防御性副本,当您调用该方法时,该变量包含struct(可能是in参数,但也包含readonly字段) struct

考虑the following code

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();
    }
}

您可以检查上面代码生成的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

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

Bar的IL是:

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

此处,ldobjstloc说明会创建防御性副本,以确保M变异structs赢得'变异(因为它是只读的)。

如果您change the code to make S a readonly struct,则Foo的IL保持不变,但Bar的IL更改为:

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

请注意,此处不再复制。

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

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

答案 1 :(得分:0)

首先,我将不惜一切代价避免使用非只读结构。只有非常强的性能要求才能证明无堆分配的热执行路径中的可变结构是合理的。而且,如果您的struct的核心是可变的,为什么要对一种方法将其设为只读?那是一条危险的,容易出错的路径。

事实:将in参数传递与非只读结构结合在一起,将在该副本的引用传递到方法中之前产生防御性副本。

因此,任何可变性都将在方法上下文内可见的编译器副本上起作用,而调用者则不可见。令人困惑且无法维护。

我认为in参数对于帮助编译器做出明智的决策以提高性能非常有用。那绝对不是真的!我出于性能原因对inreadonly结构进行了实验,得出的结论是:太多的陷阱实际上会使您的代码变慢。如果您是项目中唯一的开发人员,如果您的结构足够大,您会深知所有编译器的技巧,并且经常运行微基准测试……那么您可能会从性能方面受益。