c++ - 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?
解决方案
我相信问题在于@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 所以铿锵决定完全删除整个东西... :)
推荐阅读
- javascript - 如何在使用延迟加载的页面加载时使 jQuery 代码工作
- node.js - 使用 aws lambda 函数将 cloudwatch 日志推送到 s3
- vue.js - 使用 vue.js 在 v-for 中切换表单
- java - 可变参数和 for 循环的减法和除法运算
- node.js - Node js App显示文件夹文件
- c++ - 打开 C++ 故障转储不会在调用堆栈中显示正确的行
- java - 通过视频 url 上传 youtube 数据 api
- twitter-bootstrap - 表格水平不起作用
- google-apps-script - 在客户端验证 textInput 小部件 - Gmail 添加
- asp.net - 如何获取 Scripts.Render(“~/bundles/someScript”) 生成的 URL?