为什么在C#中会使用“in”参数修饰符?

130

所以,我(认为我)理解了 in 参数修饰符的作用。但它似乎非常冗余。

通常,我认为使用 ref 的唯一原因是修改调用变量,而这被 in 显式禁止。因此,通过in引用传递似乎在逻辑上等同于按值传递。

是否存在某种性能优势?我认为,在后端方面,ref 参数至少必须复制变量的物理地址,其大小应与任何典型对象引用相同。

那么,优势只在于更大的结构体中吗,还是有某些幕后编译器优化使其在其他地方具有吸引力?如果是后者,为什么不将in用于每个参数?


2
是的,这样做有性能优势。ref 用于通过引用传递 structs 而不是复制它们。in 表示该结构体不应被修改。 - Panagiotis Kanavos
@dbc 不,这与互操作无关。 - Panagiotis Kanavos
6
值类型的性能。C# 7.2 的新关键字"in" - Silvermind
3
详细讨论请参见此处。请注意最后的警告:“这意味着您永远不应将非只读结构作为in参数传递。” - Panagiotis Kanavos
我本来以为这是一个重复的问题,但现在找不到了。 - JAD
5个回答

123

in 最近被引入到 C# 语言中。

in 实际上是一个 ref readonly。通常情况下,只有一个用例可以使用 in:处理大量大型 readonly struct 的高性能应用程序。

假设您有:

readonly struct VeryLarge
{
    public readonly long Value1;   
    public readonly long Value2;

    public long Compute() { }
    // etc
}

void Process(in VeryLarge value) { }

在这种情况下,当在Process方法中使用VeryLarge结构体(例如调用value.Compute())时,该结构体将被按引用传递而无需创建防御性副本,并且编译器可以确保该结构体的不可变性。

请注意,在使用in修饰符传递非只读struct时,在上述Process方法中调用结构体的方法和访问属性时,编译器将会创建一个防御性副本,这将对性能产生负面影响!

有一篇非常好的MSDN博客文章,我建议您仔细阅读。

如果您想获取更多关于in引入的历史背景,您可以阅读C#语言GitHub存储库中的讨论

总的来说,大多数开发人员认为引入in可能是一个错误。这是一个相当奇特的语言特性,只有在高性能边缘情况下才有用。


它是否抑制编译器生成的防御性副本,还是仅仅消除了程序员在使用'ref'的情况下手动创建防御性副本的需要,例如通过允许someProc(in thing.someProperty);而不是propType myProp = thing.someProperty; someProc(ref myProp);?'in'是类似于'out'的C#概念,还是已经添加到.NET Framework中? - supercat
@supercat,您不能使用in传递属性,因为in实际上是带有特殊属性的ref。 所以您的第一个片段将无法编译。 @VisualMelon,没错,防御性复制发生在调用方法或从获取结构体作为参数的方法中访问结构体的属性时。 - dymanoid
@dymanoid 很抱歉刷屏了,我尝试了大约10次才明白你写的内容(我认为这不是你的错!) - VisualMelon
@VisualMelon,我没有详细解释,因为OP提到他们已经理解了这个概念。in特性当然很令人困惑。我只是试图描述这个特性有意义的(唯一)用例。 - dymanoid
5
"both in and out" 是应该从一开始就出现在框架中的概念,同时也需要一种方法来指示方法和属性是否修改 this。编译器可以通过传递一个持有该属性的临时引用来将属性传递给 in 参数;如果另一种语言编写的函数修改了该临时变量,语义将有点棘手,但这将是函数行为不匹配其签名的错误。 - supercat
3
@supercat,请不要在这里开启讨论(反正我也不是.NET Framework概念团队的成员)。答案中引用了一篇长篇讨论,而该GitHub讨论还引用了其他一些有趣的阅读材料。顺便说一下,在VB.NET中,您可以通过引用传递属性 - 编译器会为您创建这些临时变量(当然可能会导致一些模糊的问题)。 - dymanoid

59

按引用传递似乎在逻辑上等价于按值传递。

正确。

这样做是否有性能优势?

有。

我认为在后端方面,ref 参数至少必须复制变量的物理地址,这应该与任何典型对象引用的大小相同。

没有要求对象引用和变量引用的大小必须相同,也没有要求它们的大小与机器字长相同,但是在实践中,在32位机器上两者都是32位,在64位机器上两者都是64位。

我不清楚您认为“物理地址”与此有何关联。 在 Windows 上,我们在用户模式下使用的是虚拟地址,而不是物理地址。我很好奇在什么情况下您认为物理地址在 C# 程序中是有意义的。

任何一种引用类型的实现并不要求其被实现为存储的虚拟地址。在符合 CLI 规范的实现中,引用可以是指向 GC 表的不透明句柄。

这种优势只存在于较大的结构体中吗?

减少传递较大的结构体的成本是该特性的动机场景。

请注意,并非所有程序都可以通过使用in参数实现加速,它有可能会使程序变慢。关于性能的所有问题必须通过经验研究来回答。很少有一些优化措施是总是有效的,这不是一个总是有效的优化措施。

是否有某些幕后编译器优化使其在其他地方更具吸引力?

只要不违反C#规范的规则,编译器和运行时可以选择进行任何优化。据我所知,目前还没有针对in参数的这样的优化,但这并不排除将来出现这样的优化。

为什么我不应该把每个参数都定义为in呢?

好吧,假设您将一个int参数改为一个in int参数。会导致哪些成本?

  • 调用站点现在需要一个变量而不是一个值
  • 该变量无法注册。Jitter的精心调整的寄存器分配方案刚刚受到了影响。
  • 调用站点的代码变大了,因为它必须将一个引用传递给变量并将其放置在堆栈上,而之前它可以简单地将值推送到调用堆栈上。
  • 代码越大,一些短跳转指令可能已变成长跳转指令,所以代码现在更大了。这对各种事情都有连锁反应。缓存更快填满,Jitter有更多的工作要做,Jitter可能选择不对更大的代码大小进行某些优化等。
  • 在被调用者站点,我们将对栈(或寄存器)中的值的访问转换为指向指针的间接访问。现在,该指针很可能在缓存中,但是,现在我们已经将一个单指令访问值的操作转换为两个指令的访问。
  • 等等。
  • 假设它是一个double并将其更改为in double。同样,现在该变量无法注册到高性能浮点寄存器中。这不仅会影响性能,还可能改变程序行为!C#允许以高于64位精度进行浮点运算,通常只在浮点数可以注册时才这样做。

    这不是免费的优化。您必须将其性能与其他方案进行比较。您最好是根据设计指南建议首先不要创建大型结构。


8
在所有情况下,通过in传递参数真的等同于按值传递吗?如果我们有f(ref T x, in T y)并且f修改x,那么当我们以f(ref a, a)的方式调用它时,它应该观察到对y进行相同的更改。如果f接受一个in T y和一个将在调用时修改y的委托,那么同样适用。没有in,语义会有所不同,因为y永远不会改变其值,因为它是一个副本。 - chi
6
我怀疑OP在非正式的情况下指的是“物理地址”,即“描述内存位置的位模式”,与“后端选择用于描述如何查找对象的任何内容”形成对比。 - Sneftel
5
没错。如果你在变量别名方面玩危险游戏,那么可能会遇到麻烦。如果做某事会带来伤害,那就别做。 in 的目的是表示“按引用传递,只读”的概念,这在行为明智时相当于按值传递。 - Eric Lippert
你能详细说明一下“对象引用和变量引用的大小不一定相同”的要求吗?我认为 ECMA-335 中的 I.12.1.1 明确涵盖了这一点:“本机大小类型(本机 int、本机 unsigned int、O 和 &)是 CLI 中用于推迟值大小选择的机制。这些数据类型存在于 CIL 类型中;但是,CLI 将每个类型映射到特定处理器的本机大小。” - Tanner Gooding
@TannerGooding:即使是魔鬼也能引用经文。:) 很好的发现。但是让我反驳一下:对于不同类型的指针,特定处理器的本机大小没有要求必须相同,事实上,我(勉强)已经足够老了,曾经为在指向本地变量和指向对象的体系结构上运行的语言编译器工作过。 - Eric Lippert
显示剩余4条评论

8

确实有这样的优化。当传递一个struct时,关键字in允许编译器仅需要传递指针,而无需担心方法会更改内容。这点非常重要——它避免了复制操作。对于大型结构体,这可以带来很大的差别。


2
这只是重复问题已经说过的内容,没有回答它实际提出的问题。 - Servy
1
仅在只读结构体中使用。否则,编译器仍将创建防御性副本。 - Panagiotis Kanavos
1
实际上不是这样的。它确认了存在性能优势。鉴于IN是通过语言性能运行创建的,是的,那就是原因。它允许对只读结构进行优化。 - TomTom
3
@TomTom,这个问题已经涵盖了你的问题。它所问的是:“那么,优势只存在于更大的结构体中,还是有一些幕后编译器优化使其在其他情况下也具有吸引力?如果是后者,为什么我不应该将每个参数都设置为'in'?”请注意,它并没有问对于较大的结构体是否实际上有益,或者仅仅在哪种情况下有益。它只是在问是否对于较小的结构体有益(然后是一个跟进的问题是否有益)。你都没有回答这两个问题。 - Servy

4
这是因为采用了函数式编程方法。其中一个主要原则是函数不应该有副作用,这意味着它不应该改变参数的值并且应该返回某个值。在C#中,没有办法传递结构体(和值类型),而不是通过引用进行复制,这样可以更改值。在Swift中,有一种hacky算法,可以在方法开始更改其值时复制结构体(它们的集合是结构体)。使用Swift的人并不都知道复制的问题。这是很好的C#特性,因为它具有内存效率和明确性。如果你看一下新功能,你会发现越来越多的工作是围绕堆栈中的结构体和数组完成的。而in语句只是为了这些特性必要的。其他答案中提到了一些限制,但对于理解.net的方向并不那么重要。

4

in 是 C# 7.2 中的只读引用。

这意味着您不会将整个对象传递到函数堆栈中,类似于 ref 的情况,您只传递结构的引用。

但是,试图更改对象的值会导致编译器错误。

是的,如果您使用大型结构,则可以通过此功能优化代码性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接