使用一个循环和两个循环的区别

4

1
你是否使用了优化的代码?你看到了什么时间? - Alan Birtles
1
时间上有什么差别吗?可能是随机波动吗?为了更容易展示,一种方法是在计时器内运行每个循环1000次,以查看第一个循环是否始终比另一个慢。 - Korosia
@Korosia 在10次迭代中,一致性大约为300微秒。 - Sai Sankalp
@SaiSankalp 进行10次迭代根本不算什么。试试一百万次。 - Jesper Juhl
4个回答

6
TL;DR:这些循环基本相同,如果你看到不同之处,那么你的测量是错误的。性能测量以及更重要的是关于性能的推理需要大量的计算机知识、一些科学严谨性和许多工程技能。

很遗憾,你链接的文章以及这里的某些答案和评论中有一些非常不准确的信息。

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

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

分页和页面故障及其对性能的影响既不是微不足道的,也不是直观的。您需要阅读相关文献,并认真进行实验。那篇文章提供的信息很少,完全不准确,甚至可能误导。

第二个是您的分析策略和微基准测试本身。显然,对于这些数据(一个加法操作)的简单操作,瓶颈将是内存带宽本身(使用这样一个简单的循环可能会有指令 retire 限制之类的问题)。由于您只线性读取内存,并使用所有读取的内容,无论是4个交错流还是2个交错流,您都利用了所有可用的带宽。

但是,如果您在循环中调用function1function2,则将根据 N 测量不同部分的内存层次结构的带宽,从 L1 到 L3 和主内存(您应该知道机器上所有缓存级别的大小和它们的工作原理)。如果您知道 CPU 缓存的工作原理,则这是显而易见的,否则就会产生困惑。您想知道第一次做这件事时速度有多快,当数组尚未被访问时,或者您想测量热访问?

你的真正用例是一遍又一遍地复制相同的中型数组吗?

如果不是,那是什么?你在进行基准测试吗?你试图测量某些东西,还是只是在做实验?

你确定你正在使用正确的编译器开关吗?你看过生成的汇编代码,以确保编译器不会添加调试检查等,并且不会优化掉不应该优化掉的东西(毕竟,你只是执行无用循环,而优化编译器最不想做的就是避免生成不需要的代码)。

你是否查看过硬件的理论内存/缓存带宽数字? 你具体的CPU和RAM组合都有理论限制。无论是5、50还是500 GiB/s,都将为你提供一个上限,告诉你可以移动多少数据并处理。这同样适用于执行单元数量、CPU的IPC以及其他几十个数字,这些数字会影响此类微基准测试的性能。
假设你正在读取4个整数(每个整数占4个字节:a、b、c和d),然后进行两次加法,并将两个结果写回,重复这个操作100,000次,那么你大致需要2.4MB的内存读取和写入。如果你在300微秒内执行10次,则程序的内存(也就是存储缓冲区/L1)吞吐量约为80 GB/s。这是否低?高?你知道吗?(你应该有个大致的想法)
我要告诉你,在撰写本文时的其他两个答案(即这个这个)都没有意义。我对第一个完全不理解,而第二个几乎完全错误(在执行100,000次的for循环中使用条件分支很糟糕?分配额外的迭代器变量会很昂贵?栈上或堆上数组的冷访问有“严重的性能影响”?)
最后,就写法而言,这两个函数的性能非常相似。真的很难区分它们,除非你可以在实际用例中测量到实际差异,否则我认为写任何一个使你更满意的就行。
如果你真的想要理论上的差异,我会说:使用两个独立的循环比较稍好,因为交错访问不相关数据通常不是一个好主意。

1
小的语法修正:There won't be no disk caching 应该改为 There wouldn't be any disk caching。(我本可以自己编辑,但需要至少更改10个字符。) - Elliott
不错的阅读,但你忘记了指令级并行性这个方面,它可以解释为什么在循环函数2中进行两次独立的读/写可能比在两个单独的循环中更快(假设编译器没有手动展开循环)。 - glades
这是一个公正的观点,虽然不是我在上面写的那堵墙的重点,但值得探究和学习。但请记住,像指令解码/退役吞吐量和通常的IPC率等事物只有在你不再受到带宽限制时才会发挥作用。一个好的工具可以让你将CPU性能计数器与你的程序代码相关联(如英特尔vTune - 现在被称为什么或Linux perf工具),这可以极大地帮助你。 - yzt

2
这与缓存或指令效率无关,纯粹是处理长向量的带宽问题。(谷歌搜索:流基准测试。)现代 CPU 的带宽足以满足不是所有核心,但是大部分核心的需求。
因此,如果你将两个循环组合在一起,在单个核心上执行,可能有足够的带宽来满足内存支持的速率下的所有加载和存储。但是,如果你使用两个循环,就会浪费带宽,运行时间会略微超过两倍。

0

我在这里插一句话,提醒大家在关注性能时要记住一些事情 - 除非你正在编写实时设备的嵌入式软件,否则此类低级代码的性能不应成为问题。

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


-1
在你的情况下第二个函数更快的原因(我不认为这适用于任何机器)是因为更好的CPU缓存,当你的CPU有足够的缓存来存储数组、操作系统所需的内容等时,第二个函数从性能角度来看可能比第一个函数慢得多。如果有足够多的其他程序运行,我怀疑这两个循环代码将提供更好的性能,因为第二个函数的效率明显比第一个函数差,如果有足够多的其他内容被缓存,通过缓存获得的性能优势将被消除。

是的,使用两个for循环当你有足够的高速缓存时更好理解。还有一件事——编译器是否会自己执行任何这些优化,比如循环分裂或循环融合等?会检查是否有足够的高速缓存,如果有,则不执行循环分裂;如果没有足够的高速缓存,则执行循环分裂等操作吗? - Sai Sankalp
@SaiSankalp 编译器怎么知道程序将在哪台机器上运行的缓存大小?它只能(可能)知道编译程序的机器上的缓存大小——这可能非常不同。 - Jesper Juhl

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接