Допустим, сумма элементов типа double через std::reduce иногда отличается от суммы через std::accumulate. Какой механизм STL допускает такое расхождение?
std::reduce может менять порядок и группировку применения бинарной операции, а std::accumulate обрабатывает элементы последовательно слева направо. Для сложения double это важно, потому что арифметика с плавающей точкой неассоциативна: разные группировки могут давать разные результаты.
std::accumulate появился как последовательное свёртывание диапазона с накопителем. Его семантика сохраняет порядок обработки элементов, что удобно для операций, чувствительных к последовательности.
std::reduce был добавлен в C++17 вместе с алгоритмами, рассчитанными на выполнение с политиками параллелизма. Чтобы разбивать диапазон на части, обрабатывать их независимо и затем объединять результаты, алгоритму необходимо разрешить перегруппировку операций.
Если результат зависит от точного порядка вычислений, механическая замена std::accumulate на std::reduce может изменить поведение программы. Особенно заметно это при суммировании double, работе с операциями вычитания, конкатенацией или другими неассоциативными операциями.
Например, математически равенства (a + b) + c и a + (b + c) выполняются не всегда: промежуточное округление в арифметике с плавающей точкой может дать разные значения.
std::accumulate последовательно обновляет накопитель: результат предыдущего шага используется на следующем шаге. Поэтому для диапазона a, b, c вычисление концептуально имеет вид ((init + a) + b) + c.
std::reduce допускает разделение диапазона на поддиапазоны, отдельное свёртывание этих частей и последующее объединение частичных результатов. Конкретный порядок и группировка не должны использоваться как часть логики программы.
Оба алгоритма имеют линейное число операций над элементами, но reduce предоставляет реализации больше свободы для распараллеливания и векторизации. Для целочисленного сложения без переполнения результат обычно не зависит от группировки, поскольку операция ассоциативна в пределах допустимого диапазона типа.
Если важна воспроизводимость вычислений с плавающей точкой, следует использовать std::accumulate с фиксированным порядком либо специально спроектированную схему редукции с фиксированным деревом объединения. Для финансовых расчётов часто применяют целые единицы минимального номинала, например копейки, если диапазон значений это позволяет.
В сервисе аналитики результаты пакетной обработки суммировались через std::accumulate. После оптимизации разработчик заменил его на std::reduce, рассчитывая получить параллелизм, но контрольные суммы для больших наборов чисел с плавающей точкой начали незначительно различаться.
Рассматривались три варианта. Возврат к std::accumulate сохранял воспроизводимость, но ограничивал возможности распараллеливания. Безусловное использование std::reduce ускоряло обработку, но не обеспечивало стабильный результат. Третьим вариантом была фиксированная схема попарного сложения с явно заданным порядком; она давала воспроизводимость между запусками, но требовала дополнительной реализации и тестирования.
Для контрольных отчётов выбрали фиксированную схему редукции, а для некритичных приблизительных метрик оставили std::reduce. Решение разделило требования к точности и производительности вместо того, чтобы считать эти алгоритмы взаимозаменяемыми.
Гарантирует ли std::reduce другой результат при каждом запуске?
Нет. Стандарт допускает изменение порядка и группировки, но конкретная реализация может фактически использовать один и тот же порядок. Однако программа не должна полагаться на это отсутствие расхождений: обновление компилятора, параметров оптимизации или аппаратной платформы может изменить результат.
Можно ли безопасно применять std::reduce для целых чисел?
Да, если операция обладает нужными свойствами и вычисления не приводят к переполнению. Для знаковых целых переполнение является неопределённым поведением, поэтому перегруппировка не устраняет риск. Нужно выбрать подходящий тип накопителя или заранее обеспечить отсутствие выхода за его диапазон.
Есть ли у std::accumulate перегрузка с политикой выполнения, чтобы сохранить порядок и распараллелить вычисление?
У std::accumulate нет перегрузки с execution policy. Его последовательная семантика сохраняет порядок обработки. Для параллельной редукции предназначен std::reduce, но используемая операция должна корректно работать при произвольной группировке, а чувствительным к порядку вычислениям требуется специальный алгоритм.