Использование C # 7.2 в модификаторе для параметров с примитивными типами - PullRequest
0 голосов
/ 09 июня 2018

C # 7.2 представил модификатор in для передачи аргументов по ссылке с гарантией того, что получатель не изменит параметр.

Эта статья говорит:

Никогда не следует использовать структуру non-readonly в качестве параметров in, поскольку она может негативно повлиять на производительность и привести к неясному поведению, если структура изменчива.

Что это означает для встроенныхв примитивах, таких как int, double?

Я хотел бы использовать in для выражения намерений в коде, но не за счет потерь производительности для защитных копий.

Вопросы

  • Безопасно ли передавать примитивные типы с помощью in аргументов и не делать защитных копий?
  • Существуют ли другие часто используемые структуры фреймворка, такие как DateTime, TimeSpanGuid, ... считается JIT readonly?
    • Если это зависит от платформы, как мы можем узнать, какие типы безопасны в данной ситуации?

Ответы [ 4 ]

0 голосов
/ 13 июня 2018

С точки зрения 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 встроенхотя и не всегда.

0 голосов
/ 12 июня 2018

При использовании текущего компилятора защитные копии действительно создаются как для «примитивных» типов значений, так и для других структур, не предназначенных только для чтения.В частности, они генерируются аналогично тому, как они используются для полей 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
}

Тем не менее теперь, когда ссылки только для чтения станут более распространенными, «белый список» методов, которые могутбыть вызванным без защитные копии могут вырасти в будущем.Пока что это выглядит несколько произвольно.

0 голосов
/ 12 июня 2018

Быстрый тест показывает, что в настоящее время да, защитная копия создается для встроенных примитивных типов и структур.

Компиляция следующего кода с VS 2017 (.NET 4.5.2, C # 7.2,Release build):

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
0 голосов
/ 09 июня 2018

Что это означает для встроенных примитивов, таких как int, double?

Ничего, int и double и все другие встроенные "примитивы" являются неизменяемыми,Вы не можете изменить double, int или DateTime.Типичный тип фреймворка, который не будет подходящим кандидатом, например, System.Drawing.Point.

Если честно, документация может быть немного более понятной;В данном контексте readonly - запутанный термин, он должен просто сказать, что тип должен быть неизменным.

Нет правила знать, является ли какой-либо данный тип неизменным или нет;только тщательная проверка API может дать вам представление, или, если вам повезет, в документации может быть указано, есть она или нет.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...