我可以在Java代码中做什么来优化CPUcaching?

在编写Java程序时,是否对CPU如何利用其caching来存储数据有影响? 例如,如果我有一个访问了很多的数组,如果它足够小以适应一个caching行(通常是64位机器上的128字节),是否有帮助? 如果我在这个限制内保留了一个很多使用的对象,我可以期待它的成员所使用的内存靠近在一起并保持在caching中吗?

背景:我正在构build一个压缩的数字树 ,这受到了Judyarrays的启发,这个arrays是C语言的。虽然我主要使用节点压缩技术,但是Judy将CPUcaching优化作为中心devise目标,节点types为以及它们之间切换的启发式方法受到了很大的影响。 我想知道我是否有机会获得这些好处呢?

编辑到目前为止的答案的一般build议是,不要试图微远优化机器级别的细节,当你离机器太远,因为你在Java中。 我完全同意,所以觉得我不得不添加一些(希望)澄清的意见,以更好地解释为什么我认为这个问题仍然是有道理的。 这些如下:

由于计算机的构build方式,计算机处理起来通常比较容易。 我已经看到Java代码在压缩数据上(来自内存)运行明显更快,即使解压缩必须使用额外的CPU周期。 如果数据存储在磁盘上,那么显然这是为什么,但是当然在RAM中这是相同的原理。

现在,计算机科学有很多东西要说,例如,C语言中的引用的位置是很好的,我猜它在Java中仍然很棒,甚至更有可能,如果它有助于优化运行时间来做更聪明的事情。 但是,你是如何完成的可能是非常不同的。 在C中,我可能会编写pipe理大块内存的代码,并使用相邻的指针来处理相关的数据。

在Java中,我不能(也不想知道)内存是如何被特定的运行时pipe理的。 所以我不得不把优化提高到一个更高的抽象层次。 我的问题基本上是,我该怎么做? 对于参考的地点来说,“在一起”意味着什么在我正在Java中进行抽象的层面上呢? 同一个对象? 同types? 同一arrays?

总的来说,我不认为抽象层改变了“物理定律”,比喻来说。 即使不再调用malloc() ,每次用尽空间时将数组的大小加倍也是Java中的一个好策略。

Java的良好performance的关键是编写惯用代码,而不是试图超越JIT编译器。 如果你编写你的代码,试图影响它在本地指令级以某种方式做事情,那么你更有可能在自己的脚下开枪。

这并不是说像参考地点这样的共同原则并不重要。 他们这样做,但是我会考虑使用数组等等,这是性能意识的,惯用的代码,但不是“棘手的”。

HotSpot和其他优化运行时非常聪明,他们如何优化特定处理器的代码。 (举个例子, 看看这个讨论。 )如果我是一个专业的机器语言程序员,我会写机器语言,而不是Java。 如果不是,那么认为我可以比专家更好地优化我的代码是不明智的。

而且,即使你确实知道为特定的CPU实现某些东西的最佳方法,Java的美妙之处也是随时随地写入一次。 聪明的技巧来“优化”Java代码倾向于使JIT难以识别的优化机会变得更难。 遵循普通习惯用法的直接代码对于优化器来说更容易识别。 所以,即使你为testing平台获得了最好的Java代码,这些代码也可能会在不同的架构上出现可怕的performance,或者至多在未来的JIT上没有利用增强的优势。

如果你想要很好的performance,保持简单。 真正聪明的团队正在努力加快速度。

如果您正在处理的数据主要或完全由原语组成(例如数字问题),我会build议以下内容。

在初始化时分配固定大小的基元数组的平面结构,并确保其中的数据周期性地被压缩/碎片整理(0-> n,其中n是给定元素数量的最小可能的最大索引),以被迭代使用for循环。 这是保证Java中连续分配的唯一方法,并且压缩进一步提高了引用的局部性。 压缩是有益的,因为它减less了迭代未使用的元素的需要,减less了条件的数量:当for循环迭代时,终止发生得更早,迭代更less=堆中更less的移动=caching未命中的机会更less。 虽然压缩本身会产生开销,但是如果您愿意的话,可能只会定期(相对于您的主要处理领域)进行压缩。

更好的是,您可以在这些预先分配的数组中交错数值。 例如,如果要表示二维空间中成千上万个实体的空间变换,并且正在处理每个这样的运动方程,则可能会有一个像

 int axIdx, ayIdx, vxIdx, vyIdx, xIdx, yIdx; //Acceleration, velocity, and displacement for each //of x and y totals 6 elements per entity. for (axIdx = 0; axIdx < array.length; axIdx += 6) { ayIdx = axIdx+1; vxIdx = axIdx+2; vyIdx = axIdx+3; xIdx = axIdx+4; yIdx = axIdx+5; //velocity1 = velocity0 + acceleration array[vxIdx] += array[axIdx]; array[vyIdx] += array[ayIdx]; //displacement1 = displacement0 + velocity array[xIdx] += array[vxIdx]; array[yIdx] += array[vxIdx]; } 

这个例子忽略了使用它们的关联(x,y)渲染这些实体的问题…渲染总是需要非基元(因此,引用/指针)。 如果你确实需要这样的对象实例,那么你不能再保证引用的局部性,并且可能会遍布整个堆。 所以,如果你可以把你的代码分成几个部分,如上所示,你需要进行原始密集型处理,那么这种方法将会帮助你很多。 至less对于游戏来说,人工智能,dynamic地形和物理可以是处理器密集度最高的方面,并且都是数字的,所以这种方法可能是非常有益的。

如果你认为几个百分点的提高有所不同,那么使用C,你会得到50-100%的提升!

如果您认为Java的易用性使其成为一种更好的语言,那么不要把它搞砸了,这是一个可疑的优化。

好消息是,Java将会在运行时做很多事情来改善你的代码,但是它几乎肯定不会做你正在谈论的那种优化。

如果您决定使用Java,请尽可能清晰地编写您的代码,根本不要考虑轻微的优化。 (主要的例如使用正确的集合来完成正确的工作,不要在循环中分配/释放对象等仍然值得)

据我所知:不,你必须写在机器代码才能达到这样的优化水平。 随着大会,你是一步之遥,因为你不再控制的东西存储。 使用编译器,您只需两步即可,因为您甚至无法控制生成的代码的详细信息。 有了Java,你就有三步之遥了,因为有一个JVM在运行中解释你的代码。

我不知道Java中的任何结构可以让你控制这个层次的细节。 从理论上说,你可以间接地通过组织你的程序和数据来影响它,但是你太遥远了,我不知道你怎么可靠地做,甚至不知道它是否发生。

到目前为止,build议非常强大,总的来说,最好不要试图超越JIT。 但是正如你所说的,有些细节的知识有时候是有用的。

关于对象的内存布局,Sun公司的Jvm(现在的Oracle)把对象按types放到内存中(例如,首先是double和long,然后是int和float,然后是short和chars,之后是字节和布尔值,最后是对象引用)。 你可以在这里得到更多的细节 ..

局部variables通常保存在堆栈中(即引用和原始types)。

正如Nick提到的,确保Java内存布局的最好方法是使用原始数组。 这样你可以确保数据在内存中是连续的。 不过要注意数组的大小,GC对大数组有困难。 它也有缺点,你必须自己做一些内存pipe理。

另一方面,您可以使用享元模式来获得类似于对象的可用性,同时保持快速的性能。

如果在性能上需要额外的优势,只要生成的代码执行足够多的时间,并且虚拟机的本机代码caching没有满(这将禁用所有实际的JIT),即时生成自己的字节码有助于解决某些问题目的)。