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

Другие вопросы по тегам