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