Использование С# 7.2 в модификаторе для параметров с примитивными типами
С# 7.2 введены in
модификаторе для передачи аргументов по ссылке с гарантией того, что получатель не будет изменять этот параметр.
В этой статье говорится:
Вы никогда не должны использовать структуру, отличную от readonly, как параметры, поскольку это может отрицательно повлиять на производительность и может привести к неясному поведению, если структура изменена
Что это значит для встроенных примитивов, таких как int
, double
?
Я хотел бы использовать in
выражать намерение в коде, но не за счет потери производительности в оборонительные копии.
Вопросы
- Является ли это безопасно передавать примитивные типы с помощью
in
аргументах и не имеют защитные копии сделали? - Являются ли другие обычно используемые структурные структуры, такие как
DateTime
, TimeSpan
, Guid
,... считаются только для readonly
JIT? - Если это зависит от платформы, как мы можем узнать, какие типы безопасны в данной ситуации?
Ответы
Ответ 1
Быстрый тест показывает, что в настоящее время да, защитная копия создается для встроенных примитивных типов и структур.
Компиляция следующего кода с VS 2017 (.NET 4.5.2, С# 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_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
С точки зрения 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 inlines, хотя и не всегда.
Ответ 3
С текущим компилятором защитные копии действительно выглядят как для "примитивных" типов значений, так и для других нечитанных структур. В частности, они генерируются аналогично тому, как они относятся к полям только для readonly
: при доступе к свойству или методу, которые могут потенциально мутировать содержимое. Копии отображаются на каждом сайте вызова потенциально мутирующему элементу, поэтому, если вы вызываете n таких членов, вы в конечном итоге делаете n защитных копий. Как и для полей readonly
, вы можете избежать множественных копий, скопировав оригинал вручную.
Взгляните на этот набор примеров. Вы можете просмотреть как IL, так и JIT-сборку.
Безопасно ли передавать примитивные типы по аргументам и не создавать защитные копии?
Это зависит от того, используете ли вы метод или свойство in
параметре 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,... считаются readonly [компилятором]?
Нет, защитные копии будут по-прежнему выполняться на сайтах вызовов для потенциально мутирующих членов 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
}
Если это зависит от платформы, как мы можем узнать, какие типы безопасны в данной ситуации?
Ну, все типы являются "безопасными" --the копиями, гарантируют это. Я предполагаю, что вы спрашиваете, какие типы избегают защитной копии. Как мы видели выше, это сложнее, чем "какой тип параметра"? Там нет единой копии: копии излучаются в определенных ссылках на in
параметрах, например, где ссылка является целевым вызовом. Если таких ссылок нет, копий не требуется. Более того, решение о том, следует ли копировать, может зависеть от того, ссылаетесь ли вы на элемент, который, как известно, является безопасным или "чистым", и членом, который может потенциально мутировать содержимое типа значения.
На данный момент некоторые методы по умолчанию выглядят как чистые, и компилятор избегает делать копии в этих случаях. Если бы я должен был догадаться, это результат предсуществующего поведения, и компилятор использует некоторое понятие "только для чтения", которое было первоначально разработано для полей readonly
. Как вы можете видеть ниже (или в 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
Что это значит для встроенных примитивов, таких как int, double?
Ничто, int
и double
и все другие встроенные "примитивы" не изменяются. Вы не можете мутировать double
, int
или DateTime
. Типичным типом структуры, который не был бы хорошим кандидатом, является, например, System.Drawing.Point
.
Честно говоря, документация может быть немного понятнее; readonly является запутанным термином в этом контексте, он должен просто сказать, что тип должен быть неизменным.
Нет никакого правила знать, является ли какой-либо данный тип неизменным или нет; только тесная проверка API может дать вам идею или, если вам повезет, документация может указать, есть она или нет.