为什么while循环的执行时间显得如此奇怪?

时间:2014-03-25 08:15:58

标签: c++

我使用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下在虚拟机中构建并运行该程序,内核版本是&#34; Linux ubuntu 2.6.32-38-generic#83-Ubuntu SMP Wed Jan 4 11:13:04 UTC 2012 i686 GNU / Linux&#34;。

1 个答案:

答案 0 :(得分:3)

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

Why isn't RDTSC a serializing instruction?

一些背景

现代X86核心有&#34;无序&#34; (OoO)执行,这意味着一旦操作数准备好并且执行单元可用,指令就被分派到能够执行指令的execution unit ......指令不一定按程序顺序执行。指令执行按程序顺序退出,因此您可以获得寄存器和内存的精确内容,以便在发生中断,异常或故障时,架构的有序执行指定。

这意味着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内存中的某些负载在缓存中未命中,并且处理器保持该内存上的加载等待但它可以继续执行独立指令,它从函数返回(或函数是< em>由编译器内联并且它在返回时看到RDTSC,因此它会调度...嘿,它不依赖于缓存中遗漏的负载而且它不是序列化,所以它是公平的游戏。 RDTSC在22个周期内退休,比发送到DRAM的缓存未命中(数百个周期)更多 ...并且突然间你报告了执行run_iter()所花费的时间。

外环测量不会受此影响,因此它可以为您提供循环中的真实总时间。

建议修复

这是一个简单的帮助器结构/类,它允许您在没有&#34;时间泄漏的情况下在各种累加器中计算时间。&#34;任何时候你打电话给&#34;拆分&#34;成员函数你必须通过引用给它一个累加器变量,它将累积前一个时间间隔:

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;
    }
};

现在你可以使用一个实例来计算&#34;分裂&#34;你的内环和另一个用于整个外环:

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 );

这是现在&#34;紧张&#34;你不会错过任何周期。但有一点需要注意,它仍然使用RDTSC代替RDTSCP,因此它不会序列化,这意味着您可能仍然报告在一次拆分中花费的时间(喜欢 iterateTime )而 over-report 一些其他累加器(如 loopTime )。在 iterateTime 中不考虑的run_iter()中的缓存未命中将在 loopTime 中计算。

注意:虚拟机的Hypervisor可能会捕获RDTSC

有一点需要注意的是,在虚拟机中,当用户级程序尝试执行RDTSC时,管理程序可能会设置控制寄存器以强制CPU出错......这将是肯定序列化执行,并将成为巨大的性能瓶颈。在这些情况下,管理程序emulates执行RDTSC并向应用程序提供虚拟时间戳。请参阅问题Weird program latency behavior on VM

最初我认为这不是你所观察到的问题,我现在想知道它是不是。如果实际上虚拟机正在捕获RDTSC,那么你必须添加硬件的开销来保存VM寄存器,调度内核/管理程序,并在&#34;修复&#34;之后恢复你的应用程序。 EDX:EAX模拟RDTSC ... 50亿次循环很长一段时间,在3 GHz时超过16秒。这可以解释为什么你有这么多时间错过...... 110亿次......(44-33)。