【问题标题】:Which loop optimization is the compiler performing here?编译器在这里执行哪个循环优化?
【发布时间】:2020-07-29 15:17:57
【问题描述】:

假设如下代码:

#include <thread>
#include <iostream>
#include <vector>
#include <future>
#include <chrono>
#include <cmath>

long long int partialSum(const std::vector<int>& v, int begin, int end) {
    long long int sum = 0;
    for (int i = begin; i < end; i++) {
        sum += (v[i]);
    }
    return sum;
}

int main() {
    std::vector<int> v(10000000, 1);

    //2 threads
    auto start = std::chrono::high_resolution_clock::now();
    std::future<long long int> f1 = std::async(std::launch::async, partialSum, v, 0, 10000000 /2);
    std::future<long long int> f2 = std::async(std::launch::async, partialSum, v, 10000000 / 2, 10000000);
    volatile long long int a = f1.get() + f2.get();
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono:: duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "With 2 threads-> " << duration.count() << std::endl;

    //1 thread
    start = std::chrono::high_resolution_clock::now();
    volatile long long int b = partialSum(v, 0, 10000000);
    end = std::chrono::high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "With 1 thread-> " << duration.count() << std::endl;
}

以及我机器的输出(VS2019):

With 2 threads-> 35477
With 1 thread-> 7000

请注意,我必须添加 volatile 以避免编译器执行更多优化。 另请注意,我知道std 中有一个accumulate,但我目前正在学习多线程,这是一个 POC。 基本上,我想知道编译器在这里进行了哪种优化,因为与线程版本相比,它的优化非常好。 当我更改partialSum并替换操作(可能是log10)时,线程版本是常规版本的两倍。

编辑: 经过一些建议,我将代码更改为以下代码:

#include <thread>
#include <iostream>
#include <vector>
#include <future>
#include <chrono>
#include <cmath>

long long int partialSum(const std::vector<int>& v, int begin, int end) {
    long long int sum = 0;
    for (int i = begin; i < end; i++) {
        sum += (v[i]);
    }
    return sum;
}

int main() {
    std::vector<int> v(10000000, 1);

    //2 threads
    auto start = std::chrono::high_resolution_clock::now();
    std::future<long long int> f1 = std::async(std::launch::async, partialSum, std::cref(v), 0, 10000000 /2);
    std::future<long long int> f2 = std::async(std::launch::async, partialSum, std::cref(v), 10000000 / 2, 10000000);
    volatile long long int a = f1.get() + f2.get();
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono:: duration_cast<std::chrono::microseconds>(end - start);
    std::cout << "With 2 threads-> " << duration.count() << std::endl;

    //1 thread
    start = std::chrono::high_resolution_clock::now();
    f1 = std::async(std::launch::async, partialSum, std::cref(v), 0, 10000000);
    end = std::chrono::high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    std::cout << "With 1 thread-> " << duration.count() << std::endl;
}

然后输出:

With 2 threads-> 11835
With 1 thread-> 0

【问题讨论】:

  • 切换顺序(第一个线程,第二个线程)......你得到相同或相似的结果吗?
  • @ChrisMM 是的,非常相似
  • 可能有两件事:函数实际上很快,开销来自线程启动(我不知道需要多长时间,这也取决于您的操作系统);因为可能 log10 示例需要更长的时间,所以开销不再重要。另一件事可能是线程以某种方式相互干扰缓存行(我不知道这是否可能);但是对于在内存上运行的廉价函数来说,缓存才是王道。
  • @lalala 我仍然认为这里可能在幕后发生了一些事情。这可能是线程创建/控制的开销,但我认为它与缓存无关。
  • @G.Sliepen 如果sumi 实际上在内存中,编译器应该被解雇

标签: c++ multithreading for-loop optimization


【解决方案1】:

存在性能差距是因为您实际上是在此处复制向量:

std::future<long long int> f1 = std::async(std::launch::async, partialSum, v, 0, 10000000 /2);
std::future<long long int> f2 = std::async(std::launch::async, partialSum, v, 10000000 / 2, 10000000);

使用std::cref 通过常量引用传递它们:

std::future<long long int> f1 = std::async(std::launch::async, partialSum, std::cref(v), 0, 10000000 /2);
std::future<long long int> f2 = std::async(std::launch::async, partialSum, std::cref(v), 10000000 / 2, 10000000);

然后再次尝试测量性能。对我来说,进行此更改后,2 线程版本更快。在这里试试:Godbolt link

您的第二个 sn-p 正在为 1 个线程打印 0,因为您没有等待 f1 完成。

在获取end 值之前输入:

volatile long long int b = f1.get();

至于循环优化(这部分对于 OP 来说可能是不必要的),编译器 (GCC) 正在对循环进行矢量化(没有任何 -march= 选项)。生成的 asm 如下所示:

.L399:
        movdqu  xmm0, XMMWORD PTR [rax]
        movdqa  xmm2, xmm4
        add     rax, 16
        pcmpgtd xmm2, xmm0
        movdqa  xmm3, xmm0
        punpckldq       xmm3, xmm2
        punpckhdq       xmm0, xmm2
        paddq   xmm1, xmm3
        paddq   xmm1, xmm0
        cmp     rdx, rax
        jne     .L399

如果我们使用int 而不是long long int,我们可以让编译器更容易地进一步优化它。然后将 asm 输出简化为:

        pxor    xmm0, xmm0
.L13:
        movdqu  xmm2, XMMWORD PTR [rdx]
        add     rdx, 16
        paddd   xmm0, xmm2
        cmp     rcx, rdx
        jne     .L13

【讨论】:

  • 我明白了,这样性能会提高,但是使用你的链接我得到了 2 个线程-> 5834 1 个线程-> 5333
  • 仍然存在线程创建开销,对我来说是With 2 threads-&gt; 5956 With 1 thread-&gt; 6140,有时With thread 1 更快
  • 请查看我的编辑,我还为 1 线程版本的异步创建了另一个线程,并检查结果。
  • 看来你是对的。 f1.get() 等待结果,不需要 wait(),但问题是我没有为 1 线程版本执行 get()。如果您想将此添加到您的答案中,我会接受。
猜你喜欢
  • 2017-01-27
  • 1970-01-01
  • 2012-12-11
  • 1970-01-01
  • 1970-01-01
  • 2021-02-17
  • 2011-10-30
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多