观察线程子类的以下定义(为方便起见,整个可运行的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++;
}
}
}
解释:程序启动-Dpar
个此类线程,并将每个线程的sz
设置为-Dsize / -Dpar
,其中-Dsize
和{{运行程序时通过命令行设置1}}。每个线程对象都有一个字段-Dpar
,该字段使用新的array
- 元素数组进行初始化。原因是我们希望在不同数量的线程之间划分相同数量的工作 - 我们希望程序可以扩展。
然后启动每个线程并测量所有线程完成所需的时间。我们进行多次测量以抵消任何与JIT相关的影响,如下所示。每个线程都循环。在循环内,线程在偶数迭代中读取数组中位置1024
处的元素,并在奇数迭代中在512
处写入相同的元素。否则只会修改局部变量。
完整的计划如下。
分析:
使用512
进行测试 - 在此程序运行期间没有发生垃圾收集。
运行命令:
-verbose:gc
案例1:java -Xmx512m -Xms512m -server -Dsize=500000000 -Dpar=1 org.scalapool.bench.MultiStackJavaExperiment 7
个线程的运行时间,按此顺序(7次重复):
1,2,4,8
我的想法是非线性缩放是由于内存争用造成的。顺便提一下,早期的迭代实际上做得更好 - 这可能是因为在不同的迭代中,数组被分配在不同的内存区域。
案例2:接下来,我在线程的>>> 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]
方法中评论Foo[] arr = array
行,并在run
方法本身中分配一个新数组:run
。测量:
Foo[] arr = new Foo[1024]
这一次,一切都像预期的那样扩展。我不会想到分配数组的位置起任何作用,但显然它确实以某种方式。我的想法是,数组以前分配得如此接近,以至于发生了一些内存争用。
案例3:为验证此假设,我再次取消注释了行>>> 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]
,但这次将Foo[] arr = array
字段初始化为array
以确保内存中的位置被写入彼此相距甚远。所以,这里我们再次使用在创建线程对象期间分配的数组,与CASE1的区别仅在于数组更大。
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]
问题:这显然是内存争用问题。但为什么会这样呢?
逃脱分析是否已经开始?如果是这样,是否意味着在CASE2中的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)
方法中创建时,整个数组是否在堆栈上分配?此运行时优化的确切条件是什么?当然,数组没有在堆栈上分配100万个元素?
即使数组正在堆栈上分配而不是分配在堆栈上 堆,不同线程的两个数组访问应至少划分为512 * 4bytes = 2kb,即使在CASE1中,无论数组在哪里!这绝对比任何L1缓存行都要大。如果这些影响是由于错误共享造成的,那么如何写入几个完全独立的缓存行会影响性能呢? (这里的一个假设是每个数组占用JVM上的一个连续的内存块,它在创建数组时分配。我不确定它是否有效。另一个假设是数组写入不会一直到内存,但L1缓存,因为英特尔至强确实有一个ccNUMA架构 - 纠正我,如果我错了)
是否有可能每个线程都有自己的本地堆部分,它独立地分配新对象,这是在线程中分配数组时导致较低争用的原因?如果是这样,如果引用被共享,那么如何收集堆垃圾的区域?
为什么将数组大小增加到~32000个元素可以提高可伸缩性(减少内存争用)?内存层次结构到底是什么原因?
请准确并通过参考支持您的声明。
谢谢!
整个可运行的Java程序:
run
答案 0 :(得分:13)
<强>解决方案强>
使用-XX:+UseCondCardMark
标志运行JVM,仅在JDK7中可用。这解决了这个问题。
<强>解释强>
实质上,大多数托管堆环境使用卡表来标记发生写入的内存区域。一旦发生写入,这些存储区域在卡表中被标记为脏。垃圾收集需要此信息 - 不必扫描非脏存储区域的引用。卡是连续的内存块,通常为512字节。卡表通常每个卡有1个字节 - 如果设置了该字节,则卡是脏的。这意味着具有64字节的卡表覆盖64 * 512字节的内存。通常,今天的缓存行大小为64字节。
因此,每次写入对象字段时,必须将卡表中相应卡的字节设置为脏。单线程程序中有用的优化是通过简单地标记相关字节来做到这一点 - 每次都写一次。首先检查字节是否设置和条件写入的替代方法需要额外的读取和条件跳转,这稍微慢一点。
然而,如果有多个处理器写入存储器,这种优化可能是灾难性的,因为写入的相邻卡需要写入卡表中的相邻字节。因此写入的内存区域(上面数组中的条目)不在同一缓存行中,这是内存争用的常见原因。真正的原因是写入的脏字节位于同一个缓存行中。
上述标志的作用是 - 它通过首先检查字节是否已经设置来实现卡表脏字节写入,并且只有在不设置时才设置它。这样,内存争用仅在第一次写入该卡时发生 - 在此之后,仅发生对该缓存行的读取。由于只读取缓存行,因此可以在多个处理器之间复制它们,并且它们不必同步即可读取它。
我观察到这个标志在1线程的情况下增加了大约15-20%的运行时间。
此blog post及此bug report解释了-XX:+UseCondCardMark
标记。
相关的并发邮件列表讨论:Array allocation and access on the JVM。
答案 1 :(得分:1)
我相信你需要减少你的代码,以免它做很多偶然的事情,这些事情可能让人感到困惑。在减少代码之后,我很清楚您每次只访问相同的阵列位置。即位置512。
如果你最小化你的代码,重复使用你的线程,这样你就不会停止/启动它们,你会得到更多可重复的结果。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class MultiStackJavaExperiment {
static final int size = Integer.getInteger("size", 500000000);
public static void main(String... args) throws ExecutionException, InterruptedException {
int par = 8;
for (int s = 64; s <= 64 * 1024; s *= 2) {
int times = args.length == 0 ? 1 : Integer.parseInt(args[0]);
long[] measurements = new long[times];
ExecutorService es = Executors.newFixedThreadPool(par);
List<Future<?>> futures = new ArrayList<Future<?>>(times);
for (int i = 0; i < times; i++) {
long start = System.currentTimeMillis();
final int sz = size / par;
futures.clear();
for (int j = 0; j < par; j++) {
final Object[] arr = new Object[s];
futures.add(es.submit(new Runnable() {
@Override
public void run() {
final int bits = 7, arraySize = 1 << bits;
int i = 0;
int pos = 32;
Object v = new Object();
while (i < sz) {
if (i % 2 == 0) {
arr[pos] = v;
pos += 1;
} else {
pos -= 1;
v = arr[pos];
}
i++;
}
}
}));
}
for (Future<?> future : futures)
future.get();
long time = System.currentTimeMillis() - start;
// System.out.println(i + ") Running time: " + time + " ms");
measurements[i] = time;
}
es.shutdown();
System.out.println("par = " + par + " arr.length= "+ s + " >>> All running times: " + Arrays.toString(measurements));
}
}
}
这表明访问值之间的距离很重要。通过为每个线程分配一个数组,您可以使用不同的TLAB(将块中的数据隔开)
par = 8 arr.length= 64 >>> All running times: [539, 413, 444, 444, 457, 444, 456]
par = 8 arr.length= 256 >>> All running times: [398, 527, 514, 529, 445, 441, 445]
par = 8 arr.length= 1024 >>> All running times: [419, 507, 477, 422, 412, 452, 396]
par = 8 arr.length= 4096 >>> All running times: [316, 282, 250, 232, 242, 229, 238]
par = 8 arr.length= 16384 >>> All running times: [316, 207, 209, 212, 208, 208, 208]
par = 8 arr.length= 65536 >>> All running times: [211, 211, 208, 208, 208, 291, 206]
par = 8 arr.length= 262144 >>> All running times: [366, 210, 210, 210, 210, 209, 211]
par = 8 arr.length= 1048576 >>> All running times: [296, 211, 215, 216, 213, 211, 211]
如果你在你得到的线程中移动数组
par = 8 arr.length= 64 >>> All running times: [225, 151, 151, 150, 152, 153, 152]
par = 8 arr.length= 256 >>> All running times: [155, 151, 151, 151, 151, 151, 155]
par = 8 arr.length= 1024 >>> All running times: [153, 152, 151, 151, 151, 155, 152]
par = 8 arr.length= 4096 >>> All running times: [155, 156, 151, 152, 151, 155, 155]
par = 8 arr.length= 16384 >>> All running times: [154, 157, 152, 152, 158, 153, 153]
par = 8 arr.length= 65536 >>> All running times: [155, 157, 152, 184, 181, 154, 153]
par = 8 arr.length= 262144 >>> All running times: [240, 159, 166, 151, 172, 154, 160]
par = 8 arr.length= 1048576 >>> All running times: [165, 162, 163, 162, 163, 162, 163]
使用-XX:-UseTLAB
关闭tlab,并使用相同的代码给出syou
par = 8 arr.length= 64 >>> All running times: [608, 467, 467, 457, 468, 461, 428]
par = 8 arr.length= 256 >>> All running times: [437, 437, 522, 512, 522, 369, 535]
par = 8 arr.length= 1024 >>> All running times: [394, 395, 475, 525, 470, 440, 478]
par = 8 arr.length= 4096 >>> All running times: [347, 215, 238, 226, 236, 204, 271]
par = 8 arr.length= 16384 >>> All running times: [291, 157, 178, 151, 150, 151, 152]
par = 8 arr.length= 65536 >>> All running times: [163, 152, 162, 151, 159, 159, 154]
par = 8 arr.length= 262144 >>> All running times: [164, 172, 152, 169, 160, 161, 160]
par = 8 arr.length= 1048576 >>> All running times: [295, 153, 164, 153, 166, 154, 163]