在原始类型

时间:2018-06-09 19:33:51

标签: c# .net optimization c#-7.2 in-parameters

C#7.2引入了in修饰符,用于通过引用传递参数,并保证收件人不会修改参数。

article说:

  

你永远不应该使用非readonly结构作为in参数,因为它可能会对性能产生负面影响,如果结构是可变的,可能会导致模糊的行为

这对于intdouble等内置基元意味着什么?

我想使用in来表达代码意图,但不会以防御性副本的性能损失为代价。

问题

  • 通过in参数传递原始类型并且没有制作防御性副本是否安全?
  • 其他常用的框架结构,例如DateTimeTimeSpanGuid,......是否被JIT视为readonly
    • 如果这因平台而异,我们怎样才能找出在特定情况下哪些类型是安全的?

4 个答案:

答案 0 :(得分:8)

快速测试显示,目前,是的,为内置基元类型和结构创建了防御性副本。

使用VS 2017(.NET 4.5.2,C#7.2,发布版本)编译以下代码:

using System;

class MyClass
{
    public readonly struct Immutable { public readonly int I; public void SomeMethod() { } }
    public struct Mutable { public int I; public void SomeMethod() { } }

    public void Test(Immutable immutable, Mutable mutable, int i, DateTime dateTime)
    {
        InImmutable(immutable);
        InMutable(mutable);
        InInt32(i);
        InDateTime(dateTime);
    }

    void InImmutable(in Immutable x) { x.SomeMethod(); }
    void InMutable(in Mutable x) { x.SomeMethod(); }
    void InInt32(in int x) { x.ToString(); }
    void InDateTime(in DateTime x) { x.ToString(); }

    public static void Main(string[] args) { }
}
使用ILSpy进行反编译时,

会产生以下结果:

...
private void InImmutable([System.Runtime.CompilerServices.IsReadOnly] [In] ref MyClass.Immutable x)
{
    x.SomeMethod();
}

private void InMutable([System.Runtime.CompilerServices.IsReadOnly] [In] ref MyClass.Mutable x)
{
    MyClass.Mutable mutable = x;
    mutable.SomeMethod();
}

private void InInt32([System.Runtime.CompilerServices.IsReadOnly] [In] ref int x)
{
    int num = x;
    num.ToString();
}

private void InDateTime([System.Runtime.CompilerServices.IsReadOnly] [In] ref DateTime x)
{
    DateTime dateTime = x;
    dateTime.ToString();
}
...

(或者,如果您更喜欢IL:)

IL_0000: ldarg.1
IL_0001: ldobj [mscorlib]System.DateTime
IL_0006: stloc.0
IL_0007: ldloca.s 0
IL_0009: call instance string [mscorlib]System.DateTime::ToString()
IL_000e: pop
IL_000f: ret

答案 1 :(得分:4)

从jit的角度来看in改变参数的调用约定,以便它总是通过引用传递。因此,对于原始类型(复制起来很便宜)并且通常按值传递,如果使用in,则在调用方和被调用方都会产生一些额外的成本。但是,没有制作防御性副本。

例如

using System;
using System.Runtime.CompilerServices;

class X
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    static int F0(in int x) { return x + 1; }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static int F1(int x) { return x + 1; }

    public static void Main()
    {
        int x = 33;
        F0(x);
        F0(x);
        F1(x);
        F1(x);
    }
}

Main的代码是

   C744242021000000     mov      dword ptr [rsp+20H], 33
   488D4C2420           lea      rcx, bword ptr [rsp+20H]
   E8DBFBFFFF           call     X:F0(byref):int
   488D4C2420           lea      rcx, bword ptr [rsp+20H]
   E8D1FBFFFF           call     X:F0(byref):int
   8B4C2420             mov      ecx, dword ptr [rsp+20H]
   E8D0FBFFFF           call     X:F1(int):int
   8B4C2420             mov      ecx, dword ptr [rsp+20H]
   E8C7FBFFFF           call     X:F1(int):int

请注意,因为in x无法注册。

F0 & F1的代码显示前者现在必须从byref中读取值:

;; F0
   8B01                 mov      eax, dword ptr [rcx]
   FFC0                 inc      eax
   C3                   ret

;; F1
   8D4101               lea      eax, [rcx+1]
   C3                   ret

如果jit内联,这个额外费用通常可以撤消,但并非总是如此。

答案 2 :(得分:3)

使用当前编译器,防御副本确实似乎是针对“原始”值类型和其他非只读结构。具体而言,它们的生成方式与readonly字段的生成方式类似:访问可能会使内容发生变异的属性或方法时。副本在每个呼叫站点显示为潜在的变异成员,因此如果您调用 n 此类成员,您最终将 n 防御副本。与readonly字段一样,您可以通过手动将原始文件复制到本地来避免多个副本。

看看this suite of examples。您可以查看IL和JIT程序集。

  

通过参数传递原始类型并且没有制作防御性副本是否安全?

这取决于您是否访问in参数上的方法或属性。如果你这样做,你可能会看到防御性副本。如果没有,你可能不会:

// Original:
int In(in int _) {
    _.ToString();
    _.GetHashCode();
    return _ >= 0 ? _ + 42 : _ - 42;
}

// Decompiled:
int In([In] [IsReadOnly] ref int _) {
    int num = _;
    num.ToString();    // invoke on copy
    num = _;
    num.GetHashCode(); // invoke on second copy
    if (_ < 0)
        return _ - 42; // use original in arithmetic
    return _ + 42;
}
  

其他常用的框架结构如DateTime,TimeSpan,Guid,......是否被[编译器]认为是只读的?

不会,仍然会在呼叫网站上为这些类型的in参数上的潜在变异成员制作防御性副本。但有趣的是,并非所有方法和属性都被视为“可能会发生变异”。我注意到,如果我调用默认方法实现(例如,ToStringGetHashCode),则不会发出防御性副本。但是,只要我覆盖这些方法,编译器就会创建副本:

struct WithDefault {}
struct WithOverride { public override string ToString() => "RO"; }

// Original:
void In(in WithDefault d, in WithOverride o) {
    d.ToString();
    o.ToString();
}

// Decompiled:
private void In([In] [IsReadOnly] ref WithDefault d,
                [In] [IsReadOnly] ref WithOverride o) {
    d.ToString();            // invoke on original
    WithOverride withOverride = o;
    withOverride.ToString(); // invoke on copy
}
  

如果这因平台而异,我们怎样才能找出在特定情况下哪些类型是安全的?

嗯,所有类型都是'安全' - 副本确保。我假设你在问哪些类型会避免防御性副本。正如我们上面所看到的,它比“参数的类型是什么”更复杂?没有单一副本:副本是在in个参数的某些引用处发出的,例如,引用是调用目标。如果没有这样的参考,则不需要制作副本。此外,是否复制的决定取决于您是否调用已知安全或“纯”的成员与可能会改变值类型内容的成员。

目前,某些默认方法似乎被视为纯粹的,并且编译器避免在这些情况下进行复制。如果我不得不猜测,这是预先存在的行为的结果,并且编译器正在使用最初为readonly字段开发的“只读”引用的概念。如下所示(或in SharpLab),行为类似。请注意IL在调用ldflda时如何使用WithDefault.ToString(按地址加载字段)将调用目标推送到堆栈,但使用ldfld,{{ 1}},stloc序列,在调用ldloca时将副本推送到堆栈:

WithOverride.ToString

那就是说,现在只读参考文献可能会变得更加普遍,但是可以在没有防御性副本的情况下调用的方法的“白名单”可能会在将来增长。现在,它似乎有点武断。

答案 3 :(得分:1)

  

这对于内置基元(例如int,double?

)意味着什么

没有,intdouble以及所有其他内置&#34;原语&#34;是不可改变的。您无法改变doubleintDateTime。例如,System.Drawing.Point是一个典型的框架类型,不是一个好的候选者。

老实说,文档可能会更清晰一些; readonly在这种情况下是一个令人困惑的术语,它应该简单地说类型应该是不可变的。

没有规则知道任何给定类型是否是不可变的;只有仔细检查API才能给你一个想法,或者,如果幸运的话,文档可能会说明它是否存在。