使用一个循环与两个循环

Using one loop vs two loops

提问人:Sanku 提问时间:6/27/2020 最后编辑:Brendan BurkhartSanku 更新时间:7/14/2022 访问量:1635

问:

我正在阅读这个博客:- https://developerinsider.co/why-is-one-loop-so-much-slower-than-two-loops/。我决定使用 C++ 和 Xcode 来检查它。所以,我写了一个简单的程序,当我执行它时,我对结果感到惊讶。实际上,与第一个功能相比,第二个功能速度较慢,这与文章中所述相反。谁能帮我弄清楚为什么会这样?

#include <iostream>
#include <vector>
#include <chrono>
    
using namespace std::chrono;
    
void function1() {
    const int n=100000;
            
    int a1[n], b1[n], c1[n], d1[n];
            
    for(int j=0;j<n;j++){
        a1[j] = 0;
        b1[j] = 0;
        c1[j] = 0;
        d1[j] = 0;
    }
            
    auto start = high_resolution_clock::now();
        
    for(int j=0;j<n;j++){
        a1[j] += b1[j];
        c1[j] += d1[j];
    }
            
    auto stop = high_resolution_clock::now();
    auto duration = duration_cast<microseconds>(stop - start);
        
    std::cout << duration.count() << " Microseconds." << std::endl;  
}
    
void function2() {
    const int n=100000;
            
    int a1[n], b1[n], c1[n], d1[n];
            
    for(int j=0; j<n; j++){
        a1[j] = 0;
        b1[j] = 0;
        c1[j] = 0;
        d1[j] = 0;
    }
            
    auto start = high_resolution_clock::now();
            
    for(int j=0; j<n; j++){
        a1[j] += b1[j];
    }
    
    for(int j=0;j<n;j++){
        c1[j] += d1[j];
    }
            
    auto stop = high_resolution_clock::now();
    auto duration = duration_cast<microseconds>(stop - start);
        
    std::cout << duration.count() << " Microseconds." << std::endl;
}
        
int main(int argc, const char * argv[]) {
    function1();
    function2();
    
    return 0;
}
C++ 性能

评论

1赞 Alan Birtles 6/27/2020
您是否正在使用优化的代码?你看到什么时间?
1赞 Korosia 6/27/2020
时间有什么区别?会不会是随机波动?使它更容易显示的一种方法是在计时器内运行每个循环 1000 次,以查看第一个循环是否始终比另一个慢。
0赞 Sanku 6/27/2020
@Korosia,10 次迭代的一致性约为 300 微秒。
0赞 Jesper Juhl 4/12/2023
@SaiSankalp 10 次迭代都算不了什么。尝试一百万。

答:

-1赞 just a guy 6/27/2020 #1

第二个在你的情况下更快的原因(我认为这在任何机器上都不起作用)是在点上更好的 cpu 缓存,你的 cpu 有足够的缓存来存储数组、你的操作系统需要的东西等等,第二个功能可能会比第一个慢得多。 从性能的角度来看。我怀疑如果有足够的其他程序也在运行,两个循环代码是否会提供更好的性能,因为第二个函数的效率显然比第一个更差,如果缓存了足够多的其他东西,性能铅抛缓存将被消除。

评论

0赞 Sanku 6/27/2020
是的,这是有道理的,当你有足够的缓存时,使用两个for循环会更好。还有一件事 - 编译器是否自己进行任何这些优化,例如循环裂变或循环融合等,检查是否有足够的缓存,如果有,则不执行循环裂变,如果没有足够的缓存,则执行循环裂变等?
0赞 Jesper Juhl 4/27/2023
@SaiSankalp 编译器如何知道运行程序的机器的缓存大小?它只能(可能)知道编译程序的机器上的缓存大小 - 这可能非常不同。
7赞 yzt 4/15/2022 #2

TL;DR:循环基本上是一样的,如果你看到的是差异,那么你的测量是错误的。性能测量,更重要的是,对性能的推理需要大量的计算机知识、一些科学严谨性和大量的工程敏锐度。现在是长版......


不幸的是,您链接到的文章以及此处的答案和一些评论中有一些非常不准确的信息。

让我们从文章开始。不会有任何磁盘缓存对这些函数的性能产生任何影响。确实,当对物理内存的需求超过可用内存时,虚拟内存会分页到磁盘,但对于涉及 1.6MB 内存 (4 * 4 * 100K) 的程序,这不是您必须考虑的因素。

如果分页发挥作用,性能差异也不会很微妙。如果将这些阵列分页到磁盘并返回,则对于最快的磁盘,性能差异将达到 1000 倍,而不是 10% 或 100%。

分页和页面错误及其对性能的影响既不是微不足道的,也不是直观的。你需要阅读它,并认真地尝试它。那篇文章所包含的信息很少,完全不准确,以至于具有误导性。

第二个是你的分析策略和微基准本身。显然,对于如此简单的数据操作(添加),瓶颈将是内存带宽本身(可能是指令停用限制或类似的东西,具有如此简单的循环)。而且,由于您只线性读取内存,并且使用您读取的所有内存,无论是在 4 个交错流中还是在 2 个中,您都在利用所有可用的带宽。

但是,如果在循环中调用 or,则将根据 N 测量内存层次结构中不同部分的带宽,从 L1 一直到 L3 和主内存。(您应该知道计算机上所有缓存级别的大小,以及它们的工作原理。如果您知道 CPU 缓存的工作原理,这是显而易见的,否则就会让人感到困惑。您想知道第一次执行时、数组很冷时有多快,还是要测量热访问?function1function2

您的实际用例是否一遍又一遍地复制相同的中型阵列?

如果不是,它是什么?你在标杆做什么?你是想测量一些东西还是只是在做实验?

难道不应该测量循环中最快的运行速度,而不是平均值,因为这可能会受到(基本上是随机的)上下文切换或中断的巨大影响?

您是否确保使用了正确的编译器开关?你有没有查看生成的汇编代码,以确保编译器没有添加调试检查,也没有优化它不应该优化的东西(毕竟,你只是在执行无用的循环,而优化编译器只想避免生成不需要的代码)。

您是否查看过硬件的理论内存/缓存带宽数字?您的特定 CPU 和 RAM 组合将有理论限制。无论是 5、50 还是 500 GiB/s,它都会为您提供可以移动和处理的数据量的上限。执行单元的数量、IPC 或 CPU 的数量,以及其他几十个会影响这种微基准测试性能的数字也是如此。

如果你读取 4 个整数(每个字节 4 个字节,来自 a、b、c 和 d),然后做两次加法并写回两个结果,然后做 100'000 次,那么你 - 大致 - 查看 2.4MB 的内存读写。如果在 300 微秒内执行 10 次,则程序的内存(存储缓冲区/L1)吞吐量约为 80 GB/s。那么低吗?那么高吗?你知道吗?(你应该有一个粗略的想法。

让我告诉你,在撰写本文时,这里的另外两个答案(即这个和这个)没有意义。我无法确定第一个的正面或反面,而第二个几乎完全错误(100'000 次循环中的条件分支是坏的?分配一个额外的迭代器变量是昂贵的?对堆栈上的数组的冷访问与堆上的数组有“严重的性能影响?for

最后,正如所写,这两个函数具有非常相似的性能。将两者分开真的很难,除非你能在实际用例中衡量真正的差异,否则我会说写任何一个让你更快乐的。

如果你真的真的想要它们之间的理论差异,我会说具有两个独立循环的循环会稍微好一点,因为交错访问不相关的数据通常不是一个好主意。

评论

1赞 Elliott 4/15/2022
次要语法修正:应该是 .(我会自己编辑它,但我需要更改至少 10 个字符。There won't be no disk cachingThere wouldn't be any disk caching
0赞 glades 5/1/2023
很好读,但你忘记了指令级并行性的方面,这可以解释为什么在循环函数 2 中独立读取/写入可能比在两个单独的循环中更快(鉴于编译器不进行手动循环展开)。
0赞 yzt 5/2/2023
这是一个公平的观点,虽然不是我在那里写的那堵文字墙的重点,但它值得研究和学习。但请记住,指令解码/停用吞吐量以及通常的 IPC 速率等因素通常只有在您不再受带宽限制后才会发挥作用。一个很好的工具,可以让你将CPU性能计数器与你的程序代码(如英特尔vTune--不管它现在叫什么--或Linux工具)结合起来,可以有很大的帮助。perf
2赞 Victor Eijkhout 4/15/2022 #3

这与缓存或指令效率无关。长向量的简单迭代纯粹是带宽问题。(谷歌:流基准。现代 CPU 有足够的带宽来满足并非所有内核,但可以满足很多。

因此,如果将这两个循环组合在一起,在单个内核上执行它们,则可能有足够的带宽以内存可以承受的速率进行所有负载和存储。但是,如果使用两个循环,则不会使用带宽,并且运行时间将略少于两倍。

0赞 Rodney P. Barbati 7/14/2022 #4

在这里,我只想说一些在研究性能时需要记住的事情——除非你正在为实时设备编写嵌入式软件,否则像这样的低级代码的性能不应该是一个问题。

在 99.9% 的所有其他情况下,它们将足够快。