【问题标题】:What can cause a program to run much faster the second time?什么可以导致程序第二次运行得更快?
【发布时间】:2011-09-26 21:10:01
【问题描述】:

我在测试我编写的代码时注意到的一点是,长时间运行的操作往往在第一次运行程序时比在后续运行时运行更多,有时是 10 倍或更多的。显然这里存在某种冷缓存/热缓存问题,但我似乎无法弄清楚它是什么。

这不是 CPU 缓存,因为这些长时间运行的操作往往是我向其中提供大量数据的循环,并且它们应该在第一次迭代后完全加载。 (另外,卸载和重新加载程序应该会清除缓存。)

另外,它不是磁盘缓存。我已经通过预先从磁盘加载所有数据并在之后进行处理来排除这种情况,而实际的 CPU 密集型数据处理速度很慢。

那么是什么导致我的程序在我第一次运行时运行缓慢,但如果我关闭它并再次运行它,它的运行速度会大大加快?我在几个不同的程序中看到了这一点,它们做的事情非常不同,所以这似乎是一个普遍的问题。

编辑:为澄清起见,我正在用 Delphi 编写,但我并不认为这是特定于 Delphi 的问题。但这意味着无论问题是什么,它都与 JIT 问题、垃圾收集问题或托管代码带来的任何其他包袱无关。而且我不是在处理网络连接。这是纯 CPU 密集型处理。

一个例子:一个脚本编译器。它是这样运行的:

  • 将整个文件从磁盘加载到内存中
  • 将整个文件编入令牌队列
  • 将队列解析成树
  • 在树上运行 codegen 以生成字节码

如果我在将整个内容从磁盘加载到内存后为其提供一个巨大的脚本文件(约 100k 行),则第一次运行时 lex 步骤大约需要 15 秒,后续运行需要 2 秒。 (是的,我知道这还有很长的时间。我正在努力解决这个问题......)我想知道这种放缓来自哪里以及我能做些什么。

【问题讨论】:

  • 你确定不是I/O缓存?对我来说,这听起来像是操作系统的 I/O 缓存层。 CPU 计算没有缓存,所以它必须在某处存储...
  • @David 查看第一个答案以获取反例。我可以为其他语言添加更多内容。不要假设。
  • 需要 15 秒来对存储在内存中的 100 kb 文件进行 lex 处理,这非常长。先找出它为什么这么慢,然后再弹出它更快的原因。顺便说一句,注意页面错误。
  • 三个想法:1)病毒扫描程序? 2)您是否尝试过在没有 IDE 的情况下运行可执行文件? 3) 你是否使用了一些可能只在第一次加载的 DLL?
  • Gamecats 病毒扫描点是一个非常好的。许多病毒扫描程序只会在应用程序第一次运行时对其进行全面检查。其他时候,他们可能只是对文件进行哈希检查以确认它没有被更改。 @Gamecat - 你应该把它放在一个答案中。

标签: windows performance delphi caching


【解决方案1】:

尝试三件事:

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

【讨论】:

    【解决方案2】:

    即使(尤其是)非常小的命令行程序,问题也可能是加载进程、链接到动态链接库等所花费的时间。我相信现代操作系统会避免重复很多这样的工作,如果同一程序一次或重复运行两次。

    我也不会这么轻易地关闭 CPU 缓存。 0 级缓存与内部循环非常相关,但对于同一应用程序的第二次运行而言则更不重要。在我便宜的 Athlon 2 X4 645 系统上,每个核心有 64K + 64K(数据 + 指令)0 级高速缓存 - 并不是一个巨大的内存量。一级缓存是每个内核的 IIRC 512K,因此不太可能被启动新的程序运行所需的 O/S 代码、调用操作系统服务和标准库等弄脏以完成无关。二级缓存(在有它的 CPU 上 - 我的 Athlon 2 没有,IIRC)仍然更大,主板/芯片组可能提供更高级别和更大的缓存。

    至少还有另一种缓存——分支预测表。虽然我原以为它们会比 0 级缓存更快地被污染到无关紧要。

    我通常发现单元测试程序第一次运行时要慢很多倍。但是,程序越大越复杂,效果越不显着。

    一段时间以来,应用程序的性能通常被认为是不确定的。尽管严格来说并非如此,但性能是由许多难以预测的因素决定的,因此它是一个很好的模型。例如,如果 CPU 有点热,则可能会降低时钟速度以防止过热。并且温度在芯片的不同部分发生变化,变化以复杂的方式在芯片上传导。随着时钟速度的变化和不同代码段的不同需求改变了温度变化的模式,很明显有可能出现混沌(如混沌理论)行为。

    在某些平台上,如果程序的第一次运行让处理器在“快速”(而不是凉爽/安静)模式下运行,我不会感到惊讶,这意味着第二次运行的开始会受益从那个速度提升以及结束。但是,这将是一个棘手的问题 - 它必须是一个 CPU 密集型程序,如果您的冷却不足,处理器可能会再次减速以避免过热。

    【讨论】:

    • 我这里没有计算加载时间。这都是 GUI 驱动的,所以我可以确保在所有数字运算开始之前加载所有内容。
    • 我不得不承认,再想想,从 2 秒膨胀到 15 秒对于这些效果来说似乎是不可信的。
    • 恕我直言,CPU 缓存和分支预测/管道处理在这里不相关,因为代码始终相同。
    • @Arnaud - 代码相同但性能不同 - 这正是您查看 CPU 缓存、分支预测等问题的时候。如果代码已经在 CPU 指令缓存中,那么代码将比没有运行时更快。如果表中仍有相关的分支信息,则分支预测会更准确,并且该代码将比没有的情况下运行得更快。
    • WITHOUT 任何形式的缓存(并忽略一些技术细节,如初始硬盘磁头位置,并假设程序完全控制机器),所有程序都将在每次使用相同的数据运行它们时,它们的时间完全相同。曾几何时,可以确定精确的运行时间 - 到时钟周期 - 因为需要考虑的缓存很少(如果有的话),没有多任务处理等。
    【解决方案3】:

    我猜这是你所有的库/DLL。这些通常在运行时按需加载,因此您的程序第一次运行时操作系统必须从磁盘中读取它们。但是,一旦读取,它们将保持加载状态,除非您的系统开始内存不足。因此,如果您连续多次运行同一个程序,第一次运行会首当其冲,而其他运行则受益于预加载的库。

    【讨论】:

    • 不,这是从光盘加载所有内容后发生的所有事情。我确保考虑到这一点。
    • 您的代码使用了多少临时空间?也许问题不是被分页in,也许是被分页out 以腾出空间。一旦它被调出并且您的程序退出,系统将有大量可用内存用于后续运行。您是否注意到您的浏览器或其他程序在运行代码后第一次访问它们时是否倾向于“犹豫”?
    【解决方案4】:

    我通常遇到相反的情况:对于计算密集型工作(如果防病毒不起作用),我在调用之间只有 5-10% 的差异。例如,为我们的框架运行的 6,000,000 次回归测试的运行时间非常固定,而且这是非常耗费磁盘和 CPU 的工作。

    我真的不相信 CPU 缓存或流水线/分支预测问题,因为正如您所写,处理的数据和代码似乎是一致的。如果杀毒关闭,可能与操作系统线程设置有关:您是否尝试更改进程 CPU 亲和性和优先级?

    这应该非常特定于您正在运行的进程。如果没有任何实际的源代码来重现它,几乎不可能告诉你发生了什么。有多少个线程?什么是硬件配置(那里没有任何英特尔 CPU 提升 - 您使用的是笔记本电脑,您的能源设置是什么)?是否使用 CPU/FPU/MMX/SSE2(例如 MMX 和 FPU 不混合)?它是移动大量数据,还是处理一些现有数据?您的软件是否依赖于外部库(甚至某些 Windows 库可能需要一些时间来初始化)?您如何使用内存(您是否尝试预先分配内存;或者在多线程应用程序中,您是否尝试使用 scaling MM 而不是 FastMM4)?

    我认为使用示例分析器可能没有太大帮助,因为它会改变一般 CPU 内核的使用,但在所有情况下都值得尝试。我最好依靠日志分析 - 参见例如this class 或者您可以编写自己的时间戳来查找应用中时间变化的位置。

    AFAIK 一直写道,在进行基准测试时,绝不应考虑应用程序的首次运行。如今的计算机系统如此复杂,以至于第一次,所有的内部(SW 和 HW)管道都将被清洗——所以当你旅行 1 个月回来时,你不能喝第一口从水龙头流出的水。 ;)

    【讨论】:

      【解决方案5】:

      我能想到的其他因素是内存对齐(以及随后的缓存行填充),但是说有两种类型:完美对齐(最快)和不完美(更慢),人们会认为它会不规则地发生(取决于内存的布局方式)。

      也许它与物理页面布局有关?据我所知,每次内存访问都通过 MMU 页表条目,因此分散的物理页面可能比连续页面慢。 (只是一个疯狂的猜测,这个)

      我还没有提到的另一件事是,您的进程在哪个内核上运行 - 特别是在超线程 CPU 上,在两个内核中较慢的内核上运行可能会产生负面影响。尝试为每次运行在同一个内核上设置处理器关联掩码,看看这是否会影响测量到的第一次和后续运行之间的运行时差异。

      顺便问一下,您如何定义“首次运行”?可能是您刚刚编译了可执行文件吗?在这种情况下(我只是在这里再次猜测),某些进程(操作系统、病毒扫描程序甚至某些 root 工具包)可能正忙于分析您的可执行文件的行为,一旦可执行文件被跳过,这些行为可能会被跳过之前分析过。您可以尝试通过在运行之间更改可执行文件的一些随机不重要字节来证明这一点,看看这是否会再次对运行时产生负面影响?

      一旦找出原因,请发布摘要 - 这也可能对其他人有所帮助。干杯!

      【讨论】:

        【解决方案6】:

        只是一个随机猜测......

        您的处理器是否支持自适应频率?也许只是处理器在第一次运行时没有时间调整其频率,而在第二次运行时正在全速运行。

        【讨论】:

        • AFAIK 即使是旧的 AMD Athlon 64 也可以每秒改变 30 次频率,所以这并不能解释 13 秒的差距。
        【解决方案7】:

        有很多事情会导致这种情况。举个例子:如果您使用ADO.NET 进行数据访问并打开连接池(这是默认设置),那么您的应用程序第一次运行时会创建数据库连接。当您的应用程序关闭时,连接由ADO.NET 保持在其打开状态,因此下次您的应用程序运行并进行数据访问时,它不必承受实例化连接的影响,因此会显得更快。

        【讨论】:

          【解决方案8】:

          如果我错了,你可能会忽略我的大部分想法......

          连接池、JIT 编译、反射、IO 缓存不胜枚举……

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

          您可以尝试对您的程序集进行更新,因为这会删除 JIT 编译。

          【讨论】:

          • 抱歉,用 Delphi 编写。没有托管字节码的东西在这里造成麻烦。
          【解决方案9】:

          减速来自哪里以及我能做些什么。

          我会谈论下一次的快速执行可以来自性能缓存

          • 磁盘内部缓存(8MB 或更多)
          • Windows 应用程序依赖项(作为 DLL)/核心缓存
          • CPU 缓存 L3(如果某些编程循环足够小,则为 L2)

          所以你第一次看到你并没有从这些缓存系统中受益。

          【讨论】:

            猜你喜欢
            • 2014-10-09
            • 2020-01-02
            • 1970-01-01
            • 2013-03-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-04-06
            • 1970-01-01
            相关资源
            最近更新 更多