Skip to content

Repository files navigation

LinqSumProof

Код к статье про суммирование массива в .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().

Графики из статьи

Свой цикл против Sum()

Все методы, машина №3

Sum() по рантаймам

Sum() по yield-итератору

Разрыв по длинам

Ширина Vector<T> на Cascade Lake

Демо порядка сложения

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.
  • Порядок сложения и OverflowExceptionbreaking 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/ — сводка и графики из прогонов.

About

LINQ Sum() beats a hand-written loop: benchmarks on .NET 8/9/10, disasm proofs, overflow breaking change, and the AVX-512 path .NET hides on Cascade Lake

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages