提问人:Sanku 提问时间:6/27/2020 最后编辑:Brendan BurkhartSanku 更新时间:7/14/2022 访问量:1635
使用一个循环与两个循环
Using one loop vs two loops
问:
我正在阅读这个博客:- 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; }
答:
第二个在你的情况下更快的原因(我认为这在任何机器上都不起作用)是在点上更好的 cpu 缓存,你的 cpu 有足够的缓存来存储数组、你的操作系统需要的东西等等,第二个功能可能会比第一个慢得多。 从性能的角度来看。我怀疑如果有足够的其他程序也在运行,两个循环代码是否会提供更好的性能,因为第二个函数的效率显然比第一个更差,如果缓存了足够多的其他东西,性能铅抛缓存将被消除。
评论
TL;DR:循环基本上是一样的,如果你看到的是差异,那么你的测量是错误的。性能测量,更重要的是,对性能的推理需要大量的计算机知识、一些科学严谨性和大量的工程敏锐度。现在是长版......
不幸的是,您链接到的文章以及此处的答案和一些评论中有一些非常不准确的信息。
让我们从文章开始。不会有任何磁盘缓存对这些函数的性能产生任何影响。确实,当对物理内存的需求超过可用内存时,虚拟内存会分页到磁盘,但对于涉及 1.6MB 内存 (4 * 4 * 100K) 的程序,这不是您必须考虑的因素。
如果分页发挥作用,性能差异也不会很微妙。如果将这些阵列分页到磁盘并返回,则对于最快的磁盘,性能差异将达到 1000 倍,而不是 10% 或 100%。
分页和页面错误及其对性能的影响既不是微不足道的,也不是直观的。你需要阅读它,并认真地尝试它。那篇文章所包含的信息很少,完全不准确,以至于具有误导性。
第二个是你的分析策略和微基准本身。显然,对于如此简单的数据操作(添加),瓶颈将是内存带宽本身(可能是指令停用限制或类似的东西,具有如此简单的循环)。而且,由于您只线性读取内存,并且使用您读取的所有内存,无论是在 4 个交错流中还是在 2 个中,您都在利用所有可用的带宽。
但是,如果在循环中调用 or,则将根据 N 测量内存层次结构中不同部分的带宽,从 L1 一直到 L3 和主内存。(您应该知道计算机上所有缓存级别的大小,以及它们的工作原理。如果您知道 CPU 缓存的工作原理,这是显而易见的,否则就会让人感到困惑。您想知道第一次执行时、数组很冷时有多快,还是要测量热访问?function1
function2
您的实际用例是否一遍又一遍地复制相同的中型阵列?
如果不是,它是什么?你在标杆做什么?你是想测量一些东西还是只是在做实验?
难道不应该测量循环中最快的运行速度,而不是平均值,因为这可能会受到(基本上是随机的)上下文切换或中断的巨大影响?
您是否确保使用了正确的编译器开关?你有没有查看生成的汇编代码,以确保编译器没有添加调试检查,也没有优化它不应该优化的东西(毕竟,你只是在执行无用的循环,而优化编译器只想避免生成不需要的代码)。
您是否查看过硬件的理论内存/缓存带宽数字?您的特定 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
最后,正如所写,这两个函数具有非常相似的性能。将两者分开真的很难,除非你能在实际用例中衡量真正的差异,否则我会说写任何一个让你更快乐的。
如果你真的真的想要它们之间的理论差异,我会说具有两个独立循环的循环会稍微好一点,因为交错访问不相关的数据通常不是一个好主意。
评论
There won't be no disk caching
There wouldn't be any disk caching
perf
这与缓存或指令效率无关。长向量的简单迭代纯粹是带宽问题。(谷歌:流基准。现代 CPU 有足够的带宽来满足并非所有内核,但可以满足很多。
因此,如果将这两个循环组合在一起,在单个内核上执行它们,则可能有足够的带宽以内存可以承受的速率进行所有负载和存储。但是,如果使用两个循环,则不会使用带宽,并且运行时间将略少于两倍。
在这里,我只想说一些在研究性能时需要记住的事情——除非你正在为实时设备编写嵌入式软件,否则像这样的低级代码的性能不应该是一个问题。
在 99.9% 的所有其他情况下,它们将足够快。
评论