Java 虚拟机上的数组分配和访问以及内存争用
请遵守线程子类的以下定义(为方便起见,整个可运行的 Java 源文件包含在问题末尾):
final class Worker extends Thread {
Foo[] array = new Foo[1024];
int sz;
public Worker(int _sz) {
sz = _sz;
}
public void run() {
//Foo[] arr = new Foo[1024];
Foo[] arr = array;
loop(arr);
}
public void loop(Foo[] arr) {
int i = 0;
int pos = 512;
Foo v = new Foo();
while (i < sz) {
if (i % 2 == 0) {
arr[pos] = v;
pos += 1;
} else {
pos -= 1;
v = arr[pos];
}
i++;
}
}
}
说明: 程序启动此类线程,并将每个线程的 设置为 ,其中 和 在运行程序时通过命令行设置。每个线程对象都有一个字段,该字段使用新的 -element 数组进行初始化。理由是,我们希望在不同数量的线程之间分配等量的工作量 - 我们希望程序能够扩展。-Dpar
sz
-Dsize / -Dpar
-Dsize
-Dpar
array
1024
然后启动每个线程,并测量所有线程完成所需的时间。我们进行多次测量以抵消任何与JIT相关的影响,如下所示。每个线程执行一个循环。在循环中,线程在偶数迭代中读取数组中位置处的元素,并在奇数迭代中写入相同的元素。否则,仅修改局部变量。512
512
完整程序如下。
分析:
测试使用 - 在此程序运行期间没有发生垃圾回收。-verbose:gc
运行命令:
java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 org.scalapool.bench.MultiStackJavaExperiment 7
案例 1:线程的运行时间,按该顺序排列(7 次重复):1,2,4,8
>>> All running times: [2149, 2227, 1974, 1948, 1803, 2283, 1878]
>>> All running times: [1140, 1124, 2022, 1141, 2028, 2004, 2136]
>>> All running times: [867, 1022, 1457, 1342, 1436, 966, 1531]
>>> All running times: [915, 864, 1245, 1243, 948, 790, 1007]
我的想法是,非线性缩放是由于内存争用。顺便说一句,早期迭代实际上做得更好 - 这可能是因为在不同的迭代中,数组被分配在不同的内存区域中。
案例 2:接下来,我在线程的方法中注释该行,并在方法本身中分配一个新数组:。测量:Foo[] arr = array
run
run
Foo[] arr = new Foo[1024]
>>> All running times: [2053, 1966, 2089, 1937, 2046, 1909, 2011]
>>> All running times: [1048, 1178, 1100, 1194, 1367, 1271, 1207]
>>> All running times: [578, 508, 589, 571, 617, 643, 645]
>>> All running times: [330, 299, 300, 322, 331, 324, 575]
这一次,一切都像预期的那样扩展。我不会想到分配数组的位置会扮演任何角色,但显然它确实以某种方式起作用。我的想法是,这些数组以前分配得非常接近彼此,以至于开始发生一些内存争用。
案例 3:为了验证此假设,我再次取消了对该行的注释,但这次初始化了该字段,以确保要写入的内存中的位置彼此相距足够远。因此,这里我们再次使用在创建线程对象期间分配的数组,与CASE1的区别只是数组更大。Foo[] arr = array
array
new Foo[32000]
>>> All running times: [2113, 1983, 2430, 2485, 2333, 2359, 2463]
>>> All running times: [1172, 1106, 1163, 1181, 1142, 1169, 1188]
>>> All running times: [578, 677, 614, 604, 583, 637, 597]
>>> All running times: [343, 327, 320, 330, 353, 320, 320]
因此,内存争用似乎是导致这种情况的原因。
平台信息:
Ubuntu Server 10.04.3 LTS
8 core Intel(R) Xeon(R) CPU X5355 @2.66GHz
~20GB ram
java version "1.6.0_26"
Java(TM) SE Runtime Environment (build 1.6.0_26-b03)
Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02, mixed mode)
问:这显然是一个内存争用问题。但为什么会发生这种情况呢?
逃生分析是否开始发挥作用?如果是这样,这是否意味着在 CASE2 中的方法中创建时,整个数组都分配在堆栈上?此运行时优化的确切条件是什么?当然,数组没有在堆栈上为100万个元素分配?
run
即使数组是在堆栈上分配的,而不是在堆上分配的,不同线程的两个数组访问也应该至少除以512 * 4bytes = 2kb,即使在CASE1中,无论数组在哪里!这绝对比任何L1缓存行都大。如果这些影响是由于错误共享造成的,那么写入多个完全独立的缓存行对性能有何影响?(这里的一个假设是,每个数组都占用 JVM 上的一个连续内存块,该内存块是在创建数组时分配的。我不确定这是否有效。另一个假设是,数组写入不会一直到内存,而是L1缓存,因为英特尔至强确实有ccNUMA架构 - 如果我错了,请纠正我)
每个线程是否有可能有自己的本地堆部分,在其中独立分配新对象,这是在线程中分配数组时争用较低的原因?如果是这样,如果共享引用,如何收集堆垃圾区域?
为什么将数组大小增加到 ~32000 个元素可以提高可伸缩性(减少内存争用)?内存层次结构中究竟是什么原因造成的?
请准确无误,并用参考资料支持您的主张。
谢谢!
整个可运行的 Java 程序:
import java.util.ArrayList;
class MultiStackJavaExperiment {
final class Foo {
int x = 0;
}
final class Worker extends Thread {
Foo[] array = new Foo[1024];
int sz;
public Worker(int _sz) {
sz = _sz;
}
public void run() {
Foo[] arr = new Foo[1024];
//Foo[] arr = array;
loop(arr);
}
public void loop(Foo[] arr) {
int i = 0;
int pos = 512;
Foo v = new Foo();
while (i < sz) {
if (i % 2 == 0) {
arr[pos] = v;
pos += 1;
} else {
pos -= 1;
v = arr[pos];
}
i++;
}
}
}
public static void main(String[] args) {
(new MultiStackJavaExperiment()).mainMethod(args);
}
int size = Integer.parseInt(System.getProperty("size"));
int par = Integer.parseInt(System.getProperty("par"));
public void mainMethod(String[] args) {
int times = 0;
if (args.length == 0) times = 1;
else times = Integer.parseInt(args[0]);
ArrayList < Long > measurements = new ArrayList < Long > ();
for (int i = 0; i < times; i++) {
long start = System.currentTimeMillis();
run();
long end = System.currentTimeMillis();
long time = (end - start);
System.out.println(i + ") Running time: " + time + " ms");
measurements.add(time);
}
System.out.println(">>>");
System.out.println(">>> All running times: " + measurements);
System.out.println(">>>");
}
public void run() {
int sz = size / par;
ArrayList < Thread > threads = new ArrayList < Thread > ();
for (int i = 0; i < par; i++) {
threads.add(new Worker(sz));
threads.get(i).start();
}
for (int i = 0; i < par; i++) {
try {
threads.get(i).join();
} catch (Exception e) {}
}
}
}