13

我曾经_rdtsc()有时间atoi()atof()我注意到他们花了很长时间。因此,我编写了自己的这些函数版本,这些版本比第一次调用要快得多。

我使用的是 Windows 7、VS2012 IDE,但使用的是 Intel C/C++ 编译器 v13。我启用了 -/O3 和 -/Ot (“喜欢快速代码”)。我的 CPU 是 Ivy Bridge(移动)。

经过进一步调查,似乎被召唤的次数越多atoi()atof()他们执行的速度就越快??我说的幅度更快:

当我atoi()从循环外部调用一次时,它需要 5,892 个 CPU 周期,但经过数千次迭代后,这减少到 300 - 600 个 CPU 周期(相当大的执行时间范围)。

atof()最初需要 20,000 到 30,000 个 CPU 周期,然后经过几千次迭代,它需要 18 到 28 个 CPU 周期(这是我的自定义函数第一次被调用的速度)。

有人可以解释一下这种效果吗?

编辑:忘了说-我的程序的基本设置是从文件中解析字节的循环。在循环内部,我显然使用我的 atof 和 atoi 来注意上述内容。但是,我还注意到,当我在循环之前进行调查时,只调用了两次 atoi 和 atof,以及两次用户编写的等效函数,这似乎使循环执行得更快。该循环处理了 150,000 行数据,每行需要 3xatof()atoi()s。再一次,我无法理解为什么在我的主循环之前调用这些函数会影响程序调用这些函数 500,000 次的速度?!

#include <ia32intrin.h>

int main(){
    
    //call myatoi() and time it
    //call atoi() and time it
    //call myatoi() and time it
    //call atoi() and time it

    char* bytes2 = "45632";
    _int64 start2 = _rdtsc();
    unsigned int a2 = atoi(bytes2);
    _int64 finish2 = _rdtsc();
    cout << (finish2 - start2) << " CPU cycles for atoi()" << endl;
    
    //call myatof() and time it
    //call atof() and time it
    //call myatof() and time it
    //call atof() and time it
    
    
    //Iterate through 150,000 lines, each line about 25 characters.
    //The below executes slower if the above debugging is NOT done.
    while(i < file_size){
        //Loop through my data, call atoi() or atof() 1 or 2 times per line
        switch(bytes[i]){
            case ' ':
                //I have an array of shorts which records the distance from the beginning
                //of the line to each of the tokens in the line. In the below switch
                //statement offset_to_price and offset_to_qty refer to this array.

            case '\n':
                
                switch(message_type){  
                    case 'A':
                        char* temp = bytes + offset_to_price;
                        _int64 start = _rdtsc();
                        price = atof(temp);
                        _int64 finish = _rdtsc();
                        cout << (finish - start) << " CPU cycles" << endl;
                        //Other processing with the tokens
                        break;

                    case 'R':
                        //Get the 4th line token using atoi() as above
                        char* temp = bytes + offset_to_qty;
                        _int64 start = _rdtsc();
                        price = atoi(temp);
                        _int64 finish = _rdtsc();
                        cout << (finish - start) << " CPU cycles" << endl;
                        //Other processing with the tokens
                        break;
                }
            break;
        }
    }
}

文件中的行是这样的(中间没有空行):

34605792 R dacb 100

34605794 A racb S 44.17 100

34605797 R kacb 100

34605799 A sacb S 44.18 100

34605800 R nacb 100

34605800 A tacb B 44.16 100

34605801 R gacb 100

atoi()在“R”消息中的第 4 个元素和“A”消息中的第 5 个元素上使用,并在“A”消息atof()中的第 4 个元素上使用。

4

4 回答 4

7

我猜你看到atoiand有如此显着改进的原因atof是前者有大量的分支来处理所有的边缘情况。前几次,这会导致大量不正确的分支预测,代价高昂。但几次之后,预测变得更加准确。正确预测的分支几乎是免费的,这将使它们与您的简单版本竞争,后者不包括开始的分支。

缓存当然也很重要,但我认为这并不能解释为什么你自己的函数从一开始就很快,并且在重复执行后没有看到任何相关的改进(如果我理解正确的话)。

于 2013-10-12T14:24:24.817 回答
4

使用 RDTSC 进行分析是危险的。从英特尔处理器手册:

RDTSC 指令不是序列化指令。它不一定要等到所有先前的指令都已执行后再读取计数器。类似地,后续指令可以在执行读取操作之前开始执行。如果软件要求仅在本地完成所有先前指令后才执行 RDTSC,则它可以使用 RDTSCP(如果处理器支持该指令)或执行序列 LFENCE;RDTSC。

由于不可避免的海森堡效应,您现在将测量 RDTSCP 或 LFENCE 的成本。考虑改为测量一个循环。

于 2013-10-12T14:25:04.733 回答
3

不建议像这样测量单个呼叫的性能。由于功率限制、中断和其他操作系统/系统干扰、测量开销以及如上所述的冷/暖方差,您会得到太多的方差。最重要的是,rdtsc 不再被认为是一种可靠的测量方法,因为您的 CPU 可能会限制自己的频率,但为了这个简单的检查,我们可以说它已经足够好了。

您应该至少运行您的代码数千次,在开始时丢弃一些部分,然后除以得到平均值 - 这将为您提供“温暖”的性能,其中包括(如上面的评论中所述)关闭缓存代码和数据(以及 TLB)的命中延迟,良好的分支预测,并且还可能抵消一些外部影响(例如最近才将 CPU 从断电状态唤醒)。

当然,您可能会争辩说这种性能过于乐观,因为在实际场景中您不会总是命中 L1 缓存等。 - 比较两种不同的方法(例如与库 at* 函数竞争)可能仍然没问题,只是不要指望现实生活中的结果。您还可以使测试稍微困难一些,并使用更精细的输入模式调用该函数,从而更好地对缓存施加压力。

至于你关于 20k-30k 周期的问题——这正是你应该放弃前几次迭代的原因。这不仅仅是缓存未命中延迟,您实际上是在等待第一条指令进行代码提取,这也可能等待代码页翻译进行页面遍历(可能涉及多个内存访问的漫长过程) ,如果你真的不走运 - 还要从磁盘交换页面,这需要操作系统的帮助和大量的 IO 延迟。这仍然是在您开始执行第一条指令之前。

于 2013-10-12T13:59:52.983 回答
2

最可能的解释是,因为您经常调用 atoi/atof,它被识别为热点,因此被保存在 1 级或 2 级处理器代码缓存中。CPU 的替换策略——确定发生高速缓存未命中时可以清除哪些高速缓存行的微代码将标记这样的热点以保留在高速缓存中。如果您有兴趣,可以在wikipedia上写一篇不错的 cpu 缓存技术。

您的初始时间很短,因为您的代码还没有在 CPU 的性能最高的缓存中,但是一旦被调用了一些次,就可以了。

于 2013-10-12T13:32:57.547 回答