Использование C# 7.2 в модификаторе для параметров с примитивными типами
C# 7.2 представил in
модификатор для передачи аргументов по ссылке с гарантией того, что получатель не изменит параметр.
Эта статья говорит:
Вы никогда не должны использовать не-только для чтения структуру в качестве параметров in, потому что это может негативно повлиять на производительность и может привести к неясному поведению, если структура изменчива
Что это значит для встроенных примитивов, таких как int
, double
?
Я хотел бы использовать in
выражать намерение в коде, но не за счет потерь производительности для защитных копий.
Вопросы
- Безопасно ли передавать примитивные типы через
in
аргументы, а не защитные копии сделаны? - Являются ли другие часто используемые каркасные структуры, такие как
DateTime
,TimeSpan
,Guid
,... считаетсяreadonly
по JIT?- Если это зависит от платформы, как мы можем узнать, какие типы безопасны в данной ситуации?
4 ответа
Быстрый тест показывает, что в настоящее время да, защитная копия создается для встроенных примитивных типов и структур.
Компиляция следующего кода с 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_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
В текущем компиляторе защитные копии действительно создаются как для "примитивных" типов значений, так и для других структур, не предназначенных только для чтения. В частности, они создаются так же, как они предназначены для readonly
поля: при доступе к свойству или методу, которые могут изменить его содержимое. Копии появляются на каждом сайте вызовов потенциально мутирующему участнику, поэтому, если вы вызовете n таких участников, вы в конечном итоге сделаете n защитных копий. Как с readonly
поля, вы можете избежать нескольких копий, вручную копируя оригинал в локальную.
Посмотрите на этот набор примеров. Вы можете просмотреть сборку 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
поля. Как вы можете видеть ниже (или в 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
}
Тем не менее, теперь, когда ссылки только для чтения, по-видимому, станут более распространенными, "белый список" методов, которые можно вызывать без защитных копий, в будущем может возрасти. Пока что это выглядит несколько произвольно.
С точки зрения 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
х не может быть зарегистрирован.
И код для F0 & F1
показывает, что первое должно теперь читать значение из byref:
;; F0
8B01 mov eax, dword ptr [rcx]
FFC0 inc eax
C3 ret
;; F1
8D4101 lea eax, [rcx+1]
C3 ret
Эта дополнительная стоимость обычно может быть отменена, если JIT встроен, хотя и не всегда.
Что это значит для встроенных примитивов, таких как int, double?
Ничего такого, int
а также double
и все остальные встроенные "примитивы" неизменны. Вы не можете изменить double
, int
или DateTime
, Типичный тип структуры, который не был бы хорошим кандидатом, System.Drawing.Point
например.
Если честно, документация может быть немного более понятной; В этом контексте readonly - это запутанный термин, он должен просто сказать, что тип должен быть неизменным.
Нет правила знать, является ли какой-либо данный тип неизменным или нет; только тщательная проверка API может дать вам представление, или, если вам повезет, в документации может быть указано, есть она или нет.