Skip to content

Repository files navigation

ObjectSizeProof

Замеры к статье «А чё, так можно было? Порядок полей = 1,8× к скорости» — сколько занимает объект в куче, почему порядок полей меняет размер структуры и как устроены массивы и строки.

BenchmarkDotNet 0.15.8, Release, .NET 8, .NET 9 и .NET 10 в одном запуске, со снятием машинного кода.

Что проверяется

Раздел Что смотрим Что сравнивается
1 размер объекта в куче классы с одним полем каждого типа и с несколькими полями одного типа
2 порядок полей те же поля в разном порядке: размер структуры и время обхода массива
3 массивы и строки как размер зависит от длины и от типа элемента

Стенд

Машина Процессор Система
Комп 1 Intel Core i9-10900KF 3.70GHz, 10 ядер Windows 10 22H2
Комп 2 AMD Ryzen 9 5950X 3.39GHz, 16 ядер Windows 10 1809
Комп 3 Intel Xeon W-2255 3.70GHz, 10 ядер Windows Server 2022
Комп 4 Intel Xeon Silver 4314 2.40GHz, 2 CPU, 32 ядра Windows Server 2022

Все машины x64. Рантаймы 8.0.30, 9.0.19 и 10.0.5, SDK 11 в предварительной сборке. Выгрузки лежат в Results: Comp_1 — i9-10900KF, Comp_2 — Ryzen 9 5950X, Comp_3 — Xeon W-2255, Comp_4 — Xeon Silver 4314. Журналы прогона в репозиторий не вошли: они по мегабайту и нужны только при разборе сбоя.

Размеры совпали на четырёх машинах и трёх рантаймах до байта, поэтому в таблицах с размерами они приведены одним столбцом.

Результаты

1. Сколько занимает объект

Класс с одним полем. Тип поля не важен, пока он помещается в 8 байт.

Тип поля Размер объекта
полей нет 24
bool, byte, short, char 24
int, float 24
long, double, nint, ссылка 24
DateTime, TimeSpan 24
decimal 32
Guid 32

decimal и Guid сами занимают по 16 байт, поэтому в 24 не помещаются.

Несколько полей одного типа. Прочерк означает, что такое сочетание не замерялось.

Полей bool char int long
1 24 24 24
2 24 24 24 32
3 32 40
4 24 24 32
5 32
8 24
9 32

Класс с восемью полями bool занимает столько же, сколько класс без полей. Девятое поле добавляет 8 байт. У char граница проходит между четырьмя и пятью полями, у int — между двумя и тремя.

2. Порядок полей

Структура Размер Смещения полей
byte, long, byte 24 A = 0, B = 8, C = 16
byte, byte, long 16 A = 0, B = 1, C = 8

Обход массива таких структур, сумма трёх полей на элемент. Через дробь: byte-long-byte и byte-byte-long.

Элементов Комп 1 Комп 2 Комп 3 Комп 4
10 000 5,990 / 4,503 6,758 / 4,552 7,581 / 6,388 10,738 / 7,732
1 000 000 1 106,628 / 685,847 781,340 / 467,193 2 306,907 / 1 528,505 1 738,395 / 963,587
10 000 000 15 032,813 / 10 241,234 11 590,615 / 7 855,197 25 407,261 / 19 368,161 20 888,186 / 15 924,898

Микросекунды, .NET 10. Разница от 1,19 до 1,80 раза в зависимости от машины и размера массива.

С классом такой приём не сработает: раскладка у классов автоматическая, и порядок полей выбирает среда выполнения. В таблице размеры с учётом заголовка — к 24 и 16 байтам самой структуры добавляются 16 байт служебных полей.

Тип Порядок byte, long, byte Порядок byte, byte, long
class 32 32
struct в упаковке 40 32

3. Массивы и строки

Длина byte[] int[] long[] object[] строка
0 24 24 24 24 не выделяется
1 32 32 32 32 24
2 32 40 40 32
4 40 32
8 32 40
9 40
16 40

byte[1] и byte[8] занимают поровну: под данные отводится не меньше 8 байт. Девятый байт добавляет ещё 8.

Что выводит отчёт header:

  тип                  первое поле   размер элемента   база

  object без полей       0x00000200               нет     24
  class {int × 3}        0x00000200               нет     32
  byte[8]                0x800A0001                 1     24
  int[4]                 0x800A0004                 4     24
  object[2]              0x810A0008                 8     24
  строка из 4            0x80000002                 2     22

База у массива равна 24: 16 байт служебных полей и 8 под длину. У строки база 22 — в неё входит место под завершающий ноль, который рантайм добавляет после последнего символа.

Как воспроизвести

Нужны SDK .NET 8, 9 и 10: BenchmarkDotNet поднимает по процессу на рантайм.

dotnet --list-sdks

В пути к проекту не должно быть запятых и точек с запятой. BenchmarkDotNet собирает вспомогательный проект и передаёт путь в MSBuild без кавычек, тот разбирает эти знаки как разделители списка свойств и падает с MSB1006. Отчёты при этом снимаются нормально, а замеры молча не выполняются. Батник проверяет путь и останавливается сразу.

Весь прогон одной командой:

all.bat

Вручную, без скрипта:

dotnet run -c Release -f net10.0 -- checks
dotnet run -c Release -f net10.0 -- sizes
dotnet run -c Release -f net10.0 -- layout
dotnet run -c Release -f net10.0 -- header
dotnet run -c Release -f net10.0 -- --filter *
dotnet run -c Release -f net10.0 -- --filter *StructArrayBench*

Аргумент noasm выключает снятие машинного кода.

Причина любого сбоя записывается в Bdn\ObjectSizeProof.log: он пишется всегда и содержит вывод дочернего процесса целиком.

Как устроен замер

Размер объекта считается двумя независимыми способами, и оба должны сойтись.

Первый — счётчик рантайма GC.GetAllocatedBytesForCurrentThread. Объект создаётся тысячу раз, разница делится на тысячу.

Второй — чтение таблицы методов по указателю. Её раскладка описана на C# в RuntimeHelpers.CoreCLR.cs: по смещению 4 записан базовый размер экземпляра, в младшем слове первого поля — размер элемента для массивов и строк, а старший бит того же поля показывает, есть ли этот размер вообще. Полный размер считается как база плюс длина на размер элемента, округляется вверх до восьми и не бывает меньше 24.

Сверка сравнивает оба способа на девяти типах и останавливает прогон при расхождении. Смещения заданы числами, поэтому смена реализации сломает чтение — лучше остановиться, чем дать статье неверные числа.

Все методы создания помечены NoInlining, и созданный объект уходит в поле, которое читается. Иначе JIT увидит, что объект не выходит за пределы вызова, и не выделит под него память: с .NET 9 он это умеет.

Строка нулевой длины в рантайме одна на процесс, поэтому память под неё не выделяется. Отчёт помечает такую строку отдельно, а не считает расхождением.

Что проверялось и чем

Что проверялось Чем
Таблица методов читается верно отчёт checks сравнивает расчёт по ней со счётчиком выделенной памяти на девяти типах
Размер элемента есть только у массивов и строк отчёт checks проверяет старший бит флагов и сам размер
Порядок полей меняет размер структуры отчёт checks сравнивает две структуры с одинаковыми полями
Результат не зависит от инструмента отчёты работают без BenchmarkDotNet, а столбец Allocated подтверждает те же числа
Результат не зависит от размера массива замер обхода на трёх размерах
Дело не в сборщике мусора режим DOTNET_gcServer=1
Дело не в конкретной машине четыре машины, выгрузки всех четырёх в Results
Дело не в конкретной версии .NET 8, 9 и 10 в одном запуске

Все замеры сняты на x64. В 32-битной сборке указатель занимает 4 байта, служебные поля — 8, и числа получатся другими.

Дизассемблер на Linux требует установленного perf. Выключается аргументом noasm.

Отчёты

Bdn\results\                     отчёты BenchmarkDotNet: csv, md, html, машинный код
Bdn\ObjectSizeProof.log          журнал прогона: причины сбоев только здесь
Results\Comp_N\
    checks_netN.0.txt            сверка, роняет прогон при расхождении
    sizes_netN.0.txt             таблица размеров по всем типам
    layout_netN.0.txt            размеры структур и смещения полей
    header_netN.0.txt            содержимое таблицы методов
    sizes_netN.0_servergc.txt    то же на серверном сборщике
    probe_noasm.txt              проба на одном классе без дизассемблера
    probe_asm.txt                та же проба с дизассемблером
    bench\                       отчёты BenchmarkDotNet

Что где лежит

ObjectSizeProof.csproj       многоцелевой: net8.0, net9.0, net10.0
ObjectSizeProof.slnx
Program.cs                   точка входа, разбор аргументов
Subjects.cs                  создание объектов, размер которых замеряется
README.md
all.bat                      весь прогон одной командой
Benchmarks\
    CreateBench.cs           раздел 1, создание объектов
    StructArrayBench.cs      раздел 2, обход массива структур
Types\
    BenchmarkConfig.cs       три рантайма, дизассемблер, колонки и журнал
    Shapes.cs                классы и структуры для замеров
    Innards.cs               чтение таблицы методов
Diagnostics\
    Checks.cs                сверка, код возврата
    Sizes.cs                 таблица размеров
    Layout.cs                раскладка полей структуры
    Header.cs                содержимое таблицы методов
Results\
    Comp_1 .. Comp_4\        выгрузки прогона, по папке на машину

Ссылки

Границы

Числа в этом файле взяты только из отчётов.

Ссылки на исходники .NET даны на тег v10.0.0, коммит зафиксирован: main уедет, и номера строк перестанут совпадать.

About

Сколько байт занимает объект в .NET, почему порядок полей меняет размер структуры и как устроены массивы и строки. Замеры на .NET 8/9/10, четыре машины.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages