什么原因会导致程序第二次运行更快?

30
我注意到当我测试所写的代码时,长时间运行的操作在第一次运行程序时比后续运行要慢得多,有时甚至慢了10倍以上。显然这里涉及到某种冷缓存/热缓存问题,但我似乎无法弄清楚是什么问题。
这不是CPU缓存,因为这些长时间运行的操作往往是循环,我向其中输入大量数据,并且它们在第一次迭代后应该被完全加载(此外,卸载和重新加载程序应该会清除缓存)。
而且,这也不是磁盘缓存。我通过预先从磁盘加载所有数据并在处理之后进行了排除,并且实际上是CPU密集型的数据处理在缓慢执行。
那么是什么原因导致我的程序在第一次运行时运行缓慢,但如果我关闭它并再次运行,则运行速度会显着提高呢?我在几个不同的程序中都看到过这种情况,这似乎是一个普遍性问题。
编辑:为了澄清,我是用Delphi编写的,尽管我不认为这是一个特定于Delphi的问题。但这意味着无论问题是什么,它都与JIT问题、垃圾回收问题或托管代码带来的任何其他问题都无关。而且我没有涉及网络连接。这是纯CPU密集型处理。
一个例子:脚本编译器。它的运行方式如下:
- 将整个文件从磁盘加载到内存中。 - 将整个文件分析为令牌队列。 - 将队列解析成树。 - 在树上运行codegen以生成字节码。
如果我向其输入一个巨大的脚本文件(~100k行),在将整个文件从磁盘加载到内存后,第一次运行时Lex步骤大约需要15秒,而后续运行则只需2秒。(是的,我知道那还是很长时间。我正在努力改进...)我想知道这种减速来自哪里以及我可以怎么做。

7
你确定不是I/O缓存吗?听起来像是操作系统的I/O缓存层。CPU计算并没有被缓存,所以一定是存储中的某些东西…… - John Gietzen
4
请查看第一个答案以获取反例。我可以添加其他语言的反例。不要做出假设。 - Ed Staub
2
需要15秒来对存储在内存中的100 kb文件进行词法分析,这太长了。首先找出为什么速度如此慢,然后就会发现为什么它更快。顺便注意页面错误。 - Hans Passant
12
三个问题:1)病毒扫描器?2)您尝试过在没有集成开发环境的情况下运行可执行文件吗?3)您是否使用某些DLL,这些DLL可能仅在第一次加载时加载? - Toon Krijthe
2
Gamecats病毒扫描器的观点非常好。很多病毒扫描器只会在第一次运行应用程序时进行完整检查。其他时间,它们可能只会对文件进行哈希检查以确认其未被更改。@Gamecat-你应该把这个放在答案中。 - user180247
显示剩余14条评论
10个回答

13

三件事情可以尝试:

  • 在采样分析器中运行它,包括“冷”运行(重新启动后的第一件事)。通常应该足够了。
  • 检查内存使用情况,是否增长得如此之高(即使是暂时的),操作系统也必须将东西交换出RAM以为您的应用程序腾出空间?这本身就可能是您所看到的现象的原因。还要查看您启动应用程序时有多少可用RAM。
  • 启用系统性能工具并检查I/O计数器或文件访问,并确保在FileMon / Process Explorer下没有您已经忘记的某些文件或网络访问(剩余日志/测试代码)

5
即使是非常小的命令行程序,问题也可能在于加载进程所需的时间、链接动态链接库等。我相信现代操作系统会避免重复进行大量的工作,如果同一个程序同时运行两次或多次,则不需要再次执行这些工作。
我不会轻易忽略CPU缓存。对于内部循环来说,0级缓存非常重要,但对于同一应用的第二次运行则不那么重要了。在我的廉价Athlon 2 X4 645系统上,每个核心有64K + 64K(数据+指令)的0级缓存 - 不算太多的内存。一级缓存是512K每个核心,因此不太可能被操作系统代码、调用操作系统服务和标准库等所完全覆盖而变得无关紧要。二级缓存(在有的CPU上)更大,主板/芯片组还可能提供一些更高级别和更大容量的缓存。
至少还有一种缓存 - 分支预测表。虽然我认为它们可能比0级缓存更快就失效了。
通常情况下,我发现单元测试程序的首次运行速度要慢许多。但是,程序越大、越复杂,这种现象就越不显著。
长期以来,应用程序的性能通常被认为是非确定性的。虽然这不是严格意义上的真相,但性能受到许多难以预测的因素的影响,这是一个很好的模型。例如,如果CPU有点热,时钟速度可能会降低以防止过热。温度在芯片的不同部位变化,而这些变化以复杂的方式传导到整个芯片中。随着时钟速度的变化和不同代码段的不同需求改变了温度变化的模式,就会产生明显的混沌(类似于混沌理论)行为。
在某些平台上,如果第一次运行程序时处理器以“快速”(而不是冷静/安静)模式运行,我不会感到惊讶,这意味着第二次运行的开头也会受益于该速度提升以及结束时的速度。然而,这将是一个棘手的问题 - 它必须是一个需要高CPU占用率的程序,并且如果您的冷却不足,则处理器可能会再次减速以避免过热。

1
我这里不计算加载时间。这是完全由GUI驱动的,因此在所有数字计算开始之前,我可以确保所有内容都已加载完成。 - Mason Wheeler
1
我必须承认,仔细想想,从2秒到15秒的膨胀对于这些效果来说似乎不太可信。 - user180247
2
在我看来,CPU缓存和分支预测/流水线处理在这里并不相关,因为代码始终是相同的。 - Arnaud Bouchez
1
@Arnaud - 代码相同但性能不同 - 这正是你需要查看CPU缓存、分支预测等问题的时候。如果代码已经在CPU指令缓存中,则该代码运行速度比未缓存的代码更快。如果表中仍有相关分支信息,则分支预测将更准确,该代码运行速度也会比未缓存的代码更快。 - user180247
1
如果没有任何缓存(忽略一些技术细节,如初始硬盘磁头位置,并假设程序完全控制机器),所有程序每次运行时都会以完全相同的时间运行相同的数据。曾经有一段时间,可以确定精确的运行时间 - 到时钟周期 - 因为几乎没有需要考虑的缓存,也没有多任务处理等。 - user180247
@Steve314 没错。使用像Toro这样的“天真”操作系统,或在自己的扩展模式下运行程序(例如使用DWPL - 我已经做过了,非常酷),您可以尝试CPU缓存对性能的巨大影响。但是现代操作系统比那要先进得多。CPU缓存绝对不会在这里造成巨大的时间差异,但是一些其他后台任务,例如库初始化等可能会有所影响。 - Arnaud Bouchez

5
我猜想这与你的所有库/DLL有关。这些通常在运行时按需加载,因此当你的程序第一次运行时,操作系统将不得不从磁盘上读取它们。一旦读取后,它们将保持加载状态,除非你的系统开始内存不足。因此,如果你连续运行相同的程序几次,第一次运行需要承受大部分的加载时间,其他运行则可以受益于预加载的库。

1
不,这些都是在从磁盘加载完所有内容后发生的事情。我确保考虑到了这一点。 - Mason Wheeler
1
你的代码使用了多少临时空间?也许问题不在于页面调入的内容,而是需要通过页面调出来腾出空间。一旦它被页面调出并且程序退出,系统会有大量可用的内存供后续运行使用。你是否注意到在运行你的代码后第一次访问浏览器或其他程序时会出现“犹豫”现象? - TMN

4
通常情况下,如果防病毒软件没有运行,对于计算密集型工作,我通常会遇到相反的情况:在调用之间只有5-10%的差异。例如,我们框架运行的6,000,000个回归测试具有非常恒定的运行时间,并且这是非常需要磁盘和CPU资源的工作。
我真的不认为这是CPU缓存或流水线/分支预测问题,因为正如您所写的,处理的数据和代码似乎都是一致的。如果关闭防病毒软件,可能与操作系统线程设置有关:您是否尝试更改进程CPU亲和性和优先级?
这应该非常针对您正在运行的进程。如果没有任何实际的源代码可以重现它,几乎不可能告诉您发生了什么。有多少个线程?硬件配置是什么(那里没有任何英特尔CPU提升吗 - 您使用笔记本电脑,您的能源设置是什么)?它是否使用CPU / FPU / MMX / SSE2(例如,MMX和FPU不混合)?它是否移动了大量数据,还是处理了一些现有数据?您的软件是否依赖外部库(即使某些Windows库也可能需要一些时间来初始化)?您如何使用内存(您是否尝试预先分配内存;或者在多线程应用程序中,您是否尝试使用scaling MM而不是FastMM4)?
我认为使用示例分析器可能没有太大帮助,因为它会改变CPU核心的一般使用情况,但在所有情况下都值得尝试。我更倾向于依靠日志分析 - 例如,请参见此类,或者您可以编写自己的时间戳以查找应用程序中的时序更改。
据我所知,一直都说,在基准测试时,不应将应用程序的第一次运行考虑在内。现在的计算机系统非常复杂,第一次运行时,所有内部(软件和硬件)管道都必须被清除 - 因此当您从1个月的旅行中回来时,不应饮用您的水龙头中的第一口水。;)

2
我能想到的其他因素可能是内存对齐(以及随后的缓存行填充),但假设有两种类型:完美对齐(最快)和不完美对齐(较慢),人们会预期它会不规则地发生(取决于内存的布局)。
也许这与物理页面布局有关?据我所知,每个内存访问都通过MMU页表条目进行,因此离散的物理页面可能比连续的页面慢。(这只是一个猜测)
还有一件事情我还没有看到提到过,就是进程运行在哪个核心上 - 特别是在超线程CPU上,运行在两个核心中较慢的那个上可能会产生负面影响。尝试为每次运行设置处理器亲和性掩码,并查看它是否影响了第一次和后续运行之间的测量运行时间差异。
顺便说一句 - 你如何定义“第一次运行”?可能你刚刚编译了可执行文件吗?在这种情况下(我只是猜测),某些进程(无论是操作系统、病毒扫描器还是甚至某些root-kit)可能正在忙于分析你的可执行文件的行为,一旦可执行文件被分析过,这个过程可能会被跳过。你可以尝试在运行之间更改一些随意的不重要的字节,看看它是否再次对运行时产生负面影响?
请在找到原因后发布摘要 - 这可能也有助于其他人。干杯!

2

只是随机猜测...

你的处理器是否支持自适应频率?也许只是因为第一次运行处理器没有时间去适应它的频率,所以在第二次运行时以全速运行。


2
据我所知,即使是旧的AMD Athlon 64处理器也可以每秒更改频率30次,因此这并不能解释13秒的间隔。 - The_Fox

1

有很多因素可能会导致这种情况发生。举个例子:如果你正在使用打开连接池(默认情况下)的 ADO.NET 进行数据访问,那么当你的应用程序第一次运行时,它将花费时间创建数据库连接。当你的应用程序关闭时,ADO.NET 会保持连接处于打开状态,因此下一次运行并进行数据访问时,它将不必实例化连接,从而看起来更快。


1

猜测您正在使用 .net,如果我错了,您可以忽略我的大部分想法...

连接池、JIT 编译、反射、IO 缓存等等,列表还有很多....

尝试测试代码的较小部分,以查看哪些部分会对性能产生最大影响...

您可以尝试对程序集进行 ngen 处理,这将消除 JIT 编译。


抱歉,我在使用Delphi编写。这里没有受到托管字节码的干扰。 - Mason Wheeler

0
“我将讲述如何实现快速执行并解决减速问题。”
  • 硬盘内部缓存(8MB或更大)
  • Windows应用程序依赖项(如DLL)/核心缓存
  • CPU缓存L3(如果某些编程循环足够小,则为L2)
“因此,您会发现第一次没有从这些缓存系统中获益。”

0

特别是如果您使用的是Python(或者NodeJS、PHP和其他在运行时编译的脚本语言),那么第二次运行程序时通常会看到显著的速度提升,因为第一次运行将创建一个已编译为字节码的缓存版本,以便在后续启动程序时更快地加载。

我正在进行一个Python项目,当我完成对其的更改并准备好让其“在生产环境中运行”时,我总会启动脚本并让其加载,直到完成加载包括和变量声明的启动阶段,然后杀死并重新启动程序,从那时起它将运行得很顺畅。

每次代码发生任何更改时,都需要重新构建缓存的字节码,因此当将任何内容放在服务器上运行或保持运行状态时,我总是在离开之前启动和重新启动,因为这种效果在各种类型的软件中都能看到。

(还有优化器可以从由TURBO Pascal / Delphi翻译的程序的运行时库中消除所有未使用的过程,整体效果相同,即初始运行后的运行将更快加载。)


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