alexeyfv

Опубликовано

- 2 мин чтения

Правило 16 байт: Производительность структур в C#

C# Производительность
img of Правило 16 байт: Производительность структур в C#

Многие разработчики на C# знают: при передаче или возврате из метода экземпляр типа-значения (структуры) копируется, а экземпляр ссылочного типа (класса) передаётся по ссылке. Отсюда пошло мнение, что использование структур может ухудшать производительность приложения, особенно если размер структуры больше 16 байт. Споры об этом идут до сих пор. В этой статье попробуем разобраться.

Дисклеймер

Всё, что написано ниже, справедливо только при определённых условиях. На другом процессоре или с другими структурами результаты могут отличаться. Проверяйте свой код на своём железе и не полагайтесь только на статьи из интернета. :)

Бенчмарк

Бенчмарк простой: создаются структуры и классы размерами от 4 до 160 байт с шагом 4 байта (один int).

   public record struct Struct04(int Param);

// другие структуры

public record struct Struct160(
int Param1, int Param2, ..., int Param40);

public record class Class04(int Param);

// другие классы

public record class Class160(
int Param1, int Param2, ..., int Param40);

Для каждой структуры и класса есть метод, который создаёт экземпляр из одного значения int:

   // другие методы

public static Struct20 GetStruct20(int value) => new(value, value, value, value, value);

// другие методы

public static Class20 GetClass20(int value) => new(value, value, value, value, value);

// другие методы

И методы бенчмарка создают списки из 1000 структур или классов:

   public int Iterations { get; set; } = 1000;

private static void Add<T>(List<T> list, T value) => list.Add(value);

[Benchmark(Baseline = true)]
public List<Struct04> GetStruct4()
{
    var list = new List<Struct04>(Iterations);
    for (int i = 0; i < Iterations; i++) Add(list, GetStruct04(i));
    return list;
}

// другие методы

[Benchmark(Baseline = true)]
public List<Class04> GetClass4()
{
    var list = new List<Class04>(Iterations);
    for (int i = 0; i < Iterations; i++) Add(list, GetClass04(i));
    return list;
}

// другие методы

Для тестов использовалась библиотека BenchmarkDotNet. Весь код можно найти здесь.

Результаты

Измерение времени

График показывает, что структуры работают быстрее, пока их размер не превышает 64 байта.

График производительности: структуры против классов

Если увеличить масштаб для размеров до 64 байт, видно, что структуры могут быть быстрее классов на 40–70%.

Детальное сравнение производительности для структур до 64 байт

Чтобы понять, почему так происходит, нужно посмотреть на скомпилированный код. Например, методы GetStruct64 и GetStruct128 компилируются в одинаковый IL-код.

Сравнение IL-кода: одинаковая компиляция

Это значит, что стоит посмотреть ещё глубже — на ассемблерный код, сгенерированный JIT-компилятором. К счастью, BenchmarkDotNet позволяет это сделать.

При сравнении видно, что метод GetStruct64(int value) не вызывается. Вместо этого в коде много операций mov, которые просто переносят данные в регистры и память — именно так создаётся Struct64. Это результат оптимизации встраивания метода (method inlining).

Оптимизация JIT: вместо вызова метода — mov операции

Ещё одно наблюдение: структуры не аллоцируются в стеке. Компилятор настолько оптимизирует код, что данные хранятся только в регистрах, даже для структур больше 128 байт (благодаря AVX-регистрам 256 бит).

Измерение памяти

График использования памяти показывает плавную зависимость без резких скачков. Это значит, что разница в потреблении памяти между структурами и классами остаётся постоянной. Для структур до 64 байт экономия памяти составляет от 27% до 87%.

Сравнение использования памяти: структуры против классов

Вывод

Эта статья показала, что использование структур не всегда ухудшает производительность. JIT-компилятор достаточно умен, чтобы оптимизировать код на лету. Структуры до 64 байт работают эффективнее классов как по скорости, так и по памяти.