В современной разработке синхронный код встречается на удивление редко. Соответственно всё реже можно увидеть использование структур в C# коде. Иногда кажется, что программисты вовсе о них забыли и по-умолчанию плодят классы - и очень зря. Недавно я столкнулся с задачей, требующей выполнить синхронный код максимально быстро, чего удалось достичь посредством использования структур. В этой статье на примерах с бенчмарками я показываю использованные мною оптимизации
Матчасть
На всякий случай немного базы, известной всем, кто когда-либо проходил собеседования. Ключевое отличие структур от классов заключается в том, что первые (структуры) представляются значениями и создаются в стеке, в то время как классы - объектами и пишутся в кучу. Стек (stack) - это такая «быстрая» память в .NET, доступная только внутри метода, в то время как куча (heap) - более медленная память, к которой можно обращаться из любого места программы
Как ускорить синхронный метод в несколько раз
Так просто ощутить разницу в производительности довольно сложно без большого объема данных. Тем более, что .NET неплохо оптимизирован под создание объектов в куче. Тем не менее, разница есть и довольно существенная. Для того, чтобы отобразить её, я воспользовался библиотекой BenchmarkDotNet и сделал бенчмарк по двум методам: один работает с классом, другой - со структурой
[Benchmark]
public void RunСlass()
{
var a = new MyClass { Value1 = 1, Value = 2, Value2 = 3, Value3 = 4 };
Execute(a);
}
[Benchmark]
public void RunStruct()
{
var a = new MyStruct { Value1 = 1, Value = 2, Value2 = 3, Value3 = 4 };
Execute(a);
}
private void Execute(MyClass c) =>
_store = c.Value + c.Value1 + c.Value2 + c.Value3;
private void Execute(MyStruct c) =>
_store = c.Value + c.Value1 + c.Value2 + c.Value3;
RunStruct показывает производительность в два раза выше, чем RunClass
Ещё больше скорости - использование ссылок на структуры
К сожалению, копирование структуры из метода в метод может тоже ударить по производительности. Для того, чтобы прокинуть объект класса в другой метод, достаточно скопировать указатель на него (обычно это 8 байт). Но со структурами дела обстоят сложнее: для того, чтобы передать структуру в другой метод, необходимо скопировать её полностью. Для маленьких структур - это окей. Но в случае со структурами пожирнее копировать большие объемы данных может быть весьма накладным. Как быть? Бенчмарк выше немного спойлерит решение: можно передавать структуры по ссылке
public void RunRefStruct()
{
var a = new MyStruct { Value1 = 1, Value = 2, Value2 = 3, Value3 = 4 };
Execute(ref a);
}
private void Execute(ref MyStruct c)
{
// ...
}
Впрочем, в современном C# принято пользоваться ключевым словом in
. Оно гарантирует неизменяемость переданной в метод структуры. Однако рекомендуется в таком случае создавать readonly структуры, т.к. компилятор может не совсем правильно понять программиста и просто сделать копию значения, чтобы обеспечить неизменяемость
public readonly struct MyStruct2
{
// ...
}
[Benchmark]
public void RunReadonlyStruct()
{
var a = new MyStruct2 { Value1 = 1, Value = 2, Value2 = 3, Value3 = 4 };
Execute(in a);
}
private void Execute(in MyStruct2 c) {
// ...
}
Но мои опыты с бенчмарком показывают, что на сегодняшний день компилятор понимает программиста всё же достаточно правильно (по крайней мере в контексте синхронных вызовов). И иногда обычные не'readonly структуры у меня работали быстрее «оптимизированных»
Передача по ссылке через ключевое слово «in» обычный структуры без модификатора «readonly» в данном случае сработала даже быстрее оптимизированного варианта. Почему? Затрудняюсь ответить. Разработчикам из Microsoft виднее
Однако в общем случае оптимизации связанные с использованием in
работают. И это здорово. Кстати, обратите внимание, что само по себе использование readonly
структуры заметно увеличивает производительность - компилятор сам понимает, что можно передавать значение по ссылке
Здесь окончанием In отмечены бенчмарки, в которых параметр передавался с ключевым словом in; префиксом Readonly - бенчмарки, в которых передавалась readonly структура
Итого
В целом я придерживаюсь следующих простых правил:
- Если значение используется только в рамках одного стека вызовов, то лучше воспользоваться структурой вместо класса
- Если значение превышает 16 байт, то лучше передавать его по ссылке
Чтобы случайно не допустить где-то ошибку при работе со структурами я пользуюсь следующими синтаксическими фишками:
- модификатор
readonly
делает структуру неизменяемой, что позволяет компилятору в ряде случаев самостоятельно передавать её по ссылке - ключевое слово
in
при указании параметра подстраховывает от копированияreadonly
структуры