使用线程拆分任务正在减慢我的工作速度

Posted

技术标签:

【中文标题】使用线程拆分任务正在减慢我的工作速度【英文标题】:Using threads to split up task is slowing down my work 【发布时间】:2021-01-06 07:55:33 【问题描述】:

所以我有这个问题:simple-division-of-labour-over-threads-is-not-reducing-the-time-taken。我以为我已经把它整理好了,但是回去重新审视这项工作,我并没有像以前那样疯狂放慢速度(由于rand() 中的互斥锁),但我的总时间也没有任何改善。

在代码中,我在 y 个线程上拆分了 x 个工作迭代的任务。

因此,如果我想在一个线程中进行 100'000'000 次计算,可能需要大约 350 毫秒,那么我希望它在 2 个线程中(每个线程进行 50'000'000 次计算)需要大约 175 毫秒,然后三个线程〜115ms等等......

我知道由于线程开销等原因,使用线程不会完美地分割工作。但我希望至少能获得一些性能提升。

我稍微更新的代码在这里:

结果

1 个线程:

starting thread: 1 workload: 100000000
thread: 1 finished after: 303ms val: 7.02066
==========================
thread overall_total_time time: 304ms

3 个线程

starting thread: 1 workload: 33333333
starting thread: 3 workload: 33333333
starting thread: 2 workload: 33333333
thread: 3 finished after: 363ms val: 6.61467
thread: 1 finished after: 368ms val: 6.61467
thread: 2 finished after: 365ms val: 6.61467
==========================
thread overall_total_time time: 368ms

您可以看到 3 个线程实际上比 1 个线程花费的时间略长,但每个线程只执行 1/3 的工作迭代。我在家里的 8 个 CPU 内核的 PC 上发现了类似的性能提升不足。

它不像线程开销应该花费超过几毫秒 (IMO),所以我看不到这里发生了什么。我不相信有任何资源共享冲突,因为这段代码非常简单并且不使用外部输出/输入(除了 RAM)。

参考代码

在神螺栓中:https://godbolt.org/z/bGWdxE

在 main() 中,您可以调整线程数和工作量(循环迭代)。

#include <iostream>
#include <vector>
#include <thread>
#include <math.h>

void thread_func(uint32_t interations, uint32_t thread_id)

    // Print the thread id / workload
    std::cout << "starting thread: " << thread_id << " workload: " << interations << std::endl;
    // Get the start time
    auto start = std::chrono::high_resolution_clock::now();
    // do some work for the required number of interations
    double val0;
    for (auto i = 1u; i <= interations; i++)
    
        val += i / (2.2 * i) / (1.23 * i); // some work
    
    // Get the time taken
    auto total_time = std::chrono::high_resolution_clock::now() - start;
    // Print it out
    std::cout << "thread: " << thread_id << " finished after: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(total_time).count()
              << "ms" << " val: " << val << std::endl;


int main()

    uint32_t num_threads = 3; // Max 3 in godbolt  
    uint32_t total_work = 100'000'000;

    // Store the start time
    auto overall_start = std::chrono::high_resolution_clock::now();

    // Start all the threads doing work
    std::vector<std::thread> task_list;
    for (uint32_t thread_id = 1; thread_id <= num_threads; thread_id++)
    
        task_list.emplace_back(std::thread([=]() thread_func(total_work / num_threads, thread_id); ));
    

    // Wait for the threads to finish
    for (auto &task : task_list)
    
        task.join();
    

    // Get the end time and print it
    auto overall_total_time = std::chrono::high_resolution_clock::now() - overall_start;
    std::cout << "\n==========================\n"
              << "thread overall_total_time time: "
              << std::chrono::duration_cast<std::chrono::milliseconds>(overall_total_time).count()
              << "ms" << std::endl;
    return 0;

更新

我想我已经缩小了我的问题范围: 在我的 64 位 VM 上,我看到:

为 32 位无优化编译:更多线程 = 运行速度更慢! 通过优化编译为 32 位:更多线程 = 运行速度更快 为 64 位无优化编译:更多线程 = 运行速度更快(如预期) 使用优化编译为 64 位:更多线程 = 与没有 opt 的情况相同,但一般情况下一切都需要更少的时间。

所以我的问题可能只是在 64 位 VM 上运行 32 位代码。但是我真的不明白为什么如果我的可执行文件是在 64 位架构上运行的 32 位,那么添加线程不能很好地工作......

【问题讨论】:

线程创建/加入是昂贵的。无论如何,测试一次都不是可靠的测量方法。您必须至少运行此代码一千次并计算平均值。 无法复制。我有 2 个内核,我发现在代码中放入 1 和 2 之间的速度提高了约 2 倍。我想知道您的系统是否以某种方式默认将整个进程绑定到一个内核,这样即使它使多个线程都竞争时间并因此减慢彼此的速度。如果 Godbolt 这样做,我至少不会感到惊讶。 你需要开启编译器优化,未优化代码的性能测试毫无意义 @AlanBirtles 在这种情况下不是......要么工作被分割,时间减少,要么没有。启用优化不会做任何事情,除了一直按某个因素缩放,(在我的系统上,它们实际上似乎什么都不做,句号)。 @HTNW 不一定,std::thread 中可能有一些东西在没有优化的情况下速度非常慢 【参考方案1】:

有很多可能的原因可以解释观察到的结果,所以我认为没有人能给你一个明确的答案。此外,大多数原因与硬件架构的特殊性有关,因此在不同的机器上可能会有不同的答案是对还是错。

正如 cmets 中已经提到的,很可能是线程分配有问题,因此您并没有真正享受使用多线程带来的任何好处。 Godbolt.org 是一个云服务,所以它很可能是高度虚拟化的,这意味着你的线程正在与谁知道有多少其他线程竞争,所以我会对在 Godbolt 上运行的任何结果分配零信任。

未优化的 32 位代码在 64 位 VM 上性能不佳的一个可能原因是未优化的 32 位代码没有有效利用寄存器,因此它成为内存受限的。代码看起来很适合 CPU 缓存,但即使是缓存也比直接寄存器访问要慢得多,并且在多个线程竞争访问缓存的多线程场景中差异更加明显。

在 64 位 VM 上优化的 32 位代码性能仍然不理想的一个可能原因是 CPU 针对 64 位使用进行了优化,因此在 32 位模式下运行时指令没有有效地流水线化,或者 CPU 的运算单元没有被有效地使用。可能是代码中的这些划分使所有线程都在竞争除法器电路,CPU 可能只有一个,或者 CPU 在 32 位模式下运行时可能只有一个。这意味着大多数线程除了等待除数可用之外什么都不做。

请注意,线程由于电路争用而减慢的情况与线程由于等待某些设备响应而减慢的情况非常不同。在第一种情况下,没有线程上下文切换,因此线程看起来好像在全速运行,尽管它被延迟了。在第二种情况下,如果设备忙,线程通常会进入 I/O 等待模式,因此线程不会全速运行。这解释了您的 CPU 利用率观察结果。

【讨论】:

谢谢 Mike - 实际上经过更多测试后,我注意到与 x64 发布版本相比,我在 x64 调试版本上并不总是能获得很好的结果(因为百分比增益对于调试来说并不好) - 但你的解释似乎与我所看到的事情相匹配。很高兴忽略 goldbolt 的输出 :)

以上是关于使用线程拆分任务正在减慢我的工作速度的主要内容,如果未能解决你的问题,请参考以下文章

如何仅减慢当前请求的速度? sleep() 没有按预期工作

Android NDK - 多线程正在减慢渲染速度

在 OpenMp 中使用更多线程会减慢我的程序。串行速度比

控制台不必要的错误消息是不是会减慢网页速度?

单元格中的 Excel VBA 换行会减慢合并任务的执行速度

NOT IN 语句正在减慢我的查询速度