在具有原始类型的参数的修饰符中使用 C# 7.2
Using C# 7.2 in modifier for parameters with primitive types
C# 7.2 引入了 in
修饰符,用于通过引用传递参数,保证接收者不会修改参数。
这个article说:
You should never use a non-readonly struct as the in parameters because it may negatively affect performance and could lead to an obscure behavior if the struct is mutable
这对 int
、double
等内置基元意味着什么?
我想使用 in
在代码中表达意图,但不会以防御副本的性能损失为代价。
问题
- 通过
in
参数传递原始类型而不制作防御性副本是否安全?
- 其他常用的框架结构,例如
DateTime
、TimeSpan
、Guid
、... 是否被 JIT 视为 readonly
?
- 如果这因平台而异,我们如何才能找出在给定情况下哪些类型是安全的?
What does this mean for built-in primitives such as int, double?
Nothing,int
和 double
以及所有其他内置的 "primitives" 都是不可变的。您不能改变 double
、int
或 DateTime
。例如,不适合的典型框架类型是 System.Drawing.Point
。
老实说,文档可能更清楚一点; readonly 在这种情况下是一个令人困惑的术语,它应该简单地说类型应该是不可变的。
没有规则可以知道任何给定类型是否不可变;只有仔细检查 API 才能给你一个想法,或者,如果你幸运的话,文档可能会说明它是否是。
对于当前的编译器,似乎确实为 'primitive' 值类型和其他非只读结构创建了防御性副本。具体来说,它们的生成方式与 readonly
字段的生成方式类似:访问可能会改变内容的 属性 或方法时。副本出现 在每个调用站点 到一个潜在的变异成员,所以如果你调用 n 这样的成员,你最终会 n个防御副本。与readonly
字段一样,可以通过手动将原件复制到本地来避免多次复制。
看看this suite of examples。您可以同时查看 IL 和 JIT 程序集。
Is it safe to pass primitive types via in arguments and not have defensive copies made?
这取决于你访问的是方法还是属性在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;
}
Are other commonly used framework structs such as DateTime, TimeSpan, Guid, ... considered readonly by [the compiler]?
不,仍将在调用站点为这些类型的 in
参数上的潜在变异成员制作防御性副本。不过,有趣的是,并非 所有 方法和属性都被视为 'potentially mutating'。我注意到,如果我调用默认方法实现(例如 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
}
If this varies by platform, how can we find out which types are safe in a given situation?
好吧,所有类型都是 'safe'--副本确保了这一点。我假设您是在问哪些类型会避免防御性副本。正如我们在上面看到的,它比 "what's the type of the parameter" 更复杂?没有单个副本:副本是在对 in
参数的某些引用处发出的,例如,引用是调用目标的地方。如果不存在此类引用,则无需制作副本。此外,是否复制的决定取决于您调用的是已知安全的成员还是 'pure' 与可能会改变值类型内容的成员。
目前,某些默认方法似乎被视为纯方法,编译器避免在这些情况下进行复制。如果非要我猜的话,这是预先存在的行为的结果,并且编译器正在使用最初为 readonly
字段开发的 'read only' 引用的某些概念。正如您在下面看到的(或 in SharpLab),行为是相似的。请注意 IL 在调用 WithDefault.ToString
时如何使用 ldflda
(通过地址 加载字段 )将调用目标压入堆栈,但使用 ldfld
, stloc
, ldloca
序列在调用 WithOverride.ToString
:
时将 copy 推入堆栈
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
}
也就是说,现在只读引用可能会变得更加普遍,'white list' 可以在 没有 防御副本的情况下调用的方法在未来可能会增加。现在看来,这似乎有些武断。
快速测试表明,目前,是的,为内置原始类型和结构创建了一个防御副本。
使用 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
从 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 内联,通常可以取消此额外成本,但并非总是如此。
C# 7.2 引入了 in
修饰符,用于通过引用传递参数,保证接收者不会修改参数。
这个article说:
You should never use a non-readonly struct as the in parameters because it may negatively affect performance and could lead to an obscure behavior if the struct is mutable
这对 int
、double
等内置基元意味着什么?
我想使用 in
在代码中表达意图,但不会以防御副本的性能损失为代价。
问题
- 通过
in
参数传递原始类型而不制作防御性副本是否安全? - 其他常用的框架结构,例如
DateTime
、TimeSpan
、Guid
、... 是否被 JIT 视为readonly
?- 如果这因平台而异,我们如何才能找出在给定情况下哪些类型是安全的?
What does this mean for built-in primitives such as int, double?
Nothing,int
和 double
以及所有其他内置的 "primitives" 都是不可变的。您不能改变 double
、int
或 DateTime
。例如,不适合的典型框架类型是 System.Drawing.Point
。
老实说,文档可能更清楚一点; readonly 在这种情况下是一个令人困惑的术语,它应该简单地说类型应该是不可变的。
没有规则可以知道任何给定类型是否不可变;只有仔细检查 API 才能给你一个想法,或者,如果你幸运的话,文档可能会说明它是否是。
对于当前的编译器,似乎确实为 'primitive' 值类型和其他非只读结构创建了防御性副本。具体来说,它们的生成方式与 readonly
字段的生成方式类似:访问可能会改变内容的 属性 或方法时。副本出现 在每个调用站点 到一个潜在的变异成员,所以如果你调用 n 这样的成员,你最终会 n个防御副本。与readonly
字段一样,可以通过手动将原件复制到本地来避免多次复制。
看看this suite of examples。您可以同时查看 IL 和 JIT 程序集。
Is it safe to pass primitive types via in arguments and not have defensive copies made?
这取决于你访问的是方法还是属性在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;
}
Are other commonly used framework structs such as DateTime, TimeSpan, Guid, ... considered readonly by [the compiler]?
不,仍将在调用站点为这些类型的 in
参数上的潜在变异成员制作防御性副本。不过,有趣的是,并非 所有 方法和属性都被视为 'potentially mutating'。我注意到,如果我调用默认方法实现(例如 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
}
If this varies by platform, how can we find out which types are safe in a given situation?
好吧,所有类型都是 'safe'--副本确保了这一点。我假设您是在问哪些类型会避免防御性副本。正如我们在上面看到的,它比 "what's the type of the parameter" 更复杂?没有单个副本:副本是在对 in
参数的某些引用处发出的,例如,引用是调用目标的地方。如果不存在此类引用,则无需制作副本。此外,是否复制的决定取决于您调用的是已知安全的成员还是 'pure' 与可能会改变值类型内容的成员。
目前,某些默认方法似乎被视为纯方法,编译器避免在这些情况下进行复制。如果非要我猜的话,这是预先存在的行为的结果,并且编译器正在使用最初为 readonly
字段开发的 'read only' 引用的某些概念。正如您在下面看到的(或 in SharpLab),行为是相似的。请注意 IL 在调用 WithDefault.ToString
时如何使用 ldflda
(通过地址 加载字段 )将调用目标压入堆栈,但使用 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
}
也就是说,现在只读引用可能会变得更加普遍,'white list' 可以在 没有 防御副本的情况下调用的方法在未来可能会增加。现在看来,这似乎有些武断。
快速测试表明,目前,是的,为内置原始类型和结构创建了一个防御副本。
使用 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
从 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 内联,通常可以取消此额外成本,但并非总是如此。