在托管代码中,如何实现良好的局部性?
由于现在的RAM看起来像是新硬盘,这也意味着访问内存的速度被认为和访问硬盘一样慢,所以我想在高性能应用中最大化内存的局部性。举个例子,在一个排序的索引中,我希望相邻的值能够靠得近(这和哈希表不一样),而且我希望索引指向的数据也能靠得近。
在C语言中,我可以创建一个带有专门内存管理器的数据结构,就像开发(非常复杂的)Judy数组的开发者那样。通过直接控制指针,他们甚至在指针的值中编码了额外的信息。而在Python、Java或C#中,我故意选择了一种(或多种)更高层次的抽象,信任JIT编译器和优化运行时为我在底层做一些聪明的处理。
不过,我想即使在这种高层次的抽象中,仍然有一些东西可以被认为是“更近”的,因此在底层实际上也可能是更近的。例如,我在想以下问题(我的猜测在括号里):
- 我可以期待一个数组是一个相邻的内存块吗(可以)?
- 同一个实例中的两个整数比不同实例中的两个整数更近吗(可能)?
- 一个对象占用的内存区域是连续的吗(不是)?
- 一个只有两个
int
字段的对象数组和一个有两个int[]
字段的单一对象有什么区别?(这个例子可能是Java特有的)
我开始是在Java的背景下思考这些问题,但我的思考变得更为广泛,所以我建议不要把这当作一个Java问题来看待。
6 个回答
我觉得没人提过Python,所以我来试试。
我可以期待一个数组是连续的内存块吗(可以)?
在Python中,数组更像是C语言中的指针数组。所以指针是相邻的,但实际的对象不太可能是相邻的。
同一个实例中的两个整数比同一个类的不同实例中的两个整数更靠近吗(可能)?
可能不是,原因和上面一样。实例只会保存指向实际整数对象的指针。Python没有原生的int类型(像Java那样),只有包装的Int(用Java的说法)。
一个对象占用的内存区域是连续的吗(不是)?
可能不是。不过如果你使用__slots__
优化的话,那么它的一些部分会是连续的!
只有两个int字段的对象数组和一个有两个int[]字段的单个对象有什么区别? (这个例子可能是Java特有的)
在Python中,从内存位置的角度来看,它们基本上是一样的!一个会创建一个指向对象的指针数组,这些对象又会包含两个指向整数的指针,另一个则会创建两个指向整数的指针数组。
首先,你的标题提到了C#。 “托管代码”这个词是微软创造的,如果我没记错的话。
Java的基本数组是保证在内存中是连续的一块区域。如果你有一个
int[] array = new int[4];
你可以通过JNI(本地C代码)获取一个int *p
,指向实际的数组。我觉得这也适用于Array*类的容器(比如ArrayList、ArrayBlockingQueue等)。
早期的JVM实现中,对象是作为连续的结构存在的,但在新的JVM中不能这样假设。(JNI会把这些细节隐藏起来)。
同一个对象中的两个整数,正如你所说的,可能会“更近”,但也不一定。这在使用同一个JVM时可能会有所不同。
一个包含两个int字段的对象就是一个对象,我认为没有任何JVM会保证这些成员会“靠近”。而一个包含两个元素的int数组,很可能是由一个8字节的长数组支持的。
- 在.NET中,数组里的元素是连续存放的。在Java中,我认为大多数情况下也是这样,但似乎并没有保证。
- 我觉得可以合理地假设,一个实例的字段所占用的内存是一个整体……但别忘了,这些字段中有些可能是指向其他对象的引用。
关于Java数组的部分,Sun的JNI文档中提到了一句,藏在关于字符串的讨论里:
例如,Java虚拟机可能不会将数组连续存储。
对于你最后的问题,如果你有两个int[]
数组,那么这两个数组各自会是一个连续的内存块,但它们在内存中可能相隔很远。如果你有一个包含两个整数字段的对象数组,那么每个对象之间可能相距很远,但每个对象内部的两个整数会很靠近。更重要的是,使用“很多对象”的方案可能会占用更多内存,因为每个对象都有额外的开销。在.NET中,你可以使用一个包含两个整数的自定义结构,然后创建一个这样的数组——这样就能把所有数据放在一个大块里。
我相信在Java和.NET中,如果你在一个线程中快速分配很多小对象,这些对象很可能会有良好的局部性。垃圾回收器整理堆内存时,这种情况可能会改善——或者可能变得更糟,如果一个堆
A B C D E
被整理成
A D E B
(C被回收)——突然间,A和B可能之前是“靠近”的,现在却很远。我不知道这在任何垃圾回收器中是否真的会发生(有很多种垃圾回收器!),但这是有可能的。
基本上,在一个托管环境中,你对局部性管理的控制通常没有在非托管环境中那么多——你需要相信托管环境在管理这方面足够好,并且通过在更高层次的平台上编程节省的时间,能够让你有时间去优化其他地方。