Оптимизация C# кода посредством структур

14 мин. чтения
08.08.2025

description

В современной разработке синхронный код встречается на удивление редко. Соответственно всё реже можно увидеть использование структур в 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;

description 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 структуры у меня работали быстрее «оптимизированных»

description Передача по ссылке через ключевое слово «in» обычный структуры без модификатора «readonly» в данном случае сработала даже быстрее оптимизированного варианта. Почему? Затрудняюсь ответить. Разработчикам из Microsoft виднее

Однако в общем случае оптимизации связанные с использованием in работают. И это здорово. Кстати, обратите внимание, что само по себе использование readonly структуры заметно увеличивает производительность - компилятор сам понимает, что можно передавать значение по ссылке

description Здесь окончанием In отмечены бенчмарки, в которых параметр передавался с ключевым словом in; префиксом Readonly - бенчмарки, в которых передавалась readonly структура

Итого

В целом я придерживаюсь следующих простых правил:

Чтобы случайно не допустить где-то ошибку при работе со структурами я пользуюсь следующими синтаксическими фишками: