Почему компиляторы C++ не делают лучше постоянное сворачивание?
Я исследую способы ускорения большого раздела кода C++, который имеет автоматические производные для вычисления якобианов. Это включает в себя выполнение некоторого объема работы в фактических остатках, но большая часть работы (на основе профилированного времени выполнения) заключается в вычислении якобианов.
это удивило меня, поскольку большинство из якобианов распространяются вперед от 0s и 1S, поэтому объем работы должен быть 2-4х функция, а не 10-12х. Для того, чтобы смоделировать большой количество якобианской работы похоже на то, что я сделал супер минимальный пример только с точечным продуктом (вместо sin, cos, sqrt и т. д., которые были бы в реальной ситуации), что компилятор должен иметь возможность оптимизировать до одного возвращаемого значения:
#include <Eigen/Core>
#include <Eigen/Geometry>
using Array12d = Eigen::Matrix<double,12,1>;
double testReturnFirstDot(const Array12d& b)
{
Array12d a;
a.array() = 0.;
a(0) = 1.;
return a.dot(b);
}
, который должен быть таким же, как
double testReturnFirst(const Array12d& b)
{
return b(0);
}
Я был разочарован, обнаружив, что без поддержки быстрой математики ни GCC 8.2, Clang 6 или MSVC 19 не смогли сделать никаких оптимизаций на всем протяжении наивного точечного продукта с матрицей полный 0s. даже с быстрой математикой (https://godbolt.org/z/GvPXFy) оптимизации очень плохи в GCC и Clang (все еще включают умножения и дополнения), а MSVC вообще не делает никаких оптимизаций.
у меня нет фона в компиляторах, но есть причина для этого? Я довольно уверен, что в значительной части научных вычислений, способных сделать лучшее постоянное распространение / свертывание, будет больше оптимизаций, даже если постоянная складка сама по себе не привела к ускорению.
хотя меня интересуют объяснения, почему это не делается на стороне компилятора, мне также интересно, что я могу сделать на практической стороне, чтобы сделать свой собственный код быстрее, когда сталкиваюсь с такими шаблонами.
3 ответа:
это связано с тем, что Eigen явно векторизует ваш код как 3 vmulpd, 2 vaddpd и 1 горизонтальное сокращение в оставшихся 4 компонентных регистрах (это предполагает AVX, с SSE только вы получите 6 mulpd и 5 addpd). С
-ffast-math
GCC и clang могут удалить последние 2 vmulpd и vaddpd (и это то, что они делают), но они не могут действительно заменить оставшиеся vmulpd и горизонтальное сокращение, которые были явно сгенерированы Eigen.Ну и что, если вы отключите Eigen явная векторизация путем определения
EIGEN_DONT_VECTORIZE
? Тогда вы получите то, что ожидали (https://godbolt.org/z/UQsoeH) но другие части кода могут стать намного медленнее.если вы хотите локально отключить явную векторизацию и не боитесь возиться с внутренним Eigen, вы можете ввести до
Matrix
и отключить векторизацию, специализируясьtraits<>
для этогоMatrix
тип:static const int DontVectorize = 0x80000000; namespace Eigen { namespace internal { template<typename _Scalar, int _Rows, int _Cols, int _MaxRows, int _MaxCols> struct traits<Matrix<_Scalar, _Rows, _Cols, DontVectorize, _MaxRows, _MaxCols> > : traits<Matrix<_Scalar, _Rows, _Cols> > { typedef traits<Matrix<_Scalar, _Rows, _Cols> > Base; enum { EvaluatorFlags = Base::EvaluatorFlags & ~PacketAccessBit }; }; } } using ArrayS12d = Eigen::Matrix<double,12,1,DontVectorize>;
полный пример есть: https://godbolt.org/z/bOEyzv
Я был разочарован, обнаружив, что без поддержки быстрой математики ни GCC 8.2, Clang 6 или MSVC 19 не смогли сделать никаких оптимизаций на всем протяжении наивного точечного продукта с матрицей, полной 0s.
у них нет другого выбора, к сожалению. Так как IEEE плавает подписали нули, добавив
0.0
не является операцией идентификации:-0.0 + 0.0 = 0.0 // Not -0.0!
точно так же умножение на ноль не всегда дает ноль:
0.0 * Infinity = NaN // Not 0.0!
Итак, компиляторы просто невозможно выполнить эти постоянные сгибы в точечном продукте, сохраняя соответствие IEEE float-для всего, что они знают, ваш вход может содержать подписанные нули и/или бесконечности.
вам придется использовать
-ffast-math
чтобы сделать эти складки, но это может иметь нежелательные последствия. Вы можете получить более мелкозернистый контроль с определенными флагами (от http://gcc.gnu.org/wiki/FloatingPointMath). согласно приведенному выше объяснению, добавление следующих двух флагов должно позволить постоянный адрес статьи:--21--> -ffinite-math-only
,-fno-signed-zeros
действительно, Вы получаете ту же сборку, что и с
-ffast-math
таким образом:https://godbolt.org/z/vGULLA вы отказываетесь только от подписанных нулей (вероятно, не имеющих отношения к делу), NaNs и бесконечностей. Предположительно, если бы вы все еще производили их в своем коде, вы получили бы неопределенное поведение, поэтому взвесьте свои варианты.
а почему ваш пример не оптимизирован лучше даже с
-ffast-math
: то есть на Eigen. Предположительно они имеют векторизацию на своих матричных операциях,которые намного сложнее для компиляторов видеть. Простой цикл правильно оптимизирован с помощью следующих параметров:https://godbolt.org/z/OppEhY
один из способов заставить компилятор оптимизировать умножение на 0 и 1 - это вручную развернуть цикл. Для простоты давайте использовать
#include <array> #include <cstddef> constexpr std::size_t n = 12; using Array = std::array<double, n>;
тогда мы можем реализовать простой
dot
функция с использованием выражений сгиба (или рекурсии, если они недоступны):<utility> template<std::size_t... is> double dot(const Array& x, const Array& y, std::index_sequence<is...>) { return ((x[is] * y[is]) + ...); } double dot(const Array& x, const Array& y) { return dot(x, y, std::make_index_sequence<n>{}); }
теперь давайте посмотрим на вашу функцию
double test(const Array& b) { const Array a{1}; // = {1, 0, ...} return dot(a, b); }
С
-ffast-math
gcc 8.2 производит:test(std::array<double, 12ul> const&): movsd xmm0, QWORD PTR [rdi] ret
clang 6.0.0 идет по тому же пути строки:
test(std::array<double, 12ul> const&): # @test(std::array<double, 12ul> const&) movsd xmm0, qword ptr [rdi] # xmm0 = mem[0],zero ret
например,
double test(const Array& b) { const Array a{1, 1}; // = {1, 1, 0...} return dot(a, b); }
мы
test(std::array<double, 12ul> const&): movsd xmm0, QWORD PTR [rdi] addsd xmm0, QWORD PTR [rdi+8] ret
дополнительно. лязг разворачивает a
for (std::size_t i = 0; i < n; ++i) ...
цикл без всех этих трюков с выражениями сгиба, gcc не делает и нуждается в некоторой помощи.