Замеры и проверки к статье «А чё, так можно было? int == -int» — что происходит на границах диапазона целых чисел и сколько стоит от этого защититься.
BenchmarkDotNet 0.15.8, Release, .NET 8, .NET 9 и .NET 10 в одном запуске, со снятием машинного кода.
Статья: «А чё, так можно было? int == -int»
| Раздел | Что смотрим | Что сравнивается |
|---|---|---|
| 1 | цена проверки переполнения | сложение без проверки, с проверкой и в длинном типе |
| 2 | середина отрезка | три способа, два из которых взяты из исходников .NET |
| 3 | модуль числа | библиотечный, через ветвление и без ветвления |
отчёт numbers |
границы диапазонов | все знаковые типы: смена знака, модуль, деление на минус единицу |
отчёт search |
двоичный поиск | где ломается середина и чем это кончается |
| Машина | Процессор | Система |
|---|---|---|
| Комп 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. Выгрузки лежат в Results: Comp_1 — i9-10900KF, Comp_2 —
Ryzen 9 5950X, Comp_3 — Xeon W-2255, Comp_4 — Xeon Silver 4314.
тип самое маленькое -x равен x модуль x / -1
sbyte -128 да исключение -128
short -32768 да исключение -32768
int -2147483648 да исключение исключение
long -9223372036854775808 да исключение исключение
nint -9223372036854775808 да исключение исключение
Одинаково на четырёх машинах и трёх рантаймах. У sbyte и short деление
на минус единицу работает: перед вычислением они расширяются до int.
| Способ | Комп 1 | Комп 2 | Комп 3 | Комп 4 |
|---|---|---|---|---|
| без проверки | 2,521 | 2,599 | 2,928 | 4,749 |
| с проверкой | 2,600 | 2,409 | 3,057 | 4,951 |
| в длинном типе | 2,488 | 2,407 | 2,946 | 4,762 |
Микросекунды на 10 000 сложений, .NET 10. Разница от 3 до 4 процентов, а на Ryzen 9 5950X версия с проверкой оказалась быстрее.
| Способ | Комп 1 | Комп 2 | Комп 3 | Комп 4 |
|---|---|---|---|---|
| привычная запись | 5,165 | 5,891 | 6,122 | 7,964 |
| через разность | 5,217 | 5,280 | 6,070 | 7,645 |
| беззнаково | 5,167 | 5,288 | 6,049 | 7,627 |
Наносекунды на четыре отрезка, .NET 10. Экономии одного вычитания, обещанной
комментарием в SpanHelpers, в замере не видно: разница от 0,3 до 0,4 процента.
| Способ | Комп 1 | Комп 2 | Комп 3 | Комп 4 |
|---|---|---|---|---|
| библиотечный | 1,370 | 1,524 | 1,951 | 2,030 |
| через ветвление | 1,049 | 1,401 | 1,466 | 1,834 |
| без ветвления | 1,050 | 1,166 | 1,463 | 1,469 |
Микросекунды на 1024 числа, .NET 10. Проверка на переполнение внутри
Math.Abs стоит от 9 до 38 процентов в зависимости от машины и способа сравнения.
low high привычно разность беззнаково
0 10 5 5 5
0 2000000000 1000000000 1000000000 1000000000
1000000000 2000000000 -647483648 1500000000 1500000000
1500000000 2000000000 -397483648 1750000000 1750000000
2000000000 2147483647 -73741824 2073741823 2073741823
2147483646 2147483647 -1 2147483646 2147483646
Поиск числа 2000000000 на отрезке от 0 до 2147483647:
привычно IndexOutOfRangeException: номер -536870912
разность найдено, шагов: 31
беззнаково найдено, шагов: 31
Нужны 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 -- numbers
dotnet run -c Release -f net10.0 -- search
dotnet run -c Release -f net10.0 -- --filter *
dotnet run -c Release -f net10.0 -- --filter *CheckedBench*
Аргумент noasm выключает снятие машинного кода.
Причина любого сбоя записывается в Bdn\OverflowProof.log.
Все измеряемые методы лежат в Subjects.cs, у каждого NoInlining. Без
этого компилятор посчитал бы результат при сборке: переполнение на константах
он вычисляет сам и до выполнения дело не доходит. По той же причине числа
берутся из массива, а не пишутся в коде.
Набор для сложения подобран так, чтобы сумма в int помещалась. Иначе
вариант с проверкой бросал бы исключение, а вариант без проверки — нет,
и сравнивалась бы не цена проверки, а цена обработки ошибки.
В замере модуля нет самого маленького числа: там сравнивается цена, а не
поведение на границе. Поведение на границе проверяет отчёт numbers.
Двоичный поиск идёт по воображаемому массиву, где значение элемента равно его номеру: массива на два миллиарда элементов в памяти нет, а проверяется только арифметика границ. Отрицательный номер вместо чтения памяти бросает исключение.
| Что проверялось | Чем |
|---|---|
| Смена знака не меняет самое маленькое число | отчёт checks сравнивает -x с x для int и long |
| Модуль такого числа не существует | отчёт checks ловит исключение у библиотечного модуля |
| Свой модуль возвращает то же число | отчёт checks сравнивает результат с исходным |
| Деление на минус единицу бросает исключение | отчёт checks проверяет и деление, и остаток |
| Переполнение при сложении проходит молча | отчёт checks сравнивает поведение с проверкой и без |
| Три способа середины расходятся только на больших границах | отчёт checks и отчёт search на шести отрезках |
| Все три сложения дают одну сумму | отчёт checks на наборе без переполнения |
| Поведение одинаково у всех знаковых типов | отчёт numbers: sbyte, short, int, long, nint |
| Дело не в конкретной машине | четыре машины, выгрузки всех четырёх в Results |
| Дело не в конкретной версии | .NET 8, 9 и 10 в одном запуске |
Все замеры сняты на x64 под Windows.
Дизассемблер на Linux требует установленного perf. Выключается аргументом
noasm.
Bdn\results\ отчёты BenchmarkDotNet: csv, md, html, машинный код
Bdn\OverflowProof.log журнал прогона: причины сбоев только здесь
Results\Comp_N\
checks_netN.0.txt сверка, код возврата
checks_netN.0_servergc.txt то же на серверном сборщике
numbers_netN.0.txt границы диапазонов по всем знаковым типам
search_netN.0.txt середина отрезка и двоичный поиск
probe_noasm.txt проба на одном классе без дизассемблера
probe_asm.txt та же проба с дизассемблером
bench\ отчёты BenchmarkDotNet
OverflowProof.csproj многоцелевой: net8.0, net9.0, net10.0
OverflowProof.slnx
Program.cs точка входа, разбор аргументов
Subjects.cs все измеряемые методы
README.md
all.bat весь прогон одной командой
Benchmarks\
CheckedBench.cs раздел 1, цена проверки переполнения
MidpointBench.cs раздел 2, середина отрезка
AbsBench.cs раздел 3, модуль числа
Types\
BenchmarkConfig.cs три рантайма, дизассемблер, колонки и журнал
Payloads.cs наборы чисел и границы отрезков
Diagnostics\
Checks.cs сверка, код возврата
Numbers.cs границы диапазонов
Search.cs двоичный поиск
Results\
Comp_1 .. Comp_4\ выгрузки прогона, по папке на машину
- Math.Abs: проверка после смены знака
- Середина через разность в ArraySortHelper
- Середина через беззнаковое сложение в SpanHelpers
- Рост списка с оглядкой на переполнение
- Оператор checked и unchecked
- Math.Abs
Числа в этом файле берутся только из отчётов.
Отчёты снимаются на x64. На другой разрядности nint займёт четыре байта,
и его строка в отчёте numbers будет другой.
Ссылки на исходники .NET даны на тег v10.0.0, коммит зафиксирован: main
уедет, и номера строк перестанут совпадать.