首页 > 解决方案 > 多线程程序比单线程程序慢

问题描述

我创建了一个非常简单的测试程序,可以计算一些标量 c 和矩阵 A 的 c*A。您可以在这里在线运行它,或者将以下代码粘贴到您喜欢的文本编辑器中:

#include <iostream>
#include <time.h>
#include <chrono>
#include <thread>

void fill_rand_matrix(double* mat, int n){
    
    for (int i=0;i<n;i++){
        mat[i]=static_cast <double> (rand()) / static_cast <double> (RAND_MAX)*20-10;
    }
}

void test(size_t m, size_t n, double alpha, double* X) {

        for (int j = 0; j < m; ++j) {
            for (int i = 0; i < n; ++i) {
                X[i+ j*n] *= alpha;
            }
        }       
}   

int main()
{
    int m=10000;
    int n=10000;
    double res_scaling=0.5;
    
    double* res=new double[m*n];
    fill_rand_matrix(res,n*m);
    
    auto begin1 = std::chrono::steady_clock::now();
    std::thread t1(test,0.5*m,n,res_scaling,res);
    std::thread t2(test,0.5*m,n,res_scaling,(double*)(res+(m/2)*n));
    t1.join();
    t2.join();
    auto end1= std::chrono::steady_clock::now();
    std::cout << "Time taken multithreaded = " << std::chrono::duration_cast<std::chrono::milliseconds>(end1 - begin1).count() << "[ms]" << std::endl;

    auto begin2 = std::chrono::steady_clock::now();
    test(m,n,res_scaling,res);
    auto end2= std::chrono::steady_clock::now();
    std::cout << "Time taken singlethreaded = " << std::chrono::duration_cast<std::chrono::milliseconds>(end2 - begin2).count() << "[ms]" << std::endl;

    return 0;
}

当我多次运行此代码时,多线程版本要么只比单线程版本快一点,要么甚至更慢。如果我添加超过 2 个线程,甚至会发生这种情况。多线程似乎没有任何好处,即使这个问题应该几乎可以完美地随着内核数量而扩展。此外,我将矩阵大小设置得越高,运行时间的波动就越剧烈,有时甚至高达 20 倍。

你知道这里发生了什么吗?

标签: c++multithreadingc++17

解决方案


我没有足够的知识来给出明确的答案,但由于到目前为止还没有答案,我会尽力而为:


简短的回答:

对大小为L的数组(其中L大于最大缓存的大小)进行多次顺序迭代将无法利用任何缓存来对数组进行新的缓存行访问(因为缓存使用变体的LRU)。通过快速计算快速迭代大小为L的数组意味着访问(主)内存是瓶颈,并且将占用所有正在运行的进程/线程的共享内存总线。添加更多也受主内存访问约束的线程只会导致它们之间的竞争。

(如果非常聪明,您的缓存可能会在每个新数组数据进入 L2 缓存之前丢弃它,意识到在它被推出之前您无法使用它。这不会影响这个过程,但会为其他人留下更多的缓存空间。)


更多信息:

对我来说,使用g++ -std=c++17 -O3 -pthread -S -fverbose-asm

movups...给出与该行相关的指令两次的汇编输出:

X[i+ j*n] *= alpha; // line 17

movupsx86 并行化 (SIMD) 指令。像这样的 SIMD 指令通常将 4double秒放入一个巨大的寄存器中以非常快速地进行计算(总共约 10 个时钟周期,但如果我错了,请纠正我)。将此乘以 4 以在高速缓存行上完成工作:约 40 个周期。如果您不使用 x86,那么您可能正在使用具有类似并行化指令的 CPU。

主存访问速度很慢(大约需要 100-200 个时钟周期才能从主存中获取缓存线 [缓存线 = 64 字节块 ~= 16 双倍])。

但是,您的 CPU 会尝试通过预取数据来帮助您,因为您总是以比数据总线能够提供的速度更快的速度从主内存请求数据,因此预取可能只会帮助您减少等待时间〜100到〜60,如果你幸运的话。无论哪种方式,等待主存访问仍然是瓶颈。

请注意,这也适用于另一个方向,您可以用更新的数组值填充缓存,但在 8MB 左右之后,您需要不断地将数据发送回主内存。因此,我们的上述估计确实是乐观的。


挑剔:

test函数有一个小错误:

j < m并且i < n是有符号/无符号比较。


推荐阅读