GLM 会自动使用 SIMD 吗? (以及关于 glm 性能的问题)

Does GLM use SIMD automatically? (and a question about glm performance)

我想检查 glm 是否在我的机器上使用 SIMD。 CPU:第 4 代 i5,OS:ArchLinux(最新),IDE:QtCreator。

我写了一个小应用程序来测试它:

#include <iostream>
#include <chrono>
//#define GLM_FORCE_SSE2
//#define GLM_FORCE_ALIGNED
#include <glm/glm.hpp>
#include <xmmintrin.h>
float glm_dot(const glm::vec4& v1, const glm::vec4& v2)
{
   auto start = std::chrono::steady_clock::now();
   auto res = glm::dot(v1, v2);
   auto end = std::chrono::steady_clock::now();
   std::cout << "glm_dot:\t\t" << res << " elasped time: " <<    std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << std::endl;
   return res;
}

float dot_pure(const glm::vec4& v1, const glm::vec4& v2)
{
   auto start = std::chrono::steady_clock::now();
   auto res = v1[0] * v2[0] + v1[1] * v2[1] + v1[2] * v2[2];
   auto end = std::chrono::steady_clock::now();
   std::cout << "dot_pure:\t\t" << res << " elasped time: " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << std::endl;
   return res;
}

float dot_simd(const float& v1, const float& v2)
{
   auto start = std::chrono::steady_clock::now();
   const __m128& v1m = reinterpret_cast<const __m128&>(v1);
   const __m128& v2m = reinterpret_cast<const __m128&>(v2);
   __m128 mul =  _mm_mul_ps(v1m, v2m);
   auto res = mul[0] + mul[1] + mul[2];
   auto end = std::chrono::steady_clock::now();
   std::cout << "dot_simd:\t\t" << res << " elasped time: " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << std::endl;
   return res;
}

float dot_simd_glm_type(const glm::vec4& v1, const glm::vec4& v2)
{
   auto start = std::chrono::steady_clock::now();
   const __m128& v1m = reinterpret_cast<const __m128&>(v1);
   const __m128& v2m = reinterpret_cast<const __m128&>(v2);
   __m128 mul =  _mm_mul_ps(v1m, v2m);
   auto res = mul[0] + mul[1] + mul[2];
   auto end = std::chrono::steady_clock::now();
   std::cout << "dot_simd_glm_type:\t" << res << " elasped time: " << std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count() << std::endl;
   return res;
}

int main()
{
   glm::vec4 v1 = {1.1f, 2.2f, 3.3f, 0.0f};
   glm::vec4 v2 = {3.0f, 4.0f, 5.0f, 0.0f};
   float v1_raw[] = {1.1f, 2.2f, 3.3f, 0.0f};
   float v2_raw[] = {3.0f, 4.0f, 5.0f, 0.0f};
   glm_dot(v1, v2);
   dot_pure(v1, v2);
   dot_simd(*v1_raw, *v2_raw);
   dot_simd_glm_type(v1, v2);
   return 0;
}

glm_dot() 调用glm::dot,其他函数是我的实现。当我 运行 它处于调试模式时,典型的结果是:

glm_dot:        28.6 elasped time: 487
dot_pure:       28.6 elasped time: 278
dot_simd:       28.6 elasped time: 57
dot_simd_glm_type:  28.6 elasped time: 52

glm::dot 从 func_geometric.inl 调用 compute_dot::call,这是点函数的“纯”实现。我不明白为什么 glm::dot(通常)比我的 dot_pure() 实现花费更多的时间,但它是调试模式所以,让我们继续发布:

glm_dot:        28.6 elasped time: 116
dot_pure:       28.6 elasped time: 53
dot_simd:       28.6 elasped time: 54
dot_simd_glm_type:28.6 elasped time: 54

并非总是如此,但通常我的纯实现比 simd 版本花费的时间更少。也许这是因为编译器也可以在我的纯实现中使用 simd,我不知道。

  1. 然而,通常 glm::dot 调用比其他三个实现慢得多。为什么?也许 glm 这次也使用纯实现?当我使用 ReleaseWithDebugInfos 时,情况似乎就是这样。

如果我注释掉源代码中的两个定义(强制使用 simd),我会得到更好的结果,但通常 glm::dot 调用仍然较慢。 (在ReleaseWithDebugInfos中调试,这次没有任何显示)

glm_dot:        28.6 elasped time: 88
dot_pure:       28.6 elasped time: 63
dot_simd:       28.6 elasped time: 53
dot_simd_glm_type:28.6 elasped time: 53
  1. glm 不应该尽可能默认使用 simd 吗? 但是根据文档,它可能根本不是自动的: GLM 提供了一些基于编译器内在函数的 SIMD 优化。由于编译器参数,这些优化将自动进行。例如,如果程序使用 /arch:AVX 以 Visual Studio 编译,GLM 将检测此参数并在可用时自动使用 AVX 指令生成代码。 (来源:https://chromium.googlesource.com/external/github.com/g-truc/glm/+/0.9.9-a2/manual.md

  2. 有一个名为 test-core_setup_message 的 glm 测试,如果我 运行 它,glm 似乎没有检测到我的拱门(这意味着 SSE、SSE2 等):

$ ./test-core_setup_message
__cplusplus: 201703
GCC 8
GLM_MODEL_64
GLM_ARCH: 

总结一下我的问题,glm 是否自动使用 simd 指令?文档的某些部分说它是自动的,其他一些说它取决于编译器标志。当我强制使用 SSE2 时,为什么它仍然比我的 simd 调用慢?

If I comment out the two defines in the source code (to force using simd) than I got better results, but usually the the glm::dot call is still slower. (To debug in ReleaseWithDebugInfos doesn’t show anything this time)

你的测试不是很严格,很容易运行进入内存缓存工件。

举个例子,只是改变我得到的测试顺序:(使用 -O3 -march=x86-64 -mavx2 编译并且你的定义未设置):

dot_simd:       28.6 elasped time: 170
dot_pure:       28.6 elasped time: 54
dot_simd_glm_type:  28.6 elasped time: 46
glm_dot:        28.6 elasped time: 47

您需要 运行 使用基准库进行此类测试,例如 Google Benchmark

但即便如此。 “运行得更快”只是“使用 SIMD”的粗略代理测试。实际查看生成的程序集会更好。

我从你的例子中删除了时间代码,得到了以下See on godbolt

glm_dot(glm::vec<4, float, (glm::qualifier)0> const&, glm::vec<4, float, (glm::qualifier)0> const&):
        vmovss  xmm0, DWORD PTR [rdi+4]
        vmovss  xmm1, DWORD PTR [rdi]
        vmulss  xmm0, xmm0, DWORD PTR [rsi+4]
        vmovss  xmm2, DWORD PTR [rdi+8]
        vmulss  xmm1, xmm1, DWORD PTR [rsi]
        vmulss  xmm2, xmm2, DWORD PTR [rsi+8]
        vaddss  xmm0, xmm0, xmm1
        vmovss  xmm1, DWORD PTR [rdi+12]
        vmulss  xmm1, xmm1, DWORD PTR [rsi+12]
        vaddss  xmm1, xmm1, xmm2
        vaddss  xmm0, xmm0, xmm1
        ret
dot_simd(float const&, float const&):
        vmovaps xmm1, XMMWORD PTR [rsi]
        vmulps  xmm1, xmm1, XMMWORD PTR [rdi]
        vshufps xmm2, xmm1, xmm1, 85
        vaddss  xmm0, xmm1, xmm2
        vunpckhps       xmm1, xmm1, xmm1
        vaddss  xmm0, xmm0, xmm1
        ret

所以您是正确的,默认情况下显然不使用 SIMD。