为什么 std::count 比 MSVC 编译器的普通 for 循环慢,但与 GCC 相等?

Why std::count is slower that plain for loop with MSVC Compiler but equal with GCC?

我正在测试C++标准库算法的性能,遇到了一件奇怪的事情。

这是我的代码,用于比较 std::count 与普通 for 循环的性能:

#include <algorithm>
#include <vector>
#include <iostream>
#include <chrono>
using namespace std::chrono;


int my_count(const std::vector<int>& v, int val) {
    int num = 0;

    for (int i: v) {
        if (i == val)
            num++;
    }

    return num;
}

int main()
{
    int total_count = 0;

    std::vector<int> v;
    v.resize(100000000);

    // Fill vector
    for (int i = 0; i < v.size(); i++) {
        v[i] = i % 10000;
    }

    int val = 1;

    {
        auto start = high_resolution_clock::now();
        total_count += std::count(v.begin(), v.end(), val);
        auto stop = high_resolution_clock::now();

        std::cout << "std::count time:   " << duration_cast<microseconds>(stop - start).count() << std::endl;
    }

    {
        auto start = high_resolution_clock::now();
        total_count += my_count(v, val);
        auto stop = high_resolution_clock::now();

        std::cout << "my_count   time:   " << duration_cast<microseconds>(stop - start).count() << std::endl;
    }

    // We need this so the compiler does not prune the code above
    std::cout << "Total items: " << total_count << std::endl;
}

使用 MinGW 我得到了这个:

std::count time:   65827
my_count   time:   64861

使用 MSVC 我得到了一个非常奇怪的结果:

std::count time:   65532
my_count   time:   28584

MinGW 结果似乎是合理的,因为据我所知,STL 计数函数如果大致等于普通 for 循环,但 MSVC 结果似乎很奇怪 - 为什么普通 for 循环比 [= 快 2 倍以上29=]?

这些结果在我的机器上是可重现的——它不是只发生一次,而是每次我 运行 代码时都会发生。我什至尝试更改函数顺序,运行宁多个 for 循环以避免缓存或分支预测偏差,但我仍然得到相同的结果。

有什么原因吗?

嗯,这似乎是一个迭代器问题。

我做了扩展测试:

#include <algorithm>
#include <vector>
#include <iostream>
#include <chrono>
using namespace std::chrono;


int std_count(const std::vector<int>& v, int val) {
    return std::count(v.begin(), v.end(), val);
}

int my_count_for(const std::vector<int>& v, int val) {
    int num = 0;

    for (int i = 0; i < v.size(); i++) {
        if (v[i] == val) {
            num++;
        }
    }

    return num;
}

int my_count_for_in(const std::vector<int>& v, int val) {
    int num = 0;

    for (int i : v) {
        if (i == val) {
            num++;
        }
    }

    return num;
}

int my_count_iter(const std::vector<int>& v, int val) {
    int num = 0;

    for (auto i = v.begin(); i < v.end(); i++) {
        if (*i == val) {
            num++;
        }
    }

    return num;
}


int main()
{
    std::vector<int> v;
    v.resize(1000000);

    // Fill vector
    for (int i = 0; i < v.size(); i++) {
        v[i] = i % 10000;
    }

    int val = 1;

    int num_iters = 1000;

    int total_count = 0;

    for (int a = 0; a < 3; a++) {
        {
            auto start = high_resolution_clock::now();
            for (int i = 0; i < num_iters; i++) {
                total_count += std_count(v, val);
            }
            auto stop = high_resolution_clock::now();

            auto duration = duration_cast<microseconds>(stop - start);
            std::cout << "std::count time:       " << duration.count() << std::endl;
        }

        {
            auto start = high_resolution_clock::now();
            for (int i = 0; i < num_iters; i++) {
                total_count += my_count_for(v, val);
            }
            auto stop = high_resolution_clock::now();

            auto duration = duration_cast<microseconds>(stop - start);
            std::cout << "my_count_for time:     " << duration.count() << std::endl;
        }

        {
            auto start = high_resolution_clock::now();
            for (int i = 0; i < num_iters; i++) {
                total_count += my_count_for_in(v, val);
            }
            auto stop = high_resolution_clock::now();

            auto duration = duration_cast<microseconds>(stop - start);
            std::cout << "my_count_for_in time:  " << duration.count() << std::endl;
        }

        {
            auto start = high_resolution_clock::now();
            for (int i = 0; i < num_iters; i++) {
                total_count += my_count_iter(v, val);
            }
            auto stop = high_resolution_clock::now();

            auto duration = duration_cast<microseconds>(stop - start);
            std::cout << "my_count_iter time:    " << duration.count() << std::endl;
        }

        std::cout << std::endl;
    }

    std::cout << total_count << std::endl;
    std::cin >> total_count;
}

这是我得到的:

std::count time:       679683
my_count_for time:     235269
my_count_for_in time:  228185
my_count_iter time:    650714

std::count time:       656192
my_count_for time:     231248
my_count_for_in time:  231050
my_count_iter time:    652598

std::count time:       660295
my_count_for time:     238812
my_count_for_in time:  225893
my_count_iter time:    648812

STL 函数不是解决任务的最快方法,这似乎仍然很奇怪。如果有人知道详细的答案,请分享给我。

这是因为 MSVC 向量化了您手动编写的代码,但无法对 std::count 执行相同的操作。

这是矢量化代码的样子:

        movdqa  xmm5, XMMWORD PTR __xmm@00000001000000010000000100000001
        and     rcx, -8
        xorps   xmm3, xmm3
        xorps   xmm2, xmm2
        npad    3
$LL4@my_count:
        movdqu  xmm1, XMMWORD PTR [rax]
        add     r8, 8
        movdqa  xmm0, xmm5
        paddd   xmm0, xmm3
        pcmpeqd xmm1, xmm4
        pand    xmm0, xmm1
        pandn   xmm1, xmm3
        movdqa  xmm3, xmm0
        movdqa  xmm0, xmm5
        por     xmm3, xmm1
        paddd   xmm0, xmm2
        movdqu  xmm1, XMMWORD PTR [rax+16]
        add     rax, 32                             ; 00000020H
        pcmpeqd xmm1, xmm4
        pand    xmm0, xmm1
        pandn   xmm1, xmm2
        movdqa  xmm2, xmm0
        por     xmm2, xmm1
        cmp     r8, rcx
        jne     SHORT $LL4@my_count

你可以看到它是如何在xmm5寄存器中加载4个开始的。该值将用于维护 4 个单独的计数器,用于跟踪第 1、第 2、第 3 和第 4 个 DWORD 的结果。计数完成后,这 4 个值将加在一起形成函数的结果。

MSVC 向量器的问题似乎在于计数器、数据类型和参数类型应该是 "compatible":

  • Return 类型在大小上应与数据类型相匹配
  • 参数类型的大小应等于或小于数据类型

如果不满足这些约束中的任何一个,则代码不会被矢量化。这是有道理的,就好像你的数据类型是 32 位宽,你必须对 32 位计数器进行操作才能使它们一起工作,所以如果你的 return 类型是 64 位宽,则需要一些额外的操作(这是 GCC 能够做到的,但与手动编写的循环相比,这仍然会减慢 std::count

在这种情况下,应该优先使用手动编写的循环,因为语义上的细微差别(int return 类型)使矢量化更容易(即使对于生成更短代码的 GCC 也是如此)。