几个比较怎么可能比一些计算慢



我们正在开发一段代码,该代码将检查何时不应允许用户在一段时间内进入扇区,我的一位同事创建了一个函数,该函数在下面的代码中是isAllow并包含几个比较,我采用了不同的方法,即函数isAllowed2,它使用时间段之间的秒数。

起初我们毫不怀疑他的函数会更快,但在实际运行代码和比较速度时并非如此,即使差异是我们可以完全忽略的,我们也想知道为什么"应该">更快的那个实际上更慢。

考虑以下代码:

#include <iostream>
#include <chrono>
using namespace std;
using namespace std::chrono;
struct timing {
short hour;
short minute;
};
bool isAllowed(timing &from, timing &to, timing &actual) {
return !(((from.hour > to.hour && (actual.hour >= from.hour || actual.hour <= to.hour)) ||
(actual.hour >= from.hour && actual.hour <= to.hour)) &&
!(actual.minute > from.minute && actual.minute < to.minute));
}
long getSecs(short hour, short minutes) {
return (hour * 3600) + (minutes * 60);
}
bool isAllowed2(timing &from, timing &to, timing &current) {
long secsFrom = getSecs(from.hour, from.minute);
long secsTo = getSecs(to.hour, to.minute);
long secsCurrent = getSecs(current.hour, current.minute);
if (secsFrom > secsTo) secsTo += 24 * 60 * 60;
if (secsCurrent > secsFrom && secsCurrent < secsTo) {
return false;
}
return true;
}
int main() {
//debug messages
std::string okay = " - ok";
std::string error = " - er";
std::string receive = " - allowed";
std::string notReceive = " - denied";
//testing times
int const testDataCount = 5;
timing from[testDataCount] = {
{ 16, 30 },
{ 8,  30 },
{ 10, 30 },
{ 0, 30 },
{ 0, 0 }
};
timing to[testDataCount] = {
{ 8,  30 },
{ 20, 0 },
{ 20, 0 },
{ 6, 0 },
{ 7, 0 }
};
for (int i = 0; i < testDataCount; i++) {
std::cout << i + 1 << ": " << from[i].hour << ":" << from[i].minute << " to " << to[i].hour << ":"
<< to[i].minute << std::endl;
}
//test current times
timing current[5] = {
{ 12, 0 },
{ 23, 0 },
{ 17, 30 },
{ 15, 12 },
{ 0, 20 }
};
bool ergValues[][testDataCount] = {
{ true,  false, false, true, true },
{ false, true,  true, true, true },
{ false, false, false, true, true },
{ true,  false, false, true, true },
{ false,  true, true, true, false }
};
long totalNs1 = 0;
long totalNs2 = 0;
for (int i = 0; i < 4; i++) {
std::cout << std::endl << i + 1 << ". Test: " << current[i].hour << ":" << current[i].minute << std::endl;
for (int j = 0; j < testDataCount; j++) {
high_resolution_clock::time_point t1 = high_resolution_clock::now();
bool response = isAllowed(from[j], to[j], current[i]);
high_resolution_clock::time_point t2 = high_resolution_clock::now();
high_resolution_clock::time_point t3 = high_resolution_clock::now();
bool response2 = isAllowed2(from[j], to[j], current[i]);
high_resolution_clock::time_point t4 = high_resolution_clock::now();
long ns1 = duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
totalNs1 += ns1;
long ns2 = duration_cast<std::chrono::nanoseconds>(t4 - t3).count();
totalNs2 += ns2;
std::cout << j + 1 << "tt:1:" << ns1 << "ns: " << response << (response == ergValues[i][j] ? okay : error) << "tt:2:" << ns2 << "ms: " << response2 << (response2 == ergValues[i][j] ? okay : error) << "tt"
<< (ergValues[i][j] ? receive : notReceive) << std::endl;
}
}
std::cout << "rntotalNs1 = " << totalNs1 << "rntotalNs2 = " << totalNs2 << "rnrn";
return 0;
}

结果显然总是不同的,但无论如何,总Ns2总是小于总Ns1。

前任:

totalNs1 = 38796
totalNs2 = 25913

我在Windows 10和Debian 8上的AMD Phenom II X4和Intel i7-3770上进行了测试,结果非常相似。

所以最后的问题是,为什么函数isAllowed2isAllowed更快?

注意:这主要是一个好奇的问题,如果有人认为标题或标签不是最合适的,请告诉我,我会相应地更改它们,请原谅任何最终的语法错误,因为英语不是我的母语。


编辑

同时,我一直在根据所有建议和评论进一步研究,包括这个令人难以置信的详细答案,在了解了微基准测试的不准确程度之后(非常感谢Baum mit Augen链接了这个惊人的演讲,这有很大帮助)我最终使用 Google 微基准库来获得更"准确"的结果,似乎isAllow函数实际上更快(未经优化编译),如库的输出所示。

Run on (8 X 2395 MHz CPU s)
-----------------------------------------------------------------------
Benchmark                                Time           CPU Iterations
-----------------------------------------------------------------------
BM_isAllowed/2/min_time:2.000           22 ns         22 ns  128000000
BM_isAllowed/4/min_time:2.000           22 ns         22 ns  137846154
BM_isAllowed/8/min_time:2.000           22 ns         22 ns  128000000
BM_isAllowed/16/min_time:2.000          22 ns         22 ns  128000000
BM_isAllowed/22/min_time:2.000          22 ns         22 ns  137846154
BM_isAllowed2/2/min_time:2.000          24 ns         24 ns  112000000
BM_isAllowed2/4/min_time:2.000          24 ns         24 ns  119466667
BM_isAllowed2/8/min_time:2.000          24 ns         24 ns  119466667
BM_isAllowed2/16/min_time:2.000         24 ns         24 ns  119466667
BM_isAllowed2/22/min_time:2.000         24 ns         24 ns  119466667

注意:正如Martin Bonner指出的那样,isAllowed函数似乎存在逻辑缺陷,不要将其用于生产代码。

您可以在下面找到我用于执行此基准测试的代码,如果其中有任何缺陷,请告诉我,因为我不熟悉Google库。

重要的是,此代码是使用 Visual Studio 2015 编译的,为我们要测试的部分禁用优化。

#include "benchmark/benchmark.h"
using namespace std;
using namespace benchmark;
#pragma optimize( "[optimization-list]", {on | off} )
volatile const long extraDay = 24 * 60 * 60;
#pragma optimize( "", off )
struct timing {
short hour;
short minute;
timing(short hour, short minute) : hour(hour), minute(minute) {}
};
static void BM_isAllowed(benchmark::State& state) {
while (state.KeepRunning())
{
timing from(state.range(0), state.range(0));
timing to(state.range(0), state.range(0));
timing current(state.range(0), state.range(0));
bool b = !(((from.hour > to.hour && (current.hour >= from.hour || current.hour <= to.hour)) ||
(current.hour >= from.hour && current.hour <= to.hour)) &&
!(current.minute > from.minute && current.minute < to.minute));
}
}
static void BM_isAllowed2(benchmark::State& state) {
while (state.KeepRunning())
{
timing from(state.range(0), state.range(0));
timing to(state.range(0), state.range(0));
timing current(state.range(0), state.range(0));
bool b;
long secsFrom = secsFrom = (from.hour * 3600) + (from.minute * 60);
long secsTo = (to.hour * 3600) + (to.minute * 60);
long secsCurrent = (current.hour * 3600) + (current.minute * 60);
if (secsFrom > secsTo)
secsTo += extraDay;
if (secsCurrent > secsFrom && secsCurrent < secsTo)
b = false;
else
b = true;
}
}
#pragma optimize( "", on ) 
BENCHMARK(BM_isAllowed)->RangeMultiplier(2)->Range(2, 22)->MinTime(2);
BENCHMARK(BM_isAllowed2)->RangeMultiplier(2)->Range(2, 22)->MinTime(2);
BENCHMARK_MAIN();

这没有黄金法则。不幸的是,像这样的代码的性能是出了名的难以预测。从中得到的最重要的事情是

测量一切!

现在谈谈代码中发生的事情:正如其他人正确指出的那样,我们可以观察到isAllowed使用分支编译为函数,而isAllowed2最终是无分支的。

在谈论性能时,分支很有趣:它们介于字面上免费和昂贵的之间。这是由于称为分支预测器的 CPU 组件。它尝试预测您的控制流将采用哪个分支,并使 CPU 推测性地执行它。如果猜对了,则分支是免费的。如果它猜错了,分支很贵。在这个答案中可以找到对该概念的伟大而详细的解释,包括一些数字。

所以现在我们需要决定是要分支版本还是无分支版本。一般来说,两者都不需要比另一个快!这实际上取决于您的目标 CPU 对分支的预测能力,这当然取决于实际输入。(因此,选择是将函数编译为分支结果还是无分支结果对于编译器来说是一个难题,因为他们不知道二进制文件将在哪些 CPU 上运行,也不知道期望什么样的输入数据。例如,请参阅此博客文章。

因此,如果您的基准测试实际上是正确的我们已经确定,在您的CPU上,分支太难预测,无法击败相对便宜的整数算术。这也可能是由于测试用例的数量很少,分支预测器无法从如此少的调用中学习模式。但同样,我们不能只是以一种或另一种方式来称呼它,我们必须看看特定情况下的实际表现。


正如评论中所指出的,对于良好的测量来说,执行时间有点短,我看到我的机器有很大的偏差。有关微基准测试的信息,您可以查看此演讲,它比人们想象的要难。

此外,正如Martin Bonner所指出的那样,您的两个函数不会做同样的事情,当然,您必须修复它以获得正确的基准测试。

因为你没有测量你想要测量的东西。

事实上,在我的计算机上执行您的两个功能大约需要 40ns,但如果我使用您的测试代码,我会得到大约 500ns 的结果。

您没有执行所需的测量,因为: 1. 仅当这些函数的执行时间与实际获取时钟的函数的执行时间相同(甚至更小)时执行。作为拇指上的规则,要进行测试,请尝试测量大于 10ms 的时间。 2. 在两个即时报价之间,编译器放置了一个积极的内联和优化版本的函数,因为它知道输入是什么,这在实际情况下可能不会发生。

如果将两个函数的定义放在与定义输入的文件不同的文件中:

//is_allowed.cpp
struct timing {
short hour;
short minute;
};
bool isAllowed(timing &from, timing &to, timing &actual) {
return !(((from.hour > to.hour && (actual.hour >= from.hour || actual.hour <= to.hour)) ||
(actual.hour >= from.hour && actual.hour <= to.hour)) &&
!(actual.minute > from.minute && actual.minute < to.minute));
}
static long getSecs(short hour, short minutes) {
return (hour * 3600) + (minutes * 60);
}
bool isAllowed2(timing &from, timing &to, timing &current) {
long secsFrom = getSecs(from.hour, from.minute);
long secsTo = getSecs(to.hour, to.minute);
long secsCurrent = getSecs(current.hour, current.minute);
if (secsFrom > secsTo) secsTo += 24 * 60 * 60;
if (secsCurrent > secsFrom && secsCurrent < secsTo) {
return false;
}
return true;
}

然后在"即时报价"之间执行一百万次你的函数,你会得到一个更可靠的结果:

int main(){
//...
high_resolution_clock::time_point t1 = high_resolution_clock::now();
for (int x=1;x<1000000;++x)
isAllowed(from[j], to[j], current[i]);
high_resolution_clock::time_point t2 = high_resolution_clock::now();
high_resolution_clock::time_point t3 = high_resolution_clock::now();
for (int x=1;x<1000000;++x)
isAllowed2(from[j], to[j], current[i]);
high_resolution_clock::time_point t4 = high_resolution_clock::now();
long ns1 = duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
totalNs1 += ns1;
long ns2 = duration_cast<std::chrono::nanoseconds>(t4 - t3).count();
totalNs2 += ns2;
//            std::cout << j + 1 << "tt:1:" << ns1 << "ns: " << response << (response == ergValues[i][j] ? okay : error) << "tt:2:" << ns2 << "ms: " << response2 << (response2 == ergValues[i][j] ? okay : error) << "tt"
//                << (ergValues[i][j] ? receive : notReceive) << std::endl;
//...
}

惊喜,我得到:

totalNs1=38085793  //(38ms)
totalNs2=52182920  //(52ms)

当您使用完全相同的计算机和编译器的代码时,我得到了:

totalNs1 = 927
totalNs2 = 587

正如您所料,isAllowed的第一个版本实际上是赢家!

最新更新