【问题标题】:why is std::equal much slower than a hand rolled loop for two small std::array?为什么 std::equal 比两个小的 std::array 的手动循环慢得多?
【发布时间】:2017-01-08 19:18:57
【问题描述】:

我正在分析一小段代码,它是更大模拟的一部分,令我惊讶的是,STL 函数 equal (std::equal) 比简单的 for 循环慢得多,比较两个数组元素元素。我写了一个小测试用例,我认为这是两者之间的公平比较,并且使用 Debian 档案中的 g++ 6.1.1 的差异并非微不足道。我正在比较两个有符号整数的四元素数组。我测试了 std::equal、operator== 和一个小的 for 循环。我没有使用 std::chrono 来确定确切的时间,但是可以通过 time ./a.out 明确地看到差异。

我的问题是,鉴于下面的示例代码,为什么 operator== 和重载函数 std::equal(我相信它调用 operator==)需要大约 40 秒才能完成,而手写循环只需要 8 秒?我正在使用最近的基于英特尔的笔记本电脑。 for 循环在所有优化级别(-O1、-O2、-O3 和 -Ofast)上都更快。我编译了代码 g++ -std=c++14 -Ofast -march=native -mtune=native

Run the code

循环运行了很多次,只是为了让肉眼清楚地看到差异。模运算符表示对数组元素之一的廉价操作,用于防止编译器在循环外进行优化。

#include<iostream>
#include<algorithm>
#include<array>

using namespace std;
using T = array<int32_t, 4>;

bool 
are_equal_manual(const T& L, const T& R)
noexcept {
    bool test{ true };
    for(uint32_t i{0}; i < 4; ++i) { test = test && (L[i] == R[i]); }
    return test;
}

bool
are_equal_alg(const T& L, const T& R)
noexcept {
    bool test{ equal(cbegin(L),cend(L),cbegin(R)) };
    return test;
}

int main(int argc, char** argv) {

    T left{ {0,1,2,3} };
    T right{ {0,1,2,3} };

    cout << boolalpha << are_equal_manual(left,right) << endl;
    cout << boolalpha << are_equal_alg(left,right) << endl;
    cout << boolalpha << (left == right) << endl;

    bool t{};
    const size_t N{ 5000000000 };
    for(size_t i{}; i < N; ++i) {
      //t = left == right; // SLOW
      //t = are_equal_manual(left,right); // FAST
        t = are_equal_alg(left,right);  // SLOW
      left[0] = i % 10;
      right[2] = i % 8;
    }

    cout<< boolalpha << t << endl;

    return(EXIT_SUCCESS);
}

【问题讨论】:

  • Assembly output,手动比较是展开 cmpl,而 equal== 使用的是 memcmp
  • 如果你传递你自己的 lambda bool test{ equal(cbegin(L),cend(L),cbegin(R), [](int a, int b) { return a == b; })}; 编译器似乎可以更好地优化它。
  • Jesse - 在这种情况下必须传递一个 lambda 对我来说似乎有点多余;这不是 operator== 应该做的,因为它们是基本类型吗?
  • libstdc++ 的 equal dispatches to memcmp when it's safe。这是一种优化,但在这种情况下,这可能不是正确的做法。
  • @KBentley57:是的,你是对的。正如 T.C. 所指出的,这是 libstdc++ 库中的一个实现问题

标签: c++ performance stl c++14 gcc6


【解决方案1】:

这是使用are_equal_manual(left,right) 函数时main()for 循环的生成程序集:

.L21:
        xor     esi, esi
        test    eax, eax
        jne     .L20
        cmp     edx, 2
        sete    sil
.L20:
        mov     rax, rcx
        movzx   esi, sil
        mul     r8
        shr     rdx, 3
        lea     rax, [rdx+rdx*4]
        mov     edx, ecx
        add     rax, rax
        sub     edx, eax
        mov     eax, edx
        mov     edx, ecx
        add     rcx, 1
        and     edx, 7
        cmp     rcx, rdi

下面是使用are_equal_alg(left,right) 函数时生成的内容:

.L20:
        lea     rsi, [rsp+16]
        mov     edx, 16
        mov     rdi, rsp
        call    memcmp
        mov     ecx, eax
        mov     rax, rbx
        mov     rdi, rbx
        mul     r12
        shr     rdx, 3
        lea     rax, [rdx+rdx*4]
        add     rax, rax
        sub     rdi, rax
        mov     eax, ebx
        add     rbx, 1
        and     eax, 7
        cmp     rbx, rbp
        mov     DWORD PTR [rsp], edi
        mov     DWORD PTR [rsp+24], eax
        jne     .L20

我不确定在第一种情况下生成的代码中发生了什么,但它显然没有调用memcmp()。它似乎根本没有比较数组的内容。虽然循环仍在迭代 5000000000 次,但它被优化为什么都不做。但是,使用are_equal_alg(left,right) 的循环仍在执行比较。基本上,编译器仍然能够比std::equal模板更好地优化手动比较。

【讨论】:

  • Michael - 我使用模运算符来模拟对每个循环迭代的元素之一本质上是 rand() 的调用。如果模数不足以阻止编译器优化某些内容,我可以尝试编辑并发布更真实的版本。
  • @KBentley57:循环仍在迭代 - 我认为问题出在 Jesse Good 并且您注意到了:std::equal&lt;&gt;() 似乎并没有直接比较基本类型,而它可能应该这样做。我还没有完成追踪 gcc 的std::equal 实现的工作,但在这种情况下,它似乎依赖于memcmp(),因为可能有一种方法可以避免数组(或向量/其他容器?)您在上面的评论中提到的基本类型。
  • 我相信 T.C.发布了实现,它似乎默认为 memcmp。您认为这是值得报告的错误,还是只是基本类型的方式?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-19
  • 2015-04-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-22
相关资源
最近更新 更多