我已经设计了一种算法,现在我正在研究一种实现,以在多个内核上解决它。本质上,我给每个核心都相同的问题,然后我将选择得分最高的解决方案。但是,我注意到使用多个内核会减慢我的代码的运行速度,但是我不明白为什么。因此,我创建了一个非常简单的示例,该示例显示了相同的行为。我有一个简单的算法类:

算法

 class Algorithm
 {
 public:
    Algorithm() : mDummy(0) {};
    void runAlgorithm();

protected:
    long mDummy;
};


算法

    #include "algorithm.h"

    void Algorithm::runAlgorithm()
    {
        long long k = 0;
        for (long long i = 0; i < 200000; ++i)
        {
            for (long long j = 0; j < 200000; ++j)
            {
                k = k + i - j;
            }
        }
        mDummy = k;
    }


main.cpp

    #include "algorithm.h"
    #include <QtCore/QCoreApplication>
    #include <QtConcurrent/QtConcurrent>

    #include <vector>
    #include <fstream>
    #include <QFuture>
    #include <memory>

    int main(int argc, char *argv[])
    {
        QCoreApplication a(argc, argv);
        std::ofstream logFile;
        logFile.open("AlgorithmLog.log", std::ios::trunc | std::ios::out);
        if (!logFile.is_open())
        {
            return 1;
        }

        for (int i = 1; i < 8; i++)
        {
            int cores = i;
            logFile << "Start: cores = " << cores << "   " << QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1().data() << "\n";

            std::vector<std::unique_ptr<Algorithm>> cvAlgorithmRuns;
            for (int j = 0; j < cores; ++j)
                cvAlgorithmRuns.push_back(std::unique_ptr<Algorithm>(new Algorithm()));

            QFuture<void> assyncCalls = QtConcurrent::map(cvAlgorithmRuns, [](std::unique_ptr<Algorithm>& x) { x->runAlgorithm(); });
            assyncCalls.waitForFinished();

            logFile << "End: " << QDateTime::currentDateTime().toString(Qt::ISODate).toLatin1().data() << "\n";
            logFile.flush();
        }
        logFile.close();
        return a.exec();
    }


当我在笔记本电脑上运行此程序(我使用VS2015,x64,Qt 5.9.0、8个逻辑处理器)时,我得到:

Start: cores = 1   2018-06-28T10:48:30 End: 2018-06-28T10:48:44
Start: cores = 2   2018-06-28T10:48:44 End: 2018-06-28T10:48:58
Start: cores = 3   2018-06-28T10:48:58 End: 2018-06-28T10:49:13
Start: cores = 4   2018-06-28T10:49:13 End: 2018-06-28T10:49:28
Start: cores = 5   2018-06-28T10:49:28 End: 2018-06-28T10:49:43
Start: cores = 6   2018-06-28T10:49:43 End: 2018-06-28T10:49:58
Start: cores = 7   2018-06-28T10:49:58 End: 2018-06-28T10:50:13


这很有意义:无论我使用的是1核还是7核,所有步骤的运行时间都相同(14到15秒之间)。

但是,当我从以下位置更改algoritm.h中的行时:

protected:
    long mDummy;


至:

protected:
    double mDummy;


我得到以下结果:

Start: cores = 1   2018-06-28T10:52:30 End: 2018-06-28T10:52:44
Start: cores = 2   2018-06-28T10:52:44 End: 2018-06-28T10:52:59
Start: cores = 3   2018-06-28T10:52:59 End: 2018-06-28T10:53:15
Start: cores = 4   2018-06-28T10:53:15 End: 2018-06-28T10:53:32
Start: cores = 5   2018-06-28T10:53:32 End: 2018-06-28T10:53:53
Start: cores = 6   2018-06-28T10:53:53 End: 2018-06-28T10:54:14
Start: cores = 7   2018-06-28T10:54:14 End: 2018-06-28T10:54:38


在这里,我从1核的14秒运行时间开始,但是使用7核的运行时间增加到24秒。

谁能解释为什么使用多个内核时第二次运行时运行时间增加?

最佳答案

我相信问题在于@Aconcagua所建议的FPU的实际数量。
“逻辑处理器”(又称为“超线程”)与具有两倍内核的内核不同。

超线程中的8个核心仍然是4个“真实”核心。如果仔细查看时间,您会发现执行时间几乎相同,直到使用了4个以上的线程。当您使用4个以上的线程时,您可能会开始用完FPU。

但是,为了更好地理解该问题,建议您看一下实际生成的汇编代码。

当我们要测量原始性能时,我们必须记住,我们的C ++代码只是一个更高级别的表示,实际的可执行文件可能与我们期望的完全不同。

编译器将执行其优化,CPU将无序执行操作,等等。

因此,首先,我建议避免在循环中使用常量限制。根据情况,编译器可能会展开循环,甚至将其完全替换为其计算结果。

例如,代码:

int main()
{
    int z = 0;
    for(int k=0; k < 1000; k++)
        z += k;

    return z;
}


由GCC 8.1使用-O2优化编译为:

main:
  mov eax, 499500
  ret


如您所见,循环就消失了!

编译器将其替换为实际的最终结果。

使用这样的示例来衡量性能非常危险。在上面的示例中,迭代1000次或80000次是完全相同的,因为在两种情况下都将循环替换为常量(当然,如果溢出循环变量,编译器将无法再替换它)。

MSVC并不是那么积极,但是除非您查看汇编代码,否则您永远不会确切知道优化器的功能。

查看生成的汇编代码的问题在于它可能非常庞大...

解决此问题的简单方法是使用出色的compiler explorer。只需输入您的C / C ++代码,选择要使用的编译器,然后查看结果。

现在,回到您的代码,我使用MSVC2015 for x86_64在编译器资源管理器中对其进行了测试。

没有优化,它们的汇编代码看起来几乎相同,除了最后要转换为double(cvtsi2sd)的内在函数。

但是,启用优化后,事情开始变得有趣(这是在发布模式下进行编译时的默认设置)。

使用标志-O2进行编译,当mDummy是长变量(32位)时产生的汇编代码为:

Algorithm::runAlgorithm, COMDAT PROC
        xor      r8d, r8d
        mov      r9d, r8d
        npad     10
$LL4@runAlgorit:
        mov      rax, r9
        mov      edx, 100000          ; 000186a0H
        npad     8
$LL7@runAlgorit:
        dec      r8
        add      r8, rax
        add      rax, -4
        sub      rdx, 1
        jne      SHORT $LL7@runAlgorit
        add      r9, 2
        cmp      r9, 400000             ; 00061a80H
        jl       SHORT $LL4@runAlgorit
        mov      DWORD PTR [rcx], r8d
        ret      0
Algorithm::runAlgorithm ENDP


当mDummy是浮点数时结束:

Algorithm::runAlgorithm, COMDAT PROC
        mov      QWORD PTR [rsp+8], rbx
        mov      QWORD PTR [rsp+16], rdi
        xor      r10d, r10d
        xor      r8d, r8d
$LL4@runAlgorit:
        xor      edx, edx
        xor      r11d, r11d
        xor      ebx, ebx
        mov      r9, r8
        xor      edi, edi
        npad     4
$LL7@runAlgorit:
        add      r11, -3
        add      r10, r9
        mov      rax, r8
        sub      r9, 4
        sub      rax, rdx
        dec      rax
        add      rdi, rax
        mov      rax, r8
        sub      rax, rdx
        add      rax, -2
        add      rbx, rax
        mov      rax, r8
        sub      rax, rdx
        add      rdx, 4
        add      r11, rax
        cmp      rdx, 200000          ; 00030d40H
        jl       SHORT $LL7@runAlgorit
        lea      rax, QWORD PTR [r11+rbx]
        inc      r8
        add      rax, rdi
        add      r10, rax
        cmp      r8, 200000             ; 00030d40H
        jl       SHORT $LL4@runAlgorit
        mov      rbx, QWORD PTR [rsp+8]
        xorps    xmm0, xmm0
        mov      rdi, QWORD PTR [rsp+16]
        cvtsi2ss xmm0, r10
        movss    DWORD PTR [rcx], xmm0
        ret      0
Algorithm::runAlgorithm ENDP


在不深入了解这两种代码如何工作或优化器为何在两种情况下表现不同的细节时,我们可以清楚地看到一些区别。

特别是第二个版本(mDummy处于浮动状态的版本):


稍长
使用更多的寄存器
更频繁地访问内存


因此,除了超线程问题之外,第二个版本更可能产生缓存未命中,并且由于缓存是共享的,因此这也可能影响最终执行时间。

而且,诸如涡轮增压之类的事情也可能会出现。施加压力时,CPU可能会节流,从而导致整体执行时间增加。

对于记录,这是clang启用优化后产生的结果:

Algorithm::runAlgorithm(): # @Algorithm::runAlgorithm()
  mov dword ptr [rdi], 0
  ret


困惑?好吧...没人在其他地方使用mDummy,所以clang决定完全删除整个内容... :)

关于c++ - QtConcurrent为多个内核提供更长的运行时间,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/51079939/

10-14 03:58