QtConcurrent为多个内核提供更长的运行时间

时间:2018-06-28 09:57:26

标签: c++ qt

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

algorithm.h

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

protected:
    long mDummy;
};

algorithm.cpp

    #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秒。

有人可以解释为什么使用多个内核时第二次运行时运行时间会增加吗?

1 个答案:

答案 0 :(得分:1)

我相信问题在于@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 ++代码,选择要使用的编译器并查看结果即可。

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

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

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

使用标志 -O2 进行编译,当mDummy是 long 变量(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是 float 时结束:

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决定完全删除整个内容...:)