Замеры к статье «А чё, так можно было? Порядок полей = 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. Журналы прогона
в репозиторий не вошли: они по мегабайту и нужны только при разборе сбоя.
Размеры совпали на четырёх машинах и трёх рантаймах до байта, поэтому в таблицах с размерами они приведены одним столбцом.
Класс с одним полем. Тип поля не важен, пока он помещается в 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 — между двумя и тремя.
| Структура | Размер | Смещения полей |
|---|---|---|
| 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 |
| Длина | 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\ выгрузки прогона, по папке на машину
- Таблица методов, описанная на C#
- Расчёт полного размера объекта
- Заголовок массива и его длина
- GC.GetAllocatedBytesForCurrentThread
- Unsafe.SizeOf
- StructLayoutAttribute и раскладка полей
Числа в этом файле взяты только из отчётов.
Ссылки на исходники .NET даны на тег v10.0.0, коммит зафиксирован: main
уедет, и номера строк перестанут совпадать.