【问题标题】:Run time with two threads vs one thread not improving两个线程与一个线程的运行时间没有改善
【发布时间】:2020-07-27 20:36:30
【问题描述】:

我是 C++ 多线程编程的新手。我写了一段简单的代码,我将在下面粘贴。 当在两个线程中运行时,代码完成的速度几乎不比在单线程中运行时快。我遇到过其他类似的问题,但是它们不同,因为我没有两个线程都需要访问的共享资源

代码如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>

using namespace std;

typedef unsigned long long ull;

ull oddSum = 0;
ull evenSum = 0;

void addOdd(){
    for(int i = 1; i <= 1999999999; i++){
        if(i % 2 == 1)
            oddSum += i;
    }
}

void addEven(){
    for(int i = 1; i <= 1999999999; i++){
        if(i % 2 == 0)
            evenSum += i;
    }
}

int main(){
    auto startTime = std::chrono::high_resolution_clock::now();

    //Two threads
    std::thread t1(addEven);    //launch the two threads to run
    std::thread t2(addOdd); 

    t1.join();
    t2.join();    //wait for both to finish

    //One thread
    //addEven();
    //addOdd();

    auto stopTime = std::chrono::high_resolution_clock::now();
    auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(stopTime - startTime);

    cout << "Odd Sum: " << oddSum << endl;
    cout << "Even Sum: " << evenSum << endl;

    cout << elapsed.count()/(double)1000000 << endl;

    return 0;
}

当我使用单线程运行时,平均运行 10 次约为 7.3 秒

当我使用 两个线程 运行时,平均运行 10 次约为 6.8 秒

由于函数中的循环占用了绝大多数时间,我相信并行运行两个线程,每个线程都有一个函数,运行时间会减半。

注意 1:我知道时间不可能正确减半,更有根据的猜测可能是两个线程中最多 5 秒的运行时间。我了解线程对象的创建有其自身的开销。

注意 2:也许我遗漏了一些东西,但是线程没有访问两者之间的任何共享位置。

欢迎任何想法。我熟悉并发编程背后的理论,我现在才刚刚开始获得一些实践经验。我有一个 4 核的英特尔 i7。

【问题讨论】:

  • oddSum/evenSum 是否在同一缓存行上?它们应该相隔 64 字节
  • 您记得启用优化吗?
  • @mikelRychliski 成功了,谢谢。将对此进行更多阅读。
  • 你使用的是什么 CPU 和编译器?

标签: c++ multithreading performance


【解决方案1】:

当频繁从不同线程访问变量时,您应该确保它们位于不同的缓存行(在 x86 上为 64 字节宽)。

您可以通过对齐变量来做到这一点:

ull oddSum __attribute__((aligned(64))) = 0;
ull evenSum  __attribute__((aligned(64))) = 0;

未能有效地序列化写入,因为缓存行一次只能被一个 CPU 修改。

在多线程情况下,对齐变量为我减少了 30% 的运行时间。

正如@walnut 在他的评论中提到的,如果你的编译器支持 C++17,这可以以可移植的方式完成:

#include <new>
alignas(std::hardware_destructive_interference_size) ull oddSum = 0;
alignas(std::hardware_destructive_interference_size) ull evenSum  = 0;

【讨论】:

  • 这解决了我的问题。肯定会在这个问题上做更多的阅读。编辑:也发现了这篇文章,更深入的解释stackoverflow.com/questions/8469427/…
  • 我们从 C++11 开始就有 alignas,从 C++17 开始就有 std::hardware_destructive_interference_size。您不需要使用特定于编译器的属性和魔术常量。
  • 您使用的是什么 CPU 和编译器?还有什么优化级别?
  • @WBuck:GCC 4.8,没有优化(我猜这使得测量不是很有意义......)。通过优化不会出现问题,因为编译器不会在每次迭代时将变量写回内存。我猜原始海报没有根据他们的基准数字启用优化(通过优化,我 10 多年前的 Xeon 5150 比原始数字快得多)。
  • @Wbuck 我使用的是 g++ 版本 5.1.0,我有一个 4 核的 Intel i7-8550U。我没有通过优化标志(主要是因为我不知道它具体地做什么)。正如我所提到的,我只是想获得一些实践经验(学习和理解为什么程序会以特定方式运行,其背后的原因是什么),我的目标不是让这个特定程序运行得尽可能快。感谢您的意见,虽然它非常有帮助
【解决方案2】:

您没有对两个线程进行优化。下面是单线程解决方案。该线程遍历了 1999999999 个数字。

for(int i = 1; i <= 1999999999; i++){
    if(i % 2 == 1)
        oddSum += i;
    else
        evenSum += i;
}

下面是两个线程的两个循环,每个循环都遍历相同的数字。所以每个线程执行的工作量几乎相同。

for(int i = 1; i <= 1999999999; i++){
    if(i % 2 == 1)
        oddSum += i;
}

for(int i = 1; i <= 1999999999; i++){
    if(i % 2 == 0)
        evenSum += i;
}

两个线程的优化方案,两个线程共享1999999999次迭代:

for(int i = 1; i <= 1999999999; i += 2){
    oddSum += i;
}


for(int i = 2; i <= 1999999999; i += 2){
    evenSum += i;
}

现在每个线程执行的工作量减少了两倍。

【讨论】:

  • 感谢您的建议。但是我的问题不涉及优化(尽管我可以看到您的代码是如何改进的)。我只是询问在 1 个线程和 2 个线程中运行时代码之间的运行时差异。
  • 我在给你的回答中已经解释过了。解决方案中的每个线程执行的工作量与单线程解决方案相同。因此,时间间隔没有差异。
  • 如果我在 1 个线程上运行您改进的代码,与在 2 个线程中运行您的代码没有区别。我的问题是这两个全局变量显然在同一个缓存行中,正如接受的答案中所建议的那样。
  • 别说,试一试确定。单线程将遍历 1999999999 个数,双线程将并行遍历 1999999999/2 个数,速度快 2 倍。
  • “拆分工作”在问题中的完成方式确实有可能减少完成的工作(因为写入可以同时完成)。但显然,当我们仍然在每个线程上执行相同数量的比较/移位操作时,我们不应该期望一半的运行时间。
【解决方案3】:

因为您有两个相邻声明的全局变量,您可能遇到了false sharing

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-10-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-02
    • 1970-01-01
    相关资源
    最近更新 更多