前段时间,我问了一个关于堆栈溢出的问题,并展示了如何在 C++ 中执行 rdtsc 操作码。我最近使用 rdtsc 创建了一个基准函数,如下所示:
inline unsigned long long rdtsc() {
unsigned int lo, hi;
asm volatile (
"cpuid \n"
"rdtsc"
: "=a"(lo), "=d"(hi) /* outputs */
: "a"(0) /* inputs */
: "%ebx", "%ecx"); /* clobbers*/
return ((unsigned long long)lo) | (((unsigned long long)hi) << 32);
}
typedef uint64_t (*FuncOneInt)(uint32_t n);
/**
time a function that takes an integer parameter and returns a 64 bit number
Since this is capable of timing in clock cycles, we won't have to do it a
huge number of times and divide, we can literally count clocks.
Don't forget that everything takes time including getting into and out of the
function. You may want to time an empty function. The time to do the computation
can be compute by taking the time of the function you want minus the empty one.
*/
void clockBench(const char* msg, uint32_t n, FuncOneInt f) {
uint64_t t0 = rdtsc();
uint64_t r = f(n);
uint64_t t1 = rdtsc();
std::cout << msg << "n=" << n << "\telapsed=" << (t1-t0) << '\n';
}
因此,我假设如果我对一个函数进行基准测试,我将(大致)拥有它执行所需的时钟周期数。我还假设如果我想减去进入或退出函数所需的时钟周期数,我应该对一个空函数进行基准测试,然后在里面编写一个包含所需代码的函数。
这是一个示例:
uint64_t empty(uint32_t n) {
return 0;
}
uint64_t sum1Ton(uint32_t n) {
uint64_t s = 0;
for (int i = 1; i <= n; i++)
s += i;
return s;
}
代码是使用编译的
g++ -g -O2
我可以理解是否由于中断或其他条件而出现错误,但鉴于这些例程很短,并且 n 被选择得很小,我假设我可以看到实数。但令我惊讶的是,这是连续两次运行的输出
empty n=100 elapsed=438
Sum 1 to n=100 elapsed=887
empty n=100 elapsed=357
Sum 1 to n=100 elapsed=347
始终如一的空函数表明它需要的方式比它应该的要多。
毕竟,进出函数只涉及几条指令。真正的工作是在循环中完成的。不要介意差异巨大的事实。在第二次运行中,空函数声称需要 357 个时钟周期,而总和需要更少,这很荒谬。
怎么了?