Уклонение от удара по производительности при использовании `in` со структурой, не делая структуру доступной только для чтения?
В C# 7.2 добавлены две новые функции:
В параметрах
С помощью
in
для параметра давайте передадим по ссылке, но затем не позволим нам присвоить ему значение. Однако на самом деле производительность может ухудшиться, потому что она создает "защитную копию" структуры, копируя все этоТолько для чтения
Способ обойти это использовать
readonly
дляstruct
, Когда вы передаете его вin
параметр, компилятор видит, что этоreadonly
и не будет создавать защитную копию, тем самым делая ее лучшей альтернативой для исполнения.
Это все здорово, но каждое поле в struct
должен быть readonly
, Это не работает:
public readonly struct Coord
{
public int X, Y; // Error: instance fields of readonly structs must be read only
}
Авто-свойства также должны быть readonly
,
Есть ли способ получить выгоду in
параметры (проверка во время компиляции, чтобы убедиться, что параметр не изменяется, передавая по ссылке), при этом все еще имея возможность изменять поля struct
без значительного снижения производительности in
вызвано созданием защитной копии?
3 ответа
Когда вы проходите
readonly struct
] в параметре in компилятор видит, что он доступен только для чтения и не будет создавать защитную копию.
Я думаю, что вы не поняли. Компилятор создает защитную копию переменной только для чтения, которая содержит struct
(это может быть in
параметр, но также readonly
поле), когда вы вызываете метод на этомstruct
,
Рассмотрим следующий код:
struct S
{
int x, y;
public void M() {}
}
class C
{
static void Foo()
{
S s = new S();
Bar(s);
}
static void Bar(in S s)
{
s.M();
}
}
Вы можете проверить IL, сгенерированный для кода выше, чтобы увидеть, что на самом деле произойдет.
За Foo
IL это:
ldloca.s 0 // load address of the local s to the stack
initobj S // initialize struct S at the address on the stack
ldloca.s 0 // load address of the local s to the stack again
call void C::Bar(valuetype S&) // call Bar
ret // return
Обратите внимание, что нет копирования: местный s
инициализируется, а затем адрес к этому локальному Bar
,
IL для Bar
является:
ldarg.0 // load argument s (which is an address) to the stack
ldobj S // copy the value from the address on the stack to the stack
stloc.0 // store the value from the stack to an unnamed local variable
ldloca.s 0 // load the address of the unnamed local variable to the stack
call instance void S::M() // call M
ret // return
Здесь ldobj
а также stloc
инструкции создать защитную копию, чтобы убедиться, что если M
мутирует struct
, s
не будет видоизменяться (так как он доступен только для чтения).
Если вы измените код, чтобы сделать S
readonly struct
то ИЛ для Foo
остается прежним, но для Bar
меняется на:
ldarg.0 // load argument s (which is an address) to the stack
call instance void S::M() // call M
ret // return
Обратите внимание, что здесь больше нет копирования.
Это защитная копия, обозначающая ваш struct
как readonly
избегает. Но если вы не вызовете какие-либо методы экземпляра в структуре, защитных копий не будет.
Также обратите внимание, что язык диктует, что когда код выполняется, он должен вести себя так, как если бы защитная копия была там. Если JIT может выяснить, что копия на самом деле не нужна, ее можно избежать.
Как насчет такого объявления:
public struct ExampleStruct
{
public double A { readonly get => _a; set => _a = value; }
public double B { readonly get => _b; set => _b = value; }
public double C { readonly get => _c; set => _c = value; }
private double _a;
private double _b;
private double _c;
public ExampleStruct(double a, double b, double c)
{
_a = a;
_b = b;
_c = c;
}
}
Это позволяет мне изменять данные и хорошо работать в тестах
Во-первых, я бы любой ценой избегал структур, не предназначенных только для чтения. Только очень строгие требования к производительности могут оправдать изменяемую структуру в горячих путях выполнения без выделения памяти. И если ваша структура изменяема по своей сути, зачем делать ее только для чтения для одного метода? Это опасный путь, подверженный ошибкам.
Факт: сочетание in
передача параметров со структурами, не предназначенными только для чтения, приведет к защитной копии до того, как ссылка на эту копию будет передана в метод.
Таким образом, любая изменчивость будет работать с копией компилятора, видимой внутри контекста метода, но не видимой для вызывающего. Запутанно и не ремонтируется.
я думал in
параметры полезны, чтобы помочь компилятору принимать разумные решения для повышения производительности. Это определенно неправда! Я экспериментировал сin
а также readonly
структур по соображениям производительности, и мой вывод: слишком много подводных камней, которые на самом деле замедляют ваш код. Если вы единственный разработчик в проекте, если ваши структуры достаточно велики, вы хорошо знаете все хитрости компилятора и часто запускаете микротесты... тогда вы можете выиграть с точки зрения производительности.