为什么while循环的执行时间看起来如此奇怪

Why is the execution time of a while-loop appears so weird?

本文关键字:看起来 执行时间 while 循环 为什么      更新时间:2023-10-16

我使用rdstc()函数分别测试while循环的内外执行时间,两个结果有很大的差异。当我从外部进行测试时,结果大约是445亿次循环。当我从里面测试时,结果大约是330亿次循环。

代码段如下所示:

while(true){
beginTime = rdtsc();
typename TypedGlobalTable<K, V, V, D>::Iterator *it2 = a->get_typed_iterator(current_shard(), false);
getIteratorTime += rdtsc()-beginTime;
if(it2 == NULL) break;
uint64_t tmp = rdtsc();
while(true) {
beginTime = rdtsc();
if(it2->done()) break;      
bool cont = it2->Next();        //if we have more in the state table, we continue
if(!cont) break;
totalF2+=it2->value2();         //for experiment, recording the sum of v
updates++;                      //for experiment, recording the number of updates
otherTime += rdtsc()-beginTime;
//cout << "processing " << it2->key() << " " << it2->value1() << " " << it2->value2() << endl;
beginTime = rdtsc();
run_iter(it2->key(), it2->value1(), it2->value2(), it2->value3());
iterateTime += rdtsc()-beginTime;
}
flagtime += rdtsc()-tmp;
delete it2;                         //delete the table iterator}

我测试的while循环是内部循环。

rdstc()函数如下所示:

static uint64_t rdtsc() {
uint32_t hi, lo;
__asm__ __volatile__ ("rdtsc" : "=a"(lo), "=d"(hi));
return (((uint64_t)hi)<<32) | ((uint64_t)lo);
}

我在一个虚拟机中在Ubuntu 10.04LTS下构建并运行了这个程序,内核版本是"Linux Ubuntu 2.6.32-38-generic#83 Ubuntu SMP Wed Jan 4 11:13:04 UTC 2012 i686 GNU/Linux"。

RDTSC指令不是"serializing",请参阅此SO问题

为什么不是';RDTSC不是串行指令吗?

一些背景

现代X86内核具有"无序"(OoO)执行,这意味着一旦操作数准备就绪且执行单元可用,指令就会被调度到能够执行指令的execution unit。。。指令不一定按程序顺序执行。指令do按程序顺序退出,因此您可以获得寄存器和内存的精确内容,当发生中断、异常或故障时,按顺序执行体系结构会指定这些内容。

这意味着CPU可以自由地以任何顺序调度指令执行,以获得尽可能多的并发性并提高性能,只要它给人一种指令按顺序执行的错觉。

RDTSC指令被设计为尽可能快地执行,尽可能不具有侵入性,开销很小。它有大约22个处理器周期延迟,但您可以同时完成大量工作。

有一个新的变体,称为RDTSCP正在序列化。。。处理器按照程序顺序等待以前的指令完成,并阻止将来的指令被调度。。。从性能的角度来看,这是昂贵的。

回到你的问题

考虑到这一点,想想编译器生成了什么,处理器看到了什么。。。while(true)只是一个无条件分支,它并不是真正的执行,而是被流水线的前端,即指令解码器所消耗,它正在尽可能地提前获取,将指令塞进指令调度器,以尝试在每个周期内获得尽可能多的执行的指令。因此,循环中的RDTSC指令被调度,其他指令继续流动和执行,最终RDTSC失效,结果被转发到依赖于结果的指令(代码中的减法)。但您并没有真正精确地计时内部循环

让我们看看下面的代码:

beginTime = rdtsc();
run_iter(it2->key(), it2->value1(), it2->value2(), it2->value3());
iterateTime += rdtsc()-beginTime;

假设函数run_iter()在返回后调用rdtsc()时已经完成。但真正可能发生的是,run_iter中内存的一些加载在缓存中未命中,处理器保持该加载在内存上等待,但它可以继续执行独立的指令,它从函数返回(或函数被编译器内联),并在返回时看到RDTSC,因此它调度。。。嘿,它不依赖于缓存中丢失的负载,也不序列化,所以这是公平的游戏。RDTSC在22个周期内退役,这比进入DRAM的缓存未命中(数百个周期)快得多。。。突然之间,报告不足执行run_iter()所花费的时间。

外环测量不受此影响,因此它以周期为单位为您提供真实的总时间。

建议修复

这里有一个简单的helper结构/类,它可以让你在不发生"时间泄漏"的情况下计算各种累加器中的时间。任何时候你调用"split"成员函数,你都必须通过引用给它一个累加器变量,它将在这里累积上一个时间间隔:

struct Timer {
uint64_t _previous_tsc;
Timer() : _previous_tsc(rdtsc()) {}
void split( uint64_t & accumulator )
{
uint64_t tmp = rdtsc();
accumulator += tmp - _previous_tsc;
_previous_tsc = tmp;
}
};

现在,您可以使用一个实例来计时内部循环的"拆分",另一个实例用于整个外部循环:

uint64_t flagtime    = 0; // outer loop
uint64_t otherTime   = 0; // inner split
uint64_t iterateTime = 0; // inner split
uint64_t loopTime    = 0; // inner split
Timer tsc_outer;
Timer tsc_inner;
while(! it2->done()) {
tsc_inner.split( loopTime );
bool cont = it2->Next();        //if we have more in the state table, we continue
if(!cont) break;
totalF2+=it2->value2();         //for experiment, recording the sum of v
updates++;                      //for experiment, recording the number of updates
tsc_inner.split( otherTime );
run_iter(it2->key(), it2->value1(), it2->value2(), it2->value3());
tsc_inner.split( iterateTime );
}
tsc_outer.split( flagtime );

这是现在"紧"你不会错过任何周期。不过,需要注意的是,它仍然使用RDTSC而不是RDTSCP,因此它没有序列化,这意味着您可能仍然在report下报告在一个拆分中花费的时间(如iterateTime),而则在report上报告其他累加器(如loopTimeterateTime中计算的缓存未命中将在loopTime中计算。

注意:虚拟机的虚拟机监控程序可能正在捕获RDTSC

需要注意的一点是,在虚拟机中,当用户级程序试图执行RDTSC。。。这肯定会使执行串行化,并成为巨大的性能瓶颈。在这些情况下,系统管理程序emulates执行RDTSC,并为应用程序提供虚拟时间戳。请参阅SO问题虚拟机上奇怪的程序延迟行为。

最初我认为这不是你观察到的问题,现在我想知道是否是。如果虚拟机实际上捕获了RDTSC,那么你必须添加硬件的开销,保存VM寄存器,调度内核/系统管理程序,并在"修复"EDX:EAX后恢复你的应用程序以模拟RDTSC。。。500亿次循环是一个很长的时间,在3GHz下超过16秒。这就解释了为什么你错过了这么多时间。。。110亿周期。。。(44-33)。