在 Windows Vista 和 Windows 7 上使用 HEAP_NO_SERIALIZE 堆内存函数导致 ~ 100 倍减速的原因

Posted

技术标签:

【中文标题】在 Windows Vista 和 Windows 7 上使用 HEAP_NO_SERIALIZE 堆内存函数导致 ~ 100 倍减速的原因【英文标题】:Reason for ~100x slowdown with heap memory functions using HEAP_NO_SERIALIZE on Windows Vista and Windows 7 【发布时间】:2010-12-31 07:29:39 【问题描述】:

我正在尝试追踪 Windows Vista 和 Windows 7 中堆内存功能的大幅下降(我没有在任何服务器版本上进行测试)。它根本不会在 Windows XP 上发生,只会在 Microsoft 的较新操作系统上发生。

我最初在 Windows 上编译 php 时遇到了这个问题。脚本本身似乎以预期的速度运行,但在脚本执行后,我在内部 PHP 关闭函数中遇到了 1-2 秒的延迟。启动调试后,我发现它与 PHP 内存管理器对 HeapAlloc/HeapFree/HeapReAlloc 的使用有关。

我将其追溯到在堆函数上使用标志 HEAP_NO_SERIALIZE

#ifdef ZEND_WIN32
#define ZEND_DO_MALLOC(size) (AG(memory_heap) ? HeapAlloc(AG(memory_heap), HEAP_NO_SERIALIZE, size) : malloc(size))
#define ZEND_DO_FREE(ptr) (AG(memory_heap) ? HeapFree(AG(memory_heap), HEAP_NO_SERIALIZE, ptr) : free(ptr))
#define ZEND_DO_REALLOC(ptr, size) (AG(memory_heap) ? HeapReAlloc(AG(memory_heap), HEAP_NO_SERIALIZE, ptr, size) : realloc(ptr, size))
#else
#define ZEND_DO_MALLOC(size) malloc(size)
#define ZEND_DO_FREE(ptr) free(ptr)
#define ZEND_DO_REALLOC(ptr, size) realloc(ptr, size)
#endif

和(实际上为HeapAlloc/HeapFree/HeapReAlloc设置默认值)在函数start_memory_manager中:

#ifdef ZEND_WIN32
    AG(memory_heap) = HeapCreate(HEAP_NO_SERIALIZE, 256*1024, 0);
#endif

我删除了HEAP_NO_SERIALIZE 参数(替换为0),它解决了问题。脚本现在可以在 CLI 和 SAPI Apache 2 版本中快速清理。这是针对 PHP 4.4.9 的,但 PHP 5 和 6 源代码(开发中)在调用中包含相同的标志。

我不确定我所做的是否危险。这都是 PHP 内存管理器的一部分,所以我将不得不做一些挖掘和研究,但这提出了一个问题:

为什么在带有HEAP_NO_SERIALIZE 的 Windows Vista 和 Windows 7 上堆内存功能如此缓慢?

在研究这个问题时,我想出了一个很好的结果。请阅读博文http://www.brainfarter.net/?p=69,其中的发帖人解释了问题并提供了一个测试用例(源代码和二进制文件都可用)来突出问题。

我在 Windows 7 x64 四核 8 GB 机器上的测试结果为 43,836。哎哟!没有 HEAP_NO_SERIALIZE 标志的相同结果是 655,在我的情况下,速度快了约 70 倍。

最后,使用malloc/freenew/delete 使用 Visual C++ 6 创建的任何程序似乎在这些较新的平台上都会受到影响。默认情况下,Visual C++ 2008 编译器不会为这些函数/运算符设置此标志,因此它们不会受到影响——但这仍然会使许多程序受到影响!

我鼓励您下载概念证明并尝试一下。这个问题解释了为什么我在 Windows 上的正常 PHP 安装会爬行,并且可能解释了为什么 Windows Vista 和 Windows 7 有时看起来更慢。

2010-01-26 更新: 我收到了来自 Microsoft 的回复,指出 low-fragmentation heap (LFH) 是事实上的默认策略,适用于拥有任何可观数量分配的堆。在 Windows Vista 中,他们重新组织了大量代码,以删除不再是处理堆 API 调用的常见情况的一部分的额外数据结构和代码路径。使用HEAP_NO_SERIALIZE 标志和在某些调试情况下,它们不允许使用LFH,我们会卡在通过堆管理器的缓慢且优化程度较低的路径上。所以...强烈建议不要使用 HEAP_NO_SERIALIZE,因为您将错过 LFH 的所有工作以及 Windows 堆 API 中的任何未来工作。

【问题讨论】:

糟糕,谢谢。结尾被截断了! 你可以比较两个不同操作系统下ntdll中RtlFreeHeap的代码,看看有没有明显的区别。 所以原来有 allocspeed.exe 应用程序和源的链接已经失效。我没有备份。有人有它的副本吗? :) 【参考方案1】:

我注意到的第一个区别是 Windows Vista总是 使用 低碎片堆 (LFH)。 Windows XP 似乎没有。因此,Windows Vista 中的RtlFreeHeap 要短得多——所有工作都委托给RtlpLowFragHeapFree。 More information regarding LFH 及其在各种操作系统中的存在。请注意顶部的红色警告。

More information(备注部分):

Windows XP、Windows Server 2003 和 带有修补程序 KB 816542 的 Windows 2000:

后备列表是一种快速记忆 分配机制包含 只有固定大小的块。旁观 默认情况下为堆启用列表 支持他们。 从 Windows Vista,后备列表是 未使用,LFH由启用 默认

另一个重要信息:LFH 和NO_SERIALIZE 是互斥的(不能同时激活)。结合

从 Windows Vista,后备列表是 没用过

这意味着在 Windows Vista 中设置 NO_SERIALIZE 会禁用 LFH,但根据上述内容,它不会(也不能)回退到标准的后备列表(作为快速替换)引用。我不清楚在指定NO_SERIALIZE 时Windows Vista 使用什么堆分配策略。从它的性能来看,它看起来像是在使用一些非常幼稚的东西。

更多信息:

查看allocspeed.exe 的一些堆栈快照,它似乎始终处于就绪状态(不是正在运行或等待),并且处于 HeapFree 的 TryEnterCriticalSection 中,并且将 CPU 以接近 100% 的负载稳定了 40 秒。 (在 Windows Vista 上。)

示例快照:

ntdll.dll!RtlInterlockedPushEntrySList+0xe8
ntdll.dll!RtlTryEnterCriticalSection+0x33b
kernel32.dll!HeapFree+0x14
allocspeed.EXE+0x11ad
allocspeed.EXE+0x1e15
kernel32.dll!BaseThreadInitThunk+0x12
ntdll.dll!LdrInitializeThunk+0x4d

这很奇怪,因为NO_SERIALIZE 精确地告诉它跳过锁获取。有些东西没有加起来。

这是一个只有Raymond Chen 或Mark Russinovich 可以回答的问题:)

【讨论】:

另外你是如何分析 ntdll 函数的? dumpbin /disasm ntdll.dll > dump.txt 然后gvim dump.txt 看起来后备列表的删除是由于可能的堆漏洞。见blackhat.com/presentations/bh-usa-06/BH-US-06-Marinescu.pdf 和blogs.technet.com/srd/archive/2009/08/04/… 很好的发现:删除后备列表。 接受了这个答案。优秀的研究亚历克斯。我不认为我们会从任何人那里得到比微软自己更好的答案。

以上是关于在 Windows Vista 和 Windows 7 上使用 HEAP_NO_SERIALIZE 堆内存函数导致 ~ 100 倍减速的原因的主要内容,如果未能解决你的问题,请参考以下文章

Vista 和 Windows 7 中的 OLEDB JET 错误,而不是 XP

您如何在 Windows Vista 会话 0 和桌面之间进行通信?

获取 XP、Vista 和 7 的 windows 序列号

在 Windows 2008/7/Vista 中使 Windows 服务蜂鸣

如何在 Vista 和 Windows 7 上从我的应用程序启动屏幕键盘

作为开发人员,我应该如何使用 Windows Vista(和 Windows 7)中的特殊文件夹?