代码之家  ›  专栏  ›  技术社区  ›  Drew Noakes

对具有基元类型的参数使用修饰符中的c 7.2

  •  11
  • Drew Noakes  · 技术社区  · 8 年前

    C 7.2介绍了 in 通过引用传递参数的修饰符,以保证收件人不会修改参数。

    这个 article 说:

    不应将非只读结构用作In参数,因为它可能会对性能产生负面影响,并且如果结构是可变的,则可能导致不明确的行为。

    这对于诸如 int , double ?

    我想用 在里面 在代码中表达意图,但不以防御性拷贝的性能损失为代价。

    问题

    • 传递基元类型是否安全 在里面 争论,没有防御性的复制品?
    • 其他常用的框架结构,如 DateTime , TimeSpan , Guid ,…考虑过的 readonly 通过JIT?
      • 如果这因平台而异,我们如何才能找出在给定情况下哪些类型是安全的?
    4 回复  |  直到 8 年前
        1
  •  8
  •   Heinzi    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
    
        2
  •  4
  •   Andy Ayers    8 年前

    从JIT的角度来看 in 更改参数的调用约定,使其始终通过引用传递。因此,对于原语类型(复制成本很低)和通常按值传递的类型,如果使用 在里面 . 然而,没有防御性的复制品。

    Eg在

    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
    

    注意,因为 在里面 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输入,这个额外的成本通常可以撤消,尽管并不总是如此。

        3
  •  3
  •   Mike Strobel    8 年前

    对于当前的编译器,防御性拷贝似乎确实同时针对“primitive”值类型和其他非只读结构。具体来说,它们的生成方式与它们的用途类似 readonly 字段:访问可能改变内容的属性或方法时。副本出现 在每个呼叫地点 到一个可能发生变化的成员,因此如果调用 n 这样的成员,你最终会 n 防御性拷贝。和…一样 只读 字段中,您可以通过手动将原件复制到本地来避免多个副本。

    看看 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,…[编译器]认为是只读的?

    不,仍将在呼叫站点为可能发生变化的成员制作防御拷贝 在里面 这些类型的参数。不过,有趣的是,这不是 全部的 方法和属性被认为是“潜在的变异”。我注意到,如果调用默认方法实现(例如, ToString GetHashCode )未发出防御性副本。但是,一旦我超越了这些方法,编译器就创建了副本:

    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 SharpLab )行为相似。注意IL是如何使用的 ldflda (载荷场 按地址 )在调用时将调用目标推送到堆栈上 WithDefault.ToString ,但使用 ldfld , stloc , ldloca 按顺序 复制 调用时放入堆栈 WithOverride.ToString :

    struct WithDefault {}
    struct WithOverride { public override string ToString() => "RO"; }
    
    static readonly WithDefault D;
    static readonly WithOverride O;
    
    // Original:
    static void Test() {
        D.ToString();
        O.ToString();
    }
    
    // IL Disassembly:
    .method private hidebysig static void Test () cil managed {
        .maxstack 1
        .locals init ([0] valuetype Overrides/WithOverride)
    
        // [WithDefault] Invoke on original by address:
        IL_0000: ldsflda valuetype Overrides/WithDefault Overrides::D
        IL_0005: constrained. Overrides/WithDefault
        IL_000b: callvirt instance string [mscorlib]System.Object::ToString()
        IL_0010: pop
    
        // [WithOverride] Copy original to local, invoke on copy by address:
        IL_0011: ldsfld valuetype Overrides/WithOverride Overrides::O
        IL_0016: stloc.0
        IL_0017: ldloca.s 0
        IL_0019: constrained. Overrides/WithOverride
        IL_001f: callvirt instance string [mscorlib]System.Object::ToString()
        IL_0024: pop
        IL_0025: ret
    }
    

    也就是说,既然只读引用可能会变得更常见,那么可以调用的方法的“白名单”就可以了。 没有 防御性拷贝可能会在未来增长。目前,这似乎有些武断。

        4
  •  1
  •   InBetween    8 年前

    这对于内置原语(如int、double)意味着什么?

    没有什么, int double 所有其他内置的“原语”都是不变的。你不能变异 双重的 ,一个 int 或A DateTime . 一个典型的框架类型不是一个好的候选者是 System.Drawing.Point 例如。

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

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

    推荐文章