Использование С# 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 может дать вам идею или, если вам повезет, документация может указать, есть она или нет.