内存分配函数是否指示不再使用内存内容?
Do memory allocation functions indicate that the memory content is no longer used?
在处理一些数据流时,例如来自网络的请求,使用一些临时内存是很常见的。例如,一个 URL 可能被拆分成多个字符串,每个字符串都可能从堆中分配内存。这些实体的使用通常是短暂的,内存总量通常相对较小,应该适合 CPU 缓存。
当用于临时字符串的内存被释放时,字符串内容很可能只存在于缓存中。但是,CPU 并不知道正在释放的内存:释放只是内存管理系统中的更新。因此,当 CPU 缓存用于其他内存时,CPU 可能最终将未使用的内容不必要地写入实际内存 - 除非内存释放以某种方式向 CPU 表明不再使用内存。因此,问题变成:
释放内存的内存管理函数是否以某种方式指示相应内存的内容可以被丢弃?有没有办法向 CPU 指示内存不再使用? (至少,对于某些 CPUs:显然,体系结构之间可能存在差异)由于不同的实现在质量上可能会有所不同,并且可能会或可能不会做任何花哨的事情,真正的问题是是否存在 任何内存管理实现指示内存未使用?
我确实意识到始终使用相同的内存区域可能是一种缓解策略,可以避免对实际内存进行不必要的写入。在那种情况下,将使用相同的缓存内存。类似地,内存分配很可能总是产生相同的内存,也避免了不必要的内存传输。但是,我可能不需要依赖任何适用的技术。
这在很大程度上取决于您正在使用的实现和库。分配和释放的内存往往会很快重新分配。大多数分配都是小块,比需要时写入后备存储的页面小得多。
如今,RAM 大小通常如此之大,以至于当 OS 开始将脏页写入后备存储时,无论如何都会遇到麻烦。如果您有 16 GB 的 RAM,您将不会写入 100 KB 或 1 兆字节,而是写入千兆字节,并且您的计算机会慢得像爬行一样。用户将通过不使用占用过多内存的应用程序来避免这种情况。
相当多的分配器将 "free block list" 存储在自己的空闲块中。 IE。当您调用该释放函数时,分配的块被拼接到空闲列表中,这可能意味着用前向和后向指针覆盖旧数据。这些写入至少会覆盖分配的第一部分。
分配器使用的第二种技术是积极回收内存。如果下一次分配可以与最新的释放相匹配,很可能是缓存没有刷新到主内存。
您的想法的问题在于,每个单独的写入实际上并不 那么 昂贵,并且弄清楚可以丢弃的内容需要相当多的簿记工作。实际上,您不能进行系统调用。这意味着您需要在每个应用程序中进行簿记(这是合理的:这些小块的释放通常 returns 内存给应用程序,而不是 OS)。这反过来意味着应用程序需要了解 CPU 缓存设计,这绝不是常量。应用程序甚至需要了解不同的缓存一致性方案!
你在这里问了一些相关的问题。粗体字是最容易回答的。当您使用类似通用类型的释放释放内存时,您所说的 only 是 "I don't need this any more"。您 也 含蓄地说 "I don't care what you do with it"。这个 "I don't care" 其实就是你问题的答案。你不是在说"you can discard this"。你是说"I don't care if you discard it or not"。
为了回答您关于 CPU 支持的问题,MSI protocol 是一个基本的缓存一致性协议。 I
状态代表 "invalid",这就是您可以实现所询问的 "memory not used" 状态的方式。为此,您需要创建一个具有非泛型语义的发布接口,即这种发布意味着 "This memory is no longer used and you should avoid writing it back to main memory"。请注意,此语义对通用版本没有的 CPU 的行为有要求。要实现这一点,您需要根据 CPU 缓存分配内存,然后使用可用的 CPU 指令使缓存项无效。您几乎肯定需要编写汇编代码来完成这项工作,以避免使用显式缓存管理指令会导致对内存模型的无根据(和不正确)假设。
我个人已经有一段时间不需要在这个级别上工作了,所以我不熟悉随处可用的东西,也就是说,这种技术是否可以合理地移植。 Intel CPU 有 INVLPG
指令。这里的讨论应该是您下一阶段关注的一个不错的起点:
没有
你提到的缓存操作(将缓存内存标记为未使用并丢弃而不回写到主内存)称为缓存行失效而不回写。这是通过带有操作数的特殊指令执行的,该操作数可能(或可能不)指示要失效的缓存行的地址。
在我熟悉的所有体系结构中,这条指令都是特权指令,我认为这是有充分理由的。这意味着用户模式代码不能使用该指令;只有内核可以。否则可能发生的变态欺骗、数据丢失和拒绝服务的数量令人难以置信。
因此,没有内存分配器可以执行您的建议;他们根本没有(在用户模式下)这样做的工具。
建筑支持
- x86 和 x86-64 架构有特权
invd
指令,它使所有内部缓存无效,没有写回和指示外部缓存也使自己失效。这是唯一能够在没有写回的情况下失效的指令,它确实是一把钝器。
- 非特权
clflush
指令指定了受害者地址,但在失效前会回写,所以我只是顺便提一下。
- 所有这些说明的文档都在英特尔的 SDMs 第 2 卷中。
- ARM 架构在 a write to coprocessor 15, register 7
MCR p15, 0, <Rd>, c7, <CRm>, <Opcode_2>
的情况下执行缓存失效而不回写。可以指定受害者高速缓存行。写入该寄存器是有特权的。
- PowerPC 有
dcbi
,它可以让你指定一个受害者,dci
没有,两者都有指令缓存版本,但是所有四个享有特权 (see page 1400).
- MIPS 拥有
CACHE
instruction which can specify a victim. It was privileged as of MIPS Instruction Set v5.04,但在 6.04 Imagination Technologies 中搅浑了水,不再清楚什么是特权,什么不是。
因此,这排除了在没有 flushing/writing 完全返回用户模式的情况下使用缓存失效。
内核模式?
但是,出于多种原因,我认为在内核模式下这仍然是一个坏主意:
- Linux 的分配器,
kmalloc()
,为不同大小的分配分配 arenas。特别是,它有一个舞台,每个分配大小 <=192
字节,步长为 8
;这意味着对象之间的距离可能比缓存行更近,或者与下一个缓存行部分重叠,因此使用无效可能会破坏正确位于缓存中但尚未写回的附近对象。这是错误的。
- 缓存行可能非常大(在 x86-64 上,64 字节),而且在整个缓存层次结构中大小不一定统一,这一事实使问题更加复杂。例如,Pentium 4 有 64B L1 缓存行,但有 128B L2 缓存行。
- 它使释放时间与要释放的对象的缓存行数成线性关系。
- 它的好处非常有限; L1 缓存的大小通常以 KB 为单位,因此几千次刷新将完全清空它。此外,缓存可能已经在没有您提示的情况下刷新了数据,因此您的无效比无用更糟糕:内存带宽已被使用,但缓存中不再有该行,因此下一次将其部分写入时需要重新获取。
- 下一次内存分配器 returns 阻塞时,可能很快,其用户将遭受有保证的缓存未命中并从主 RAM 中获取,同时他可能有一条脏的未刷新行或而是清洁冲洗线。保证高速缓存未命中和从主 RAM 获取的成本 比高速缓存行刷新 没有 的成本大 很多 缓存硬件。
- 循环和刷新这些行所需的额外代码会浪费指令缓存 space。
- 更好地利用上述循环使高速缓存行无效的数十个周期是继续做有用的工作,同时让高速缓存和内存子系统的相当大的带宽写回您的脏高速缓存行。
- 我的现代 Haswell 处理器具有 32 字节/时钟周期写入 L1 带宽和 25GB/s 主 RAM 带宽。我确定可以在其中的某个地方挤入几个额外的可刷新 32 字节缓存行。
- 最后,对于像这样的短暂的、小的分配,可以选择在堆栈上分配它。
实际内存分配器实践
None 其中会使内存无效,因为它们不能。为了使缓存行无效而进行系统调用会非常慢,并且会导致更多的缓存流量 in/out,这仅仅是因为上下文切换。
我不知道有任何架构愿意将其缓存一致性协议公开给软件(用户甚至内核)这样的操作。这会产生几乎无法处理的警告。
请注意,用户启动的刷新是可以接受的暴露,但绝不会威胁破坏内存一致性。
举个例子,假设您有一个缓存行,其中包含您不再需要的临时数据。由于它被写入,它在缓存中将处于 "modified" 状态。
现在你想要一种机制告诉缓存避免将它写回,但这意味着你创建了一个竞争条件 - 如果其他人在你应用这个技巧之前寻找该行,他会从核心中窥探它并且收到更新的数据。如果你的核心先行,新数据就会丢失 - 因此,内存中该地址的结果取决于竞争。
您可能会争辩说在多线程编程中经常会出现这种情况,但是这种情况也可能发生在 运行 单个线程(如果缓存是完整的,或一些较低的包含级别丢失它)。更糟糕的是,这打破了整个虚拟内存呈现为平坦的前提,缓存版本由 CPU 维护只是为了性能,但不能破坏一致性或一致性(除了一些记录的多线程情况,取决于内存排序模型,可以通过软件保护来克服)。
编辑:
如果您愿意扩展您所认为的 "memory" 的定义,您可以寻找非连贯类型的内存,它们在定义和实现上有所不同,但有些可能会提供您所寻求的。一些架构公开了“scratchpad”内存,它由用户控制并允许快速访问而没有缓存一致性的麻烦(但也没有它的好处)。有些架构甚至提供可配置的硬件,允许您 select 是否喜欢在其中缓存主内存,或者将其用作暂存器区域。
在处理一些数据流时,例如来自网络的请求,使用一些临时内存是很常见的。例如,一个 URL 可能被拆分成多个字符串,每个字符串都可能从堆中分配内存。这些实体的使用通常是短暂的,内存总量通常相对较小,应该适合 CPU 缓存。
当用于临时字符串的内存被释放时,字符串内容很可能只存在于缓存中。但是,CPU 并不知道正在释放的内存:释放只是内存管理系统中的更新。因此,当 CPU 缓存用于其他内存时,CPU 可能最终将未使用的内容不必要地写入实际内存 - 除非内存释放以某种方式向 CPU 表明不再使用内存。因此,问题变成:
释放内存的内存管理函数是否以某种方式指示相应内存的内容可以被丢弃?有没有办法向 CPU 指示内存不再使用? (至少,对于某些 CPUs:显然,体系结构之间可能存在差异)由于不同的实现在质量上可能会有所不同,并且可能会或可能不会做任何花哨的事情,真正的问题是是否存在 任何内存管理实现指示内存未使用?
我确实意识到始终使用相同的内存区域可能是一种缓解策略,可以避免对实际内存进行不必要的写入。在那种情况下,将使用相同的缓存内存。类似地,内存分配很可能总是产生相同的内存,也避免了不必要的内存传输。但是,我可能不需要依赖任何适用的技术。
这在很大程度上取决于您正在使用的实现和库。分配和释放的内存往往会很快重新分配。大多数分配都是小块,比需要时写入后备存储的页面小得多。
如今,RAM 大小通常如此之大,以至于当 OS 开始将脏页写入后备存储时,无论如何都会遇到麻烦。如果您有 16 GB 的 RAM,您将不会写入 100 KB 或 1 兆字节,而是写入千兆字节,并且您的计算机会慢得像爬行一样。用户将通过不使用占用过多内存的应用程序来避免这种情况。
相当多的分配器将 "free block list" 存储在自己的空闲块中。 IE。当您调用该释放函数时,分配的块被拼接到空闲列表中,这可能意味着用前向和后向指针覆盖旧数据。这些写入至少会覆盖分配的第一部分。
分配器使用的第二种技术是积极回收内存。如果下一次分配可以与最新的释放相匹配,很可能是缓存没有刷新到主内存。
您的想法的问题在于,每个单独的写入实际上并不 那么 昂贵,并且弄清楚可以丢弃的内容需要相当多的簿记工作。实际上,您不能进行系统调用。这意味着您需要在每个应用程序中进行簿记(这是合理的:这些小块的释放通常 returns 内存给应用程序,而不是 OS)。这反过来意味着应用程序需要了解 CPU 缓存设计,这绝不是常量。应用程序甚至需要了解不同的缓存一致性方案!
你在这里问了一些相关的问题。粗体字是最容易回答的。当您使用类似通用类型的释放释放内存时,您所说的 only 是 "I don't need this any more"。您 也 含蓄地说 "I don't care what you do with it"。这个 "I don't care" 其实就是你问题的答案。你不是在说"you can discard this"。你是说"I don't care if you discard it or not"。
为了回答您关于 CPU 支持的问题,MSI protocol 是一个基本的缓存一致性协议。 I
状态代表 "invalid",这就是您可以实现所询问的 "memory not used" 状态的方式。为此,您需要创建一个具有非泛型语义的发布接口,即这种发布意味着 "This memory is no longer used and you should avoid writing it back to main memory"。请注意,此语义对通用版本没有的 CPU 的行为有要求。要实现这一点,您需要根据 CPU 缓存分配内存,然后使用可用的 CPU 指令使缓存项无效。您几乎肯定需要编写汇编代码来完成这项工作,以避免使用显式缓存管理指令会导致对内存模型的无根据(和不正确)假设。
我个人已经有一段时间不需要在这个级别上工作了,所以我不熟悉随处可用的东西,也就是说,这种技术是否可以合理地移植。 Intel CPU 有 INVLPG
指令。这里的讨论应该是您下一阶段关注的一个不错的起点:
没有
你提到的缓存操作(将缓存内存标记为未使用并丢弃而不回写到主内存)称为缓存行失效而不回写。这是通过带有操作数的特殊指令执行的,该操作数可能(或可能不)指示要失效的缓存行的地址。
在我熟悉的所有体系结构中,这条指令都是特权指令,我认为这是有充分理由的。这意味着用户模式代码不能使用该指令;只有内核可以。否则可能发生的变态欺骗、数据丢失和拒绝服务的数量令人难以置信。
因此,没有内存分配器可以执行您的建议;他们根本没有(在用户模式下)这样做的工具。
建筑支持
- x86 和 x86-64 架构有特权
invd
指令,它使所有内部缓存无效,没有写回和指示外部缓存也使自己失效。这是唯一能够在没有写回的情况下失效的指令,它确实是一把钝器。- 非特权
clflush
指令指定了受害者地址,但在失效前会回写,所以我只是顺便提一下。 - 所有这些说明的文档都在英特尔的 SDMs 第 2 卷中。
- 非特权
- ARM 架构在 a write to coprocessor 15, register 7
MCR p15, 0, <Rd>, c7, <CRm>, <Opcode_2>
的情况下执行缓存失效而不回写。可以指定受害者高速缓存行。写入该寄存器是有特权的。 - PowerPC 有
dcbi
,它可以让你指定一个受害者,dci
没有,两者都有指令缓存版本,但是所有四个享有特权 (see page 1400). - MIPS 拥有
CACHE
instruction which can specify a victim. It was privileged as of MIPS Instruction Set v5.04,但在 6.04 Imagination Technologies 中搅浑了水,不再清楚什么是特权,什么不是。
因此,这排除了在没有 flushing/writing 完全返回用户模式的情况下使用缓存失效。
内核模式?
但是,出于多种原因,我认为在内核模式下这仍然是一个坏主意:
- Linux 的分配器,
kmalloc()
,为不同大小的分配分配 arenas。特别是,它有一个舞台,每个分配大小<=192
字节,步长为8
;这意味着对象之间的距离可能比缓存行更近,或者与下一个缓存行部分重叠,因此使用无效可能会破坏正确位于缓存中但尚未写回的附近对象。这是错误的。- 缓存行可能非常大(在 x86-64 上,64 字节),而且在整个缓存层次结构中大小不一定统一,这一事实使问题更加复杂。例如,Pentium 4 有 64B L1 缓存行,但有 128B L2 缓存行。
- 它使释放时间与要释放的对象的缓存行数成线性关系。
- 它的好处非常有限; L1 缓存的大小通常以 KB 为单位,因此几千次刷新将完全清空它。此外,缓存可能已经在没有您提示的情况下刷新了数据,因此您的无效比无用更糟糕:内存带宽已被使用,但缓存中不再有该行,因此下一次将其部分写入时需要重新获取。
- 下一次内存分配器 returns 阻塞时,可能很快,其用户将遭受有保证的缓存未命中并从主 RAM 中获取,同时他可能有一条脏的未刷新行或而是清洁冲洗线。保证高速缓存未命中和从主 RAM 获取的成本 比高速缓存行刷新 没有 的成本大 很多 缓存硬件。
- 循环和刷新这些行所需的额外代码会浪费指令缓存 space。
- 更好地利用上述循环使高速缓存行无效的数十个周期是继续做有用的工作,同时让高速缓存和内存子系统的相当大的带宽写回您的脏高速缓存行。
- 我的现代 Haswell 处理器具有 32 字节/时钟周期写入 L1 带宽和 25GB/s 主 RAM 带宽。我确定可以在其中的某个地方挤入几个额外的可刷新 32 字节缓存行。
- 最后,对于像这样的短暂的、小的分配,可以选择在堆栈上分配它。
实际内存分配器实践
None 其中会使内存无效,因为它们不能。为了使缓存行无效而进行系统调用会非常慢,并且会导致更多的缓存流量 in/out,这仅仅是因为上下文切换。
我不知道有任何架构愿意将其缓存一致性协议公开给软件(用户甚至内核)这样的操作。这会产生几乎无法处理的警告。 请注意,用户启动的刷新是可以接受的暴露,但绝不会威胁破坏内存一致性。
举个例子,假设您有一个缓存行,其中包含您不再需要的临时数据。由于它被写入,它在缓存中将处于 "modified" 状态。 现在你想要一种机制告诉缓存避免将它写回,但这意味着你创建了一个竞争条件 - 如果其他人在你应用这个技巧之前寻找该行,他会从核心中窥探它并且收到更新的数据。如果你的核心先行,新数据就会丢失 - 因此,内存中该地址的结果取决于竞争。
您可能会争辩说在多线程编程中经常会出现这种情况,但是这种情况也可能发生在 运行 单个线程(如果缓存是完整的,或一些较低的包含级别丢失它)。更糟糕的是,这打破了整个虚拟内存呈现为平坦的前提,缓存版本由 CPU 维护只是为了性能,但不能破坏一致性或一致性(除了一些记录的多线程情况,取决于内存排序模型,可以通过软件保护来克服)。
编辑: 如果您愿意扩展您所认为的 "memory" 的定义,您可以寻找非连贯类型的内存,它们在定义和实现上有所不同,但有些可能会提供您所寻求的。一些架构公开了“scratchpad”内存,它由用户控制并允许快速访问而没有缓存一致性的麻烦(但也没有它的好处)。有些架构甚至提供可配置的硬件,允许您 select 是否喜欢在其中缓存主内存,或者将其用作暂存器区域。