繁体   English   中英

多线程程序比单线程程序慢

[英]Multithreaded Program slower than singlethreaded

我创建了一个非常简单的测试程序,用于计算一些标量 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 倍。

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

我的知识不够,无法给出明确的答案,但由于目前没有答案,我会尽力而为:


简短的回答:

对大小为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 指令通常将 4 个double放在一个巨大的寄存器上以非常快速地进行计算(大约 10 个时钟周期,但如果我错,请纠正我)。 将此乘以 4 以在缓存行上完成工作:~40 个周期。 如果您不使用 x86,那么您可能使用的是具有类似并行化指令的 CPU。

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

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

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


挑剔:

test功能有个小bug:

j < mi < n是有符号/无符号比较。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM