首页 > 解决方案 > QtConcurrent gives longer runtimes for multiple cores

问题描述

I have designed an algorithm and now I'm working on an implementation to solve it on multiple cores. Essentially I'm giving each core the same problem and I'll choose the solution with the best score. However, I'm noticing that using multiple cores slows down the runtime of my code, but I don't understand why. So I created a very simple example that shows the same behaviour. I have a simple Algoritmn class:

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();
    }

When I run this on my laptop (I'm using VS2015, x64, Qt 5.9.0, 8 logical processors) I get:

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

Which makes sense: the same runtime (between 14 and 15 seconds) for all steps, whether I'm using 1 core or 7 cores.

But when I change the line in algoritm.h from:

protected:
    long mDummy;

to:

protected:
    double mDummy;

I get these results:

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

Here I start with 14 seconds runtime for 1 core, but the runtime increases to 24 seconds using 7 cores.

Can anybody explain why in the second run the runtime increases when using multiple cores?

标签: c++qt

解决方案


我相信问题在于@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 没有那么激进,但你永远不知道优化器到底做了什么,除非你查看汇编代码

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

解决此问题的一个简单方法是使用出色的编译器资源管理器。只需输入您的 C/C++ 代码,选择您要使用的编译器并查看结果。

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

如果没有优化,它们的汇编代码看起来几乎相同,除了最后转换为双精度 (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 所以铿锵决定完全删除整个东西... :)


推荐阅读