我有一堆在几纳秒内完成的函数,我想测量它们的执行时间以评估它们的性能。
基本思想非常简单,取一个时间戳,在循环中多次执行相同的代码块,取另一个时间戳,计算时间戳之间的差值,并将该值除以迭代次数。
这一切都非常简单,问题是当我将基准测试代码放入
main
函数中时,编译器识别出我做了相同的计算并且对结果不执行任何操作,因此它使循环无操作。所以我得到的数字非常小。
我已经设法通过伪造用法来执行代码,它可以在 Visual Studio 2022 中运行,但我无法使其在 Code::Blocks(MSYS2 g++ 编译器)中运行。
这种方法很难重用,因为每当我想对另一个函数进行基准测试时,我都必须复制粘贴相同的代码行。所以我把它变成了一个函数,这增加了可重用性,但数量却大幅增加。我的很多函数都是内联的,并且使用该函数来基准其他函数,内联优化被忽略。
最小可重现示例:
#include <array>
#include <chrono>
#include <cmath>
#include <functional>
#include <iomanip>
#include <iostream>
#include <utility>
#include <vector>
using std::array;
using std::chrono::steady_clock;
using std::chrono::duration;
using std::cout;
using std::vector;
using std::function;
double d = 0.0;
float r = 0.0;
constexpr double DU = 1.0 / (uint64_t(1) << 52);
constexpr float FU = 1.0 / (1 << 23);
constexpr array<double, 10> LOG2_POLY9 = {
-3.27179702e00,
7.19195108e00,
-7.34289702e00,
5.01474324e00,
-1.64079955e00,
-3.25941179e-01,
5.83100708e-01,
-2.58134034e-01,
5.44419681e-02,
-4.66794032e-03,
};
template <std::size_t N, std::size_t... I>
inline double apply_poly_impl(double m1, const std::array<double, N>& data, std::index_sequence<I...>) {
double s = 0;
double m = 1;
((s += std::get<I>(data) * m, m *= m1), ...);
return s;
}
template <std::size_t N>
inline double apply_poly(double m1, const std::array<double, N>& data) {
return apply_poly_impl(m1, data, std::make_index_sequence<N>{});
}
inline float fast_log2_p9(float f) {
uint32_t bits = std::bit_cast<uint32_t>(f);
int e = ((bits >> 23) & 0xff) - 127;
double m1 = 1 + (bits & 0x7fffff) * FU;
double s = apply_poly(m1, LOG2_POLY9);
return e + s;
}
template <typename T>
double timeit(const function<T(T)>& func, const vector<T>& values, int runs = 1048576){
auto start = steady_clock::now();
size_t len = values.size();
for (int64_t i = 0; i < runs; i++) {
func(values[i % len]);
}
auto end = steady_clock::now();
duration<double, std::nano> time = end - start;
return time.count() / runs;
}
int main()
{
double r4096 = 1.0 / 4096;
double n;
vector<double> random_doubles(256);
vector<float> random_floats(256);
for (int j = 0; j < 256; j++) {
n = rand() % 4096 + (rand() % 4096) * r4096;
random_doubles[j] = n;
random_doubles[j] = float(n);
}
cout << std::setprecision(16);
auto start = steady_clock::now();
for (int64_t i = 0; i < 1048576; i++) {
r = fast_log2_p9(random_floats[i % 256]);
}
auto end = steady_clock::now();
duration<double, std::nano> time = end - start;
r = fast_log2_p9(0.7413864135742188f);
cout << "fast_log_p9: " << time.count() / 1048576 << " nanoseconds\n";
cout << r << '\n';
cout << timeit<float>(fast_log2_p9, random_floats);
}
在 Visual Studio 2022 中编译,具有以下标志:
/permissive- /ifcOutput "x64\Release\" /GS /GL /W3 /Gy /Zc:wchar_t /Zi /Gm- /O2 /Ob1 /sdl /Fd"x64\Release\vc143.pdb" /Zc:inline /fp:fast /D "NDEBUG" /D "_CONSOLE" /D "_UNICODE" /D "UNICODE" /errorReport:prompt /WX- /Zc:forScope /std:c17 /Gd /Oi /MD /std:c++20 /FC /Fa"x64\Release\" /EHsc /nologo /Fo"x64\Release\" /Ot /Fp"x64\Release\exponentiation.pch" /diagnostics:column
输出:
PS C:\Users\Xeni> C:\Users\Xeni\source\repos\exponentiation\x64\Release\exponentiation.exe
fast_log_p9: 0.9078025817871094 nanoseconds
-0.4317024052143097
15.96231460571289
使用代码::块编译,命令:
g++.exe -Wall -fexceptions -fomit-frame-pointer -fexpensive-optimizations -flto -O3 -m64 --std=c++20 -march=native -fext-numeric-literals -c D:\MyScript\CodeBlocks\testapp\main.cpp -o obj\Release\main.o
g++.exe -o bin\Release\testapp.exe obj\Release\main.o -O3 -flto -s -static-libstdc++ -static-libgcc -static -m64
输出:
PS C:\Users\Xeni> D:\MyScript\CodeBlocks\testapp\bin\Release\testapp.exe
fast_log_p9: 9.5367431640625e-05 nanoseconds
-0.4317024052143097
7.897567749023438
那么如何使用易于重用的方法来测量所有优化后的代码块的执行时间?
C++ 代码的性能测量可能非常困难。
主要问题是编译器。 CPU 不执行您的 C++ 代码。他们执行CPU指令。您的 C++ 代码被转换为 CPU 指令。现代编译器作为其设计的基本部分,不会将 C++ 代码的每个片段转换为唯一的连续 CPU 指令序列。
因此,您的问题“这个函数需要多少时间”已经开始于假设该函数对应于一系列 CPU 指令,有开始和结束,中间没有其他指令。
更糟糕的是,即使编译器会输出这样的序列,现代 CPU 也不太关心。几十年来,乱序执行一直是 CPU 的标准功能。指令的结尾到底是什么?值被写回内存的时间?对于现代 CPU,这甚至可能不会发生。 CPU 核心有一个未完成写入的队列,稍后的写入可能会使较早的写入变得毫无意义。
您的基本想法是插入 C++ 基准测试代码。这真的非常干扰你的程序。您不再测量原始功能。您正在测量新功能。请注意,计时代码在功能上无关。你的乱序 CPU 可以检测到steady_clock::now();
fast_log2_p9
无关,因此它可以决定以
fast_log2_p9
开始,即使 C++ 代码说不应该。
正因为如此,这个问题的基本思想就是有缺陷的。如果您关心纳秒,则需要了解流水线、无序 CPU、缓存和内存的影响以及其他硬件现实。