C# 7.2引入了in
修饰符,用于通过引用传递参数,并保证接收者不会修改参数。
此article表示:
切勿将非只读结构用作in参数,因为这可能会对性能产生负面影响,并且如果该结构是可变的,还可能导致模糊的行为
这对于int
、double
等内置原语意味着什么
我想使用in
在代码中表达意图,但不会以防御性副本的性能损失为代价。
问题
in
参数传递原始类型并且不制作防御性副本是否安全?DateTime
、TimeSpan
、Guid
等...如果平台不同,我们如何才能找出在给定的situation?中哪些类型是安全的?
发布于 2018-06-12 20:58:46
快速测试表明,目前,是的,为内置的原始类型和结构创建了一个防御性副本。
使用VS 2017 (.NET 4.5.2,C# 7.2,release build)编译以下代码:
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
发布于 2018-06-12 20:38:48
在当前的编译器中,似乎确实为'primitive‘值类型和其他非只读结构创建了防御性副本。具体地说,它们的生成方式类似于readonly
字段:当访问可能会改变内容的属性或方法时。这些副本在每个调用点对一个潜在的变异成员显示为,所以如果您调用n个这样的成员,您将最终生成n个防御性副本。
看看this suite of examples吧。您可以查看IL和JIT程序集。
在参数中通过传递原始类型而不进行防御性复制是否安全?
如果不是,你可能不会:
// 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
}
如果平台不同,我们如何找出在给定情况下哪些类型是安全的?
好吧,所有的类型都是“安全的”--副本确保了这一点。我假设你问的是哪种类型可以避免防御性拷贝。如果不存在这样的引用,则不需要制作副本。此外,是否复制的决定可能取决于您是否调用一个已知是安全的或“纯”的成员,而不是一个可能会改变a值类型内容的成员。
如果我不得不猜测,这是一个预先存在的行为的结果,编译器正在利用一些最初为readonly
字段开发的“只读”引用的概念。正如您在下面看到的(或in SharpLab),行为是相似的。注意,在调用WithDefault.ToString
时,IL如何使用ldflda
(按地址加载字段)将调用目标推送到堆栈上,但在调用WithOverride.ToString
时,使用ldfld
、stloc
、ldloca
序列将副本推送到堆栈上
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
}
也就是说,既然只读引用可能会变得更加常见,那么可以在没有防御性副本的情况下调用的方法的“白名单”在未来可能会增长。就目前而言,这似乎有点武断。
发布于 2018-06-13 09:37:08
从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内联,这个额外的开销通常可以取消,但并不总是如此。
https://stackoverflow.com/questions/50777828
复制相似问题