более обновленийJava многопоточной масштабируемости проблема
Как поясняется в выбранном ответе, проблема заключается в алгоритме сборки мусора JVM в. JVM использует алгоритм маркировки карт для отслеживания модифицированных ссылок в полях объектов. Для каждого задания ссылки в поле он отмечает соответствующий бит в карте, чтобы быть правдой - это приводит к тому, что функция ложного обмена блокирует масштабирование. Подробности хорошо описаны в этой статье: https://blogs.oracle.com/dave/entry/false_sharing_induced_by_card
Опция -XX: + UseCondCardMark (в Java 1.7u40 и выше) смягчает проблему и делает ее масштабируемой почти идеально.
обновление
я узнал (намекает из Парка Eung-ж), что назначение объекта в поле переменного делает разницу. Если я удалю задание, он отлично масштабируется. Я думаю, что, возможно, это имеет какое-то отношение к модели памяти Java - например, ссылка на объект должна указывать на действительный адрес до того, как он станет видимым, но я не совсем уверен. И двойной, и Object reference (вероятно) имеют размер 8 байтов на 64-битной машине, поэтому мне кажется, что назначение двойного значения и ссылки на объект должно быть одинаковым с точки зрения синхронизации.
У любого есть разумное объяснение?
Здесь у меня есть странная проблема масштабируемости многопоточности Java.
Мой код просто выполняет итерацию по массиву (используя шаблон посетителя) для вычисления простых операций с плавающей запятой и присваивания результата другому массиву. Нет зависимости от данных и синхронизации, поэтому он должен масштабироваться линейно (в 2 раза быстрее с 2 потоками, 4 раза быстрее с 4 потоками).
Когда используется примитивный (двойной) массив, он очень хорошо масштабируется. Когда тип объекта используется (например, String) массив, не масштабируется на всех (даже если значение массива Строка не используется вообще ...)
Вот весь исходный код:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.CyclicBarrier;
class Table1 {
public static final int SIZE1=200000000;
public static final boolean OBJ_PARAM;
static {
String type=System.getProperty("arg.type");
if ("double".equalsIgnoreCase(type)) {
System.out.println("Using primitive (double) type arg");
OBJ_PARAM = false;
} else {
System.out.println("Using object type arg");
OBJ_PARAM = true;
}
}
byte[] filled;
int[] ivals;
String[] strs;
Table1(int size) {
filled = new byte[size];
ivals = new int[size];
strs = new String[size];
Arrays.fill(filled, (byte)1);
Arrays.fill(ivals, 42);
Arrays.fill(strs, "Strs");
}
public boolean iterate_range(int from, int to, MyVisitor v) {
for (int i=from; i<to; i++) {
if (filled[i]==1) {
// XXX: Here we are passing double or String argument
if (OBJ_PARAM) v.visit_obj(i, strs[i]);
else v.visit(i, ivals[i]);
}
}
return true;
}
}
class HeadTable {
byte[] filled;
double[] dvals;
boolean isEmpty;
HeadTable(int size) {
filled = new byte[size];
dvals = new double[size];
Arrays.fill(filled, (byte)0);
isEmpty = true;
}
public boolean contains(int i, double d) {
if (filled[i]==0) return false;
if (dvals[i]==d) return true;
return false;
}
public boolean contains(int i) {
if (filled[i]==0) return false;
return true;
}
public double groupby(int i) {
assert filled[i]==1;
return dvals[i];
}
public boolean insert(int i, double d) {
if (filled[i]==1 && contains(i,d)) return false;
if (isEmpty) isEmpty=false;
filled[i]=1;
dvals[i] = d;
return true;
}
public boolean update(int i, double d) {
assert filled[i]==1;
dvals[i]=d;
return true;
}
}
class MyVisitor {
public static final int NUM=128;
int[] range = new int[2];
Table1 table1;
HeadTable head;
double diff=0;
int i;
int iv;
String sv;
MyVisitor(Table1 _table1, HeadTable _head, int id) {
table1 = _table1;
head = _head;
int elems=Table1.SIZE1/NUM;
range[0] = elems*id;
range[1] = elems*(id+1);
}
public void run() {
table1.iterate_range(range[0], range[1], this);
}
//YYY 1: with double argument, this function is called
public boolean visit(int _i, int _v) {
i = _i;
iv = _v;
insertDiff();
return true;
}
//YYY 2: with String argument, this function is called
public boolean visit_obj(int _i, Object _v) {
i = _i;
iv = 42;
sv = (String)_v;
insertDiff();
return true;
}
public boolean insertDiff() {
if (!head.contains(i)) {
head.insert(i, diff);
return true;
}
double old = head.groupby(i);
double newval=Math.min(old, diff);
head.update(i, newval);
head.insert(i, diff);
return true;
}
}
public class ParTest1 {
public static int THREAD_NUM=4;
public static void main(String[] args) throws Exception {
if (args.length>0) {
THREAD_NUM = Integer.parseInt(args[0]);
System.out.println("Setting THREAD_NUM:"+THREAD_NUM);
}
Table1 table1 = new Table1(Table1.SIZE1);
HeadTable head = new HeadTable(Table1.SIZE1);
MyVisitor[] visitors = new MyVisitor[MyVisitor.NUM];
for (int i=0; i<visitors.length; i++) {
visitors[i] = new MyVisitor(table1, head, i);
}
int taskPerThread = visitors.length/THREAD_NUM;
MyThread[] threads = new MyThread[THREAD_NUM];
CyclicBarrier barrier = new CyclicBarrier(THREAD_NUM+1);
for (int i=0; i<THREAD_NUM; i++) {
threads[i] = new MyThread(barrier);
for (int j=taskPerThread*i; j<taskPerThread*(i+1); j++) {
if (j>=visitors.length) break;
threads[i].addVisitors(visitors[j]);
}
}
Runtime r=Runtime.getRuntime();
System.out.println("Force running gc");
r.gc(); // running GC here (excluding GC effect)
System.out.println("Running gc done");
// not measuring 1st run (excluding JIT compilation effect)
for (int i=0; i<THREAD_NUM; i++) {
threads[i].start();
}
barrier.await();
for (int i=0; i<10; i++) {
MyThread.start = true;
long s=System.currentTimeMillis();
barrier.await();
long e=System.currentTimeMillis();
System.out.println("Iter "+i+" Exec time:"+(e-s)/1000.0+"s");
}
}
}
class MyThread extends Thread {
static volatile boolean start=true;
static int tid=0;
int id=0;
ArrayList<MyVisitor> tasks;
CyclicBarrier barrier;
public MyThread(CyclicBarrier _barrier) {
super("MyThread"+(tid++));
barrier = _barrier;
id=tid;
tasks = new ArrayList(256);
}
void addVisitors(MyVisitor v) {
tasks.add(v);
}
public void run() {
while (true) {
while (!start) { ; }
for (int i=0; i<tasks.size(); i++) {
MyVisitor v=tasks.get(i);
v.run();
}
start = false;
try { barrier.await();}
catch (InterruptedException e) { break; }
catch (Exception e) { throw new RuntimeException(e); }
}
}
}
код Java может быть собран без каких-либо зависимостей, и вы можете запустить его с помощью следующей команды:
Java -Darg.type = двойной -server ParTest1 2
вы передаете количество рабочего потоки как аргумент (t он выше использует 2 потока). После настройки массивов (которые исключены из измеренного времени), он выполняет одну и ту же операцию в 10 раз, распечатывая время выполнения на каждой итерации. С приведенной выше опцией он использует двойной массив, и он очень хорошо масштабируется с 1,2,4 нитями (т.е. время выполнения уменьшается до 1/2 и 1/4), но
java -Darg.type = Object -server ParTest1 2
С помощью этой опции он использует массив Object (String), и он не масштабируется вообще! Я измерил время GC, но это было незначительно (и я также запустил GC перед измерением времени). Я тестировал с Java 6 (обновления 43) и Java 7 (обновления 51), но это то же самое.
В коде есть комментарии с XXX и YYY, описывающие разницу, когда используется аргумент arg.type = double или arg.type = Object.
Вы можете понять, что происходит с аргументом типа String?
Просьба указать код, указанный в вашем запросе; ссылки на внешние места могут быть удалены. – GreySwordz
Можете ли вы выполнить профиль, чтобы увидеть, есть ли значительная активность в GC? Строка '(String)' on line 126 также кажется потенциальной проблемой. Можете ли вы попробовать принять 'String' напрямую, чтобы проверить? – hexafraction
Я измерил время GC с помощью GarbageCollectorMXBean; это было незначительно (я также запустил GC перед измерением времени). Типизация не проблема - я тестировал без кастинга, и это было то же самое. Для включения кода это слишком долго, чтобы его можно было включить. Я попытаюсь включить его, как только у меня будет достаточно ответов. – Jiwon