C#7.2引入了in
修饰符,用于通过引用传递参数,并保证收件人不会修改参数。
这article说:
你永远不应该使用非readonly结构作为in参数,因为它可能会对性能产生负面影响,如果结构是可变的,可能会导致模糊的行为
这对于int
,double
等内置基元意味着什么?
我想使用in
来表达代码意图,但不会以防御性副本的性能损失为代价。
问题
in
参数传递原始类型并且没有制作防御性副本是否安全?DateTime
,TimeSpan
,Guid
,......是否被JIT视为readonly
?
答案 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
参数上的潜在变异成员制作防御性副本。但有趣的是,并非所有方法和属性都被视为“可能会发生变异”。我注意到,如果我调用默认方法实现(例如,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
个参数的某些引用处发出的,例如,引用是调用目标。如果没有这样的参考,则不需要制作副本。此外,是否复制的决定取决于您是否调用已知安全或“纯”的成员与可能会改变值类型内容的成员。
目前,某些默认方法似乎被视为纯粹的,并且编译器避免在这些情况下进行复制。如果我不得不猜测,这是预先存在的行为的结果,并且编译器正在使用最初为readonly
字段开发的“只读”引用的概念。如下所示(或in SharpLab),行为类似。请注意IL在调用ldflda
时如何使用WithDefault.ToString
(按地址加载字段)将调用目标推送到堆栈,但使用ldfld
,{{ 1}},stloc
序列,在调用ldloca
时将副本推送到堆栈:
WithOverride.ToString
那就是说,现在只读参考文献可能会变得更加普遍,但是可以在没有防御性副本的情况下调用的方法的“白名单”可能会在将来增长。现在,它似乎有点武断。
答案 3 :(得分:1)
这对于内置基元(例如int,double?
)意味着什么
没有,int
和double
以及所有其他内置&#34;原语&#34;是不可改变的。您无法改变double
,int
或DateTime
。例如,System.Drawing.Point
是一个典型的框架类型,不是一个好的候选者。
老实说,文档可能会更清晰一些; readonly在这种情况下是一个令人困惑的术语,它应该简单地说类型应该是不可变的。
没有规则知道任何给定类型是否是不可变的;只有仔细检查API才能给你一个想法,或者,如果幸运的话,文档可能会说明它是否存在。