Do atoi() 和 atof() cache??调用的次数越多,它们似乎执行得越快

Do atoi() and atof() cache?? They seem to execute quicker the more times called

本文关键字:执行 atof atoi cache 调用 Do      更新时间:2023-10-16

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

我使用的是Windows 7,VS2012 IDE,但使用Intel C/C++编译器v13。我启用了 -/O3 和 -/Ot("支持快速代码")。我的CPU是常春藤桥(移动)。

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

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

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

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

编辑:忘了说 - 我的程序的基本设置是从文件中解析字节的循环。在循环中,我显然使用我的atof和atoi来注意上述内容。但是,我还注意到,当我在循环之前进行调查时,只需调用 atoi 和 atof 两次,以及两次用户编写的等效函数,它似乎使循环执行得更快。循环处理 150,000 行数据,每行需要 3 倍atof()atoi() 秒。再一次,我不明白为什么在我的主循环之前调用这些函数会影响调用这些函数的程序的速度 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 拉克布 S 44.17 100

34605797 R 卡克布 100

34605799 A 囊 S 44.18 100

34605800 R NACB 100

34605800 A tacb B 44.16 100

34605801 R gacb 100

我在"R"消息的第 4 个元素和"A"消息的第 5 个元素上使用atoi(),并在"A"消息的第 4 个元素上使用atof()

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

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

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

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

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

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

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

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

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

最可能的解释是,由于您经常调用atoi/atof,因此它被标识为热点,因此保留在1级或2级处理器代码缓存中。CPU 的替换策略 - 确定在发生缓存未命中时可以清除哪些缓存行的微代码)将标记要保留在缓存中的热点。如果你有兴趣,维基百科上有一篇不错的 CPU 缓存技术文章。

您的

初始计时很低,因为您的代码尚未在 CPU 性能最高的缓存中,但曾经调用过几次。