我已经设计了一种算法,现在我正在研究一种实现,以在多个内核上解决它。本质上,我给每个核心都相同的问题,然后我将选择得分最高的解决方案。但是,我注意到使用多个内核会减慢我的代码的运行速度,但是我不明白为什么。因此,我创建了一个非常简单的示例,该示例显示了相同的行为。我有一个简单的算法类:
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秒。
有人可以解释为什么使用多个内核时第二次运行时运行时间会增加吗?
答案 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决定完全删除整个内容...:)