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

3

我创建了一个非常简单的测试程序,计算一些标量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倍。
您知道这里发生了什么吗?

评论不适合进行长时间的讨论;此对话已被移至聊天室 - Samuel Liew
1个回答

2
我不够有知识来给出一个明确的答案,但由于目前还没有答案,我会尽力而为:
短答案:
多次顺序迭代大小为L的数组(其中L大于最大缓存大小)将无法利用任何缓存进行数组的新缓存行访问(因为缓存使用LRU的变体)。快速迭代大小为L的数组并进行快速计算意味着访问(主)内存是瓶颈,并将占用所有运行进程/线程的共享内存总线。添加更多由主内存访问限制的线程将只会引起它们之间的竞争。
(如果非常聪明,您的缓存可能会在新的数组数据到达L2缓存之前放弃它,因为它们意识到在它被推出之前,您将无法使用它。这不会影响此过程,但会为其他人留下更多的缓存空间。)

更多信息:

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

...会给出带有movups指令的汇编输出,该指令与该行代码相关联两次:

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

"movups"是一条x86并行化(SIMD)指令。像这样的SIMD指令通常将4个放入巨型寄存器中,以便快速进行计算(总时间约为10个时钟周期,但如果我错了,请纠正我)。乘以4以得到在缓存线上完成的工作:~40个周期。如果您没有使用x86,则可能使用具有类似并行化指令的CPU。
主内存访问速度较慢(从主内存获取缓存行大约需要100-200个时钟周期[缓存行=64字节块〜16个double])。
您的CPU将尝试通过预取数据来帮助您,但是因为您始终以比数据总线传递速度更快的速率从主内存请求数据,所以预取可能 只能帮助您将等待时间从约100降至60,如果您很幸运。无论如何,等待主内存访问仍然是瓶颈。
请注意,这也适用于另一个方向,您可以使用更新的数组值填充缓存,但在大约8MB后,您需要不断将该数据发送回主内存。因此,我们上面的估计实际上是乐观的。
挑剔的细节: < p > test 函数有一个小错误:

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


我相信你在聊天中的回答和ALX23z的评论是正确的。像Linux的perf stat -a这样的性能监测工具表明,多线程版本的每周期指令数(IPC)比单线程版本低。这可能表明存在内存带宽问题。为了进一步测试这个理论,如果你在内部赋值循环中让线程睡眠一毫秒(以消除内存带宽问题),那么多线程版本的速度将再次加倍。我相信你是正确的,因此已经回答了这个问题。谢谢。 - Theodor Johnson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接