Код к статье про суммирование массива в .NET: свой цикл против LINQ Sum(). Контринтуитив: начиная с .NET 8 Sum() на int и long векторизован (PR #84519), а обычный for — скаляр: RyuJIT произвольный цикл не векторизует, запрос на автовекторизацию открыт с 2019 (issue #12466). То есть LINQ здесь быстрее цикла. Бонусом: у векторного Sum() другой порядок сложения — официально задокументированный breaking change по OverflowException, и отдельная история про AVX-512, который рантайм намеренно не даёт части процессоров.
Статья на Хабре: Бенчмаркая Sum: ускорил циклом — замедлил в ×4,7
- Реалистичные данные: случайные значения 0..15 с фиксированным seed, а не массив из одного повторяющегося числа. Диапазон узкий сознательно: на самой длинной выборке (4 194 304 элемента) сумма обязана влезать в int с запасом — иначе checked-методы лягут.
- Сравнение на равных условиях:
Sum()гарантируетOverflowException, обычный цикл — нет. Поэтому в замерах есть иSum_ForChecked— цикл с той же гарантией. - Несколько длин: 32 / 512 / 65 536 / 4 194 304. Последняя — 16 МБ данных, заведомо мимо кэша.
- Assert совпадения:
GlobalSetupпроверяет, что все методы возвращают одинаковую сумму, иначе падает. Float сверяется с допуском: порядок сложения и double-аккумулятор BCL дают дрейф в младших битах. - Границы названы:
Sum_LinqIterator(yield-обёртка над тем же массивом) в наборе, а не спрятан — тамSum()медленный, и это показано.
| Бенчмарк | Что | Итог |
|---|---|---|
Sum_Linq (baseline) |
values.Sum() |
векторный (Vector<T>) плюс контроль переполнения |
Sum_For |
свой for, unchecked |
скаляр, до ×4,67 медленнее |
Sum_ForChecked |
свой for + checked |
скаляр плюс jo, до ×4,84 |
Sum_ForUnrolled |
развёрнутый ×4 | всё равно скаляр; выигрывает у Sum() только на 32 элементах |
Sum_Foreach |
foreach по массиву |
тот же скаляр |
Sum_LinqInterface |
Sum() через параметр IEnumerable<int> |
вровень с прямым вызовом: проверка типа у Sum рантаймная |
Sum_LinqIterator |
Sum() по yield-итератору |
до ×32 плюс 48 байт аллокаций на вызов |
Sum_ForList / Sum_LinqList |
List<int>: индексатор против Sum() |
Sum быстрее и на списке |
Sum_Vector256 |
свой ymm без контроля переполнения | быстрее BCL в 1,3–1,9 раза — без гарантии исключения |
Sum_Tensor |
TensorPrimitives.Sum |
векторный и unchecked: переполнение заворачивается |
SumFloat_For / SumFloat_Linq |
контраст: float | Sum() на float не векторизован — порядок сложения менять нельзя |
Отдельный класс LinqSumWidthBench — ширина Vector<T>: дефолт против DOTNET_MaxVectorTBitWidth=512 против пары с DOTNET_PreferredVectorBitWidth=512. На Cascade Lake последняя снимает запрет рантайма и даёт zmm: минус 27% времени Sum().
Disasm.exe overflow
128 пар (int.MaxValue, int.MinValue): последовательная checked-сумма держится около нуля и возвращает −128, векторный Sum() раскладывает элементы по позициям вектора — все MaxValue попадают в одни и те же позиции, и вторая итерация складывает MaxValue с MaxValue: OverflowException. С DOTNET_EnableHWIntrinsic=0 исключения нет. Пруфы: breaking change .NET 8, issue #92230.
- Векторизация
Sum()— PR dotnet/runtime #84519 (brantburnett, ревью tannergooding). Sum()остаётся checked в векторном виде: контроль переполнения по знаковым битам, проверка в конце каждой итерации развёрнутого цикла — Sum.cs, SumSignedIntegersVectorized. Порог векторизации там же:span.Length >= Vector<T>.Count * 4.- RyuJIT не векторизует произвольный цикл — issue #12466.
- Порядок сложения и
OverflowException— breaking change .NET 8, issue #92230. - CPUID-проверка на снижение частоты (Skylake Server / Cascade Lake / Cooper Lake), флаг
CORJIT_FLAG_VECTOR512_THROTTLING— issue #88233, PR #86655, разбор в Hardware Intrinsics in .NET 8 со ссылкой на Intel Optimization Reference Manual, раздел 2.5.3. - Шириной Vector<T> управляет
DOTNET_MaxVectorTBitWidth— issue #104978; поддержку 512-битного Vector<T> дорабатывали в PR #111472. TensorPrimitives.Sumбез checked-семантики — исходник.
Бенчмарки (гоняются .NET 8 / 9 / 10; хостом обязательно net10):
dotnet run -c Release -f net10.0 # полный прогон
dotnet run -c Release -f net10.0 -- width # только LinqSumWidthBench, минуты
Результаты — в BenchmarkDotNet.Artifacts/results: таблицы *-report-github.md, csv, сводка _RECAP.txt, графики _CHARTS.html.
Машинный код:
Disasm\snap.bat # обычный проход + DOTNET_MaxVectorTBitWidth=512 (*_vt512.txt)
Disasm\snap_pref512.bat # + DOTNET_PreferredVectorBitWidth=512 (*_pref512.txt) — снятие запрета на Cascade Lake
Disasm/snap.sh # Linux-вариант первого
В начале каждого файла — сводка железа: Avx2/Avx512, фактическая ширина Vector<T>.
Subjects.cs— все методы, имена совпадают со статьёй один в один.Benchmarks/—LinqSumBench(основной набор) иLinqSumWidthBench(ширина Vector<T>, три джоба).Disasm/— снятие листингов, демо переполнения, готовые листинги четырёх машин вDisasm/Listings_Comp_1..4/.Results/Comp_1..4/— готовые прогоны каждой машины (основной набор +Width512/с тремя джобами ширины); машины те же, что в таблице статьи: №1 Ryzen 9 5950X, №2 i9-10900KF, №3 — 2 × Xeon Silver 4314, №4 — Xeon W-2255.Results/Docs/— картинки из статьи.Reporting/— сводка и графики из прогонов.





