首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >在具有基元类型的参数的修饰符中使用C# 7.2

在具有基元类型的参数的修饰符中使用C# 7.2
EN

Stack Overflow用户
提问于 2018-06-10 03:33:51
回答 4查看 1.1K关注 0票数 17

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

article表示:

切勿将非只读结构用作in参数,因为这可能会对性能产生负面影响,并且如果该结构是可变的,还可能导致模糊的行为

这对于intdouble等内置原语意味着什么

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

问题

  • 通过in参数传递原始类型并且不制作防御性副本是否安全?
  • 是其他常用的框架结构,如DateTimeTimeSpanGuid等...如果平台不同,我们如何才能找出在给定的situation?

中哪些类型是安全的?

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2018-06-12 20:58:46

快速测试表明,目前,是的,为内置的原始类型和结构创建了一个防御性副本。

使用VS 2017 (.NET 4.5.2,C# 7.2,release build)编译以下代码:

代码语言:javascript
复制
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反编译时会产生以下结果:

代码语言:javascript
复制
...
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:)

代码语言:javascript
复制
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
票数 9
EN

Stack Overflow用户

发布于 2018-06-12 20:38:48

在当前的编译器中,似乎确实为'primitive‘值类型和其他非只读结构创建了防御性副本。具体地说,它们的生成方式类似于readonly字段:当访问可能会改变内容的属性或方法时。这些副本在每个调用点对一个潜在的变异成员显示为,所以如果您调用n个这样的成员,您将最终生成n个防御性副本。

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

在参数中通过传递原始类型而不进行防御性复制是否安全?

如果不是,你可能不会:

代码语言:javascript
复制
// 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),则不会发出防御性副本。

代码语言:javascript
复制
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
}

如果平台不同,我们如何找出在给定情况下哪些类型是安全的?

好吧,所有的类型都是“安全的”--副本确保了这一点。我假设你问的是哪种类型可以避免防御性拷贝。如果不存在这样的引用,则不需要制作副本。此外,是否复制的决定可能取决于您是否调用一个已知是安全的或“纯”的成员,而不是一个可能会改变a值类型内容的成员。

如果我不得不猜测,这是一个预先存在的行为的结果,编译器正在利用一些最初为readonly字段开发的“只读”引用的概念。正如您在下面看到的(或in SharpLab),行为是相似的。注意,在调用WithDefault.ToString时,IL如何使用ldflda (按地址加载字段)将调用目标推送到堆栈上,但在调用WithOverride.ToString时,使用ldfldstlocldloca序列将副本推送到堆栈上

代码语言:javascript
复制
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
EN

Stack Overflow用户

发布于 2018-06-13 09:37:08

从jit的角度来看,in更改了参数的调用约定,使其始终按引用传递。因此,对于原语类型(复制成本很低)和通常按值传递的类型,如果使用in,调用方和被调用方都会有少量额外开销。然而,没有防御性的拷贝。

例如在

代码语言:javascript
复制
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的代码是

代码语言:javascript
复制
   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中读取值:

代码语言:javascript
复制
;; F0
   8B01                 mov      eax, dword ptr [rcx]
   FFC0                 inc      eax
   C3                   ret

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

如果jit内联,这个额外的开销通常可以取消,但并不总是如此。

票数 4
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/50777828

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档