【问题标题】:Does heap allocation always mean immediate RAM access?堆分配是否总是意味着立即访问 RAM?
【发布时间】:2021-10-01 05:55:08
【问题描述】:

假设 Java JIT 编译代码,例如构建一个链表, new Link(及其构造函数)是否有可能在完全不访问 RAM 的情况下返回?换句话说,VM 至少在理论上是否可以只在处理器缓存中执行分配,而在刷新时只执行实际的内存分配(例如,一次性分配整个列表或列表段)?

除了它本身很有趣之外,当我考虑在与新对象创建配对时使用 volatile 关键字的实际相对惩罚时,这个问题就出现了。假设一个可变列表定义如下:

class Link<E> {
    final E elem;
    Link<E> next = null
    Link(E e, Link<E> tail) {
        elem = e;
        next = tail;
    }

    public void append(E e) {
        next = new Link(e, null);
    }
}

可以将volatile 关键字添加到字段next 影响重复调用append 的性能(可能不久之后取消引用整个集合,释放内存用于垃圾收集),限制JVM 能够进行的优化以重要的方式处理代码(至少在理论上)?

【问题讨论】:

  • “仅在处理器缓存中执行分配” - 不,我不这么认为。我对你关于volatile 的观点有点困惑——如果你需要正确的语义,你需要使用它,与正确性相比,在这种情况下性能无关紧要。
  • 如果有足够的 CPU 缓存,操作可以完全在 CPU 缓存中完成,因为它们在所有现代处理器上都是一致的。 volatile 的成本不是因为刷新到内存(这不会发生)。代价是减少编译器优化和执行防止 CPU 内重新排序的栅栏。
  • 例如;如果 volatile 存储之后是对不同地址的 volatile 加载,则需要按顺序执行它们,因为这通常是 CPU 想要优化的东西(查找存储缓冲区)在某些平台 (ARM/X86) 上,它会导致从缓存加载到存储提交到缓存的延迟。这通常是使 volatile 变得昂贵的原因。
  • 现代处理器都是加载/存储架构。这意味着像 ALU 操作这样的大多数操作都不能直接访问内存,并且总是需要通过寄存器。即使是内存/寄存器架构的 X86,一旦转换为 uops 也是加载/存储架构。所以 volatile 不能阻止使用寄存器;关键区别在于 volatile 控制它将在寄存器中保留多长时间,或者是否需要将其写入缓存。这是编译器的问题。
  • 如果高速缓存行在进行访问的 CPU 上处于正确状态,则访问高速缓存非常便宜。根本不需要访问主存储器。要付出的主要代价是限制 CPU 或编译器中的重新排序。而且当然;如果缓存行在缓存中没有处于正确的状态,那么价格就会很高。

标签: java volatile jit


【解决方案1】:

换句话说,至少在理论上,VM 是否只能在 处理器缓存,并且仅在以下情况下进行实际内存分配 冲洗...

真正的机器一般不具备这种能力。一个内存字有一个“地址”(让我们忽略这个讨论中的任何 MMU),这个地址标识这个字是在 CPU 缓存中还是在实际内存中。操作系统知道地址意味着它在某种意义上具有“分配的内存”。 CPU 写入内存。硬件可能现在或以后将其存储在缓存和/或内存中,具体取决于实现。

通常不会有“不要从缓存中写回”指令(在处理器初始化之外)。因此,CPU 没有办法阻止更改出现在内存中。它通常确实有一种方法可以强制更改内存。 'volatile' 告诉编译器这是需要的。

当然,JVM 是一个虚拟机,所以它可以为所欲为。但如果有一个不同于真正 CPU 缓存的“虚拟缓存”,我会感到非常惊讶。

进一步说明-操作系统通常具有分配零需求页面的机制,因此尽管存在虚拟内存,但它不一定由页面框架支持,直到它被触摸,即“RAM”尚未被“访问” .可以想象,在这种状态下可以部分创建一个大堆对象。但是,这与 CPU 缓存无关。

作为一名长期实践的程序员,我发现考虑“RAM”通常没有用,而是考虑“地址空间”和(虚拟)“内存”。

【讨论】:

  • “它通常确实有一种方法可以强制更改内存;'volatile' 告诉编译器这是需要的。”请定义“内存”的含义,因为如果您指的是主内存,那么这是不正确的:现代处理器具有一致的缓存,因此它们不需要与主内存通信。
  • 可能缓存不足,或者缓存一致性算法存在限制(例如 MESI,如果不将缓存行推送到 DRAM,就无法共享脏缓存行;MOESI 不会有这个问题)强制写入缓存行。
  • 看来我用错了;我并不怀疑 JVM(或就此而言的本机编译器)在 CPU 缓存中显式执行操作的能力,但总体效果在实践中是这样的。特别是,对任何同步 JVM 的缓存和执行的影响都是堆分配的一部分。
【解决方案2】:

在字段 next 中添加 volatile 关键字是否会影响对 append 的重复调用的性能(可能不久之后取消引用整个集合,释放内存以进行垃圾回收),从而限制了 JVM 能够对代码中的代码进行的优化重要的方式(至少在理论上)?

答案当然是肯定的。 JVM 是一个抽象的概念。关于某些 java 代码性能的概括性陈述仅属于这些类别:

  • Java 内存模型保证这可以正常工作(但这始终是关于某些东西是否存在错误(有时存在错误的方式是现有 VM 无法重现,但仍然存在错误,因为某些未来版本可能会破坏您的代码并且它) d是你的错))-但这与性能特征无关。

  • 这样的声明:对于所有主要平台和所有主要 VM 版本,在其当前版本中,此代码将运行良好。这不包括保证下一个版本会发生什么或 java 是否在新平台上运行。此外,实际上将这些放在一起非常困难:您要么需要成为一本了解所有主要 VM 实现的所有详细信息的活字典,要么您需要完成这项工作并在大约 3 个操作系统中的每个操作系统上安装大约 15 个 JVM,在 30 种不同类型的硬件平台上,总共超过 1000 种组合,您需要在做出此类声明之前进行性能测试。

因此,鉴于上述情况属于边缘性精神错乱,您的问题的答案是“是”。

主要是因为这是任何形式问题的答案:

至少在理论上可以通过限制 JVM 能够以显着方式进行的优化来影响(任何)性能吗?

是“是”。

一些可能有用的具体知识:

  • JVM通常能够(几乎)消除 volatile 关键字的性能影响,前提是该关键字没有做任何有用的事情(例如,该字段仅被读/写为单个线程或多线程访问在时间上相距很远)。
  • 一般情况下,您不会添加 volatile 来搞笑。您需要线程行为,因此您不能只比较具有关键字与没有关键字的性能 - 没有关键字意味着代码已损坏,因此这显然是无用且不公平的比较。相反,您需要与下一个最佳替代方案进行比较,例如使用 AtomicX 或重写代码以提高线程之间的同步。

【讨论】:

  • 这只是陈述问题所需的最小化示例。真正的问题是“如果我可以添加一些优化(在多个实例之间共享数据,或在大对象中使用内部缓存,以线程同步为代价(即易失性检查)),当它可以有一个不利影响。假设在最近读取的节点绕过 O(log(n)) bintree 查找时添加 O(1)“焦点”,但很有可能大部分树可能位于 CPU 缓存中。正如你所说,分析非常昂贵,一些初步的直觉非常有帮助。
  • 另外,“重要”这个词的使用将我的兴趣限制在“是的,很有可能”的明确案例上。
猜你喜欢
  • 2015-03-12
  • 2018-11-13
  • 1970-01-01
  • 1970-01-01
  • 2019-05-28
  • 2018-06-14
  • 2016-05-13
  • 1970-01-01
  • 2014-08-15
相关资源
最近更新 更多