IT Share you

C ++ 컴파일러가 더 나은 상수 폴딩을 수행하지 않는 이유는 무엇입니까?

shareyou 2020. 12. 7. 21:09
반응형

C ++ 컴파일러가 더 나은 상수 폴딩을 수행하지 않는 이유는 무엇입니까?


jacobians를 계산하기위한 자동 파생 기능이있는 C ++ 코드의 많은 부분의 속도를 높이는 방법을 조사 중입니다. 여기에는 실제 잔차에서 일정량의 작업을 수행하는 것이 포함되지만 대부분의 작업 (프로파일 된 실행 시간 기준)은 jacobians를 계산하는 것입니다.

대부분의 jacobians가 0과 1에서 앞으로 전파되므로 작업량은 10-12x가 아니라 함수의 2-4x 여야합니다. 많은 양의 야 코비안 작업이 어떤 것인지 모델링하기 위해 컴파일러가 할 수 있어야하는 내적 (실제 상황에있는 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 모두 0으로 가득 찬 행렬을 사용하여 순진한 내적에 대해 최적화를 수행 할 수 없다는 사실에 실망했습니다. 빠른 수학 ( https://godbolt.org/z/GvPXFy )을 사용 하더라도 GCC 및 Clang (여전히 곱셈과 덧셈 포함)에서 최적화가 매우 좋지 않으며 MSVC는 최적화를 전혀 수행하지 않습니다.

컴파일러에 대한 배경 지식이 없지만 그 이유가 있습니까? 나는 많은 비율의 과학적 계산에서 더 나은 상수 전파 / 접기를 할 수 있다는 것이 상수 접기 자체가 속도 향상을 가져 오지 않더라도 더 많은 최적화를 명백하게 할 것이라고 확신합니다.

왜 이것이 컴파일러 측에서 수행되지 않는지에 대한 설명에 관심이 있지만, 이러한 유형의 패턴에 직면했을 때 내 코드를 더 빠르게 만들기 위해 실용적인 측면에서 무엇을 할 수 있는지도 관심이 있습니다.


이는 Eigen이 코드를 나머지 4 개의 구성 요소 레지스터 내에서 3 개의 vmulpd, 2 개의 vaddpd 및 1 개의 수평 축소로 명시 적으로 벡터화하기 때문입니다 (AVX를 가정하고 SSE 만 사용하면 6 개의 mulpd 및 5 개의 addpd를 얻을 수 있음). -ffast-mathGCC와 clang을 사용하면 마지막 2 개의 vmulpd 및 vaddpd를 제거 할 수 있지만 (그리고 이것이 수행하는 작업입니다) Eigen에서 명시 적으로 생성 한 나머지 vmulpd 및 수평 축소를 실제로 대체 할 수는 없습니다.

그래서 정의하여 Eigen의 명시 적 벡터화를 비활성화하면 어떻게 EIGEN_DONT_VECTORIZE될까요? 그런 다음 예상 한 것을 얻지 ( https://godbolt.org/z/UQsoeH ) 다른 코드 부분이 훨씬 느려질 수 있습니다.

명시 적 벡터화를 로컬에서 비활성화하고 Eigen의 내부를 엉망으로 만드는 것을 두려워하지 않는 경우 다음 유형 을 전문화 하여 벡터화 DontVectorize옵션을 도입 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 모두 0으로 가득 찬 행렬을 사용하여 순진한 내적에 대해 최적화를 수행 할 수 없다는 사실에 실망했습니다.

They have no other choice unfortunately. Since IEEE floats have signed zeros, adding 0.0 is not an identity operation:

-0.0 + 0.0 = 0.0 // Not -0.0!

Similarly, multiplying by zero does not always yield zero:

0.0 * Infinity = NaN // Not 0.0!

So the compilers simply cannot perform these constant folds in the dot product while retaining IEEE float compliance - for all they know, your input might contain signed zeros and/or infinities.

You will have to use -ffast-math to get these folds, but that may have undesired consequences. You can get more fine-grained control with specific flags (from http://gcc.gnu.org/wiki/FloatingPointMath). According to the above explanation, adding the following two flags should allow the constant folding:
-ffinite-math-only, -fno-signed-zeros

Indeed, you get the same assembly as with -ffast-math this way: https://godbolt.org/z/vGULLA. You only give up the signed zeros (probably irrelevant), NaNs and the infinities. Presumably, if you were to still produce them in your code, you would get undefined behavior, so weigh your options.


As for why your example is not optimized better even with -ffast-math: That is on Eigen. Presumably they have vectorization on their matrix operations, which are much harder for compilers to see through. A simple loop is properly optimized with these options: https://godbolt.org/z/OppEhY


One way to force a compiler to optimize multiplications by 0's and 1`s is to manually unroll the loop. For simplicity let's use

#include <array>
#include <cstddef>
constexpr std::size_t n = 12;
using Array = std::array<double, n>;

Then we can implement a simple dot function using fold expressions (or recursion if they are not available):

<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>{});
}

Now let's take a look at your function

double test(const Array& b)
{
    const Array a{1};    // = {1, 0, ...}
    return dot(a, b);
}

With -ffast-math gcc 8.2 produces:

test(std::array<double, 12ul> const&):
  movsd xmm0, QWORD PTR [rdi]
  ret

clang 6.0.0 goes along the same lines:

test(std::array<double, 12ul> const&): # @test(std::array<double, 12ul> const&)
  movsd xmm0, qword ptr [rdi] # xmm0 = mem[0],zero
  ret

For example, for

double test(const Array& b)
{
    const Array a{1, 1};    // = {1, 1, 0...}
    return dot(a, b);
}

we get

test(std::array<double, 12ul> const&):
  movsd xmm0, QWORD PTR [rdi]
  addsd xmm0, QWORD PTR [rdi+8]
  ret

Addition. Clang unrolls a for (std::size_t i = 0; i < n; ++i) ... loop without all these fold expressions tricks, gcc doesn't and needs some help.

참고URL : https://stackoverflow.com/questions/52113522/why-dont-c-compilers-do-better-constant-folding

반응형