2014-02-17 3 views
1

более обновлений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?

+1

Просьба указать код, указанный в вашем запросе; ссылки на внешние места могут быть удалены. – GreySwordz

+0

Можете ли вы выполнить профиль, чтобы увидеть, есть ли значительная активность в GC? Строка '(String)' on line 126 также кажется потенциальной проблемой. Можете ли вы попробовать принять 'String' напрямую, чтобы проверить? – hexafraction

+0

Я измерил время GC с помощью GarbageCollectorMXBean; это было незначительно (я также запустил GC перед измерением времени). Типизация не проблема - я тестировал без кастинга, и это было то же самое. Для включения кода это слишком долго, чтобы его можно было включить. Я попытаюсь включить его, как только у меня будет достаточно ответов. – Jiwon

ответ

1

HotSpot VM создает следующие сборки для байт-кода типа put-типа ссылочного типа.

mov ref, OFFSET_OF_THE_FIELD(this) <- this puts the new value for field. 

mov this, REGISTER_A 
shr 0x9, REGISTER_A 
movabs OFFSET_X, REGISTER_B 
mov %r12b, (REGISTER_A, REGISTER_B, 1) 

операция пуска завершена в 1 инструкции. , но после этого есть дополнительные инструкции.

Это инструкции по маркировке карт. (http://www.ibm.com/developerworks/library/j-jtp11253/)

Записывая поле ссылки на все объекты карты (512 байт), будет хранить значение в том же адресе памяти.

И, я думаю, хранить на одном и том же адресе памяти из нескольких ядер испортил кеш и конвейеры.

просто добавить

byte[] garbage = new byte[600]; 

для определения MyVisitor.

тогда каждый экземпляр MyVisitor будет достаточно расположенным, чтобы не разделить бит маркировки карты, вы увидите программные шкалы.

1

Это не полный ответ, но может дать вам подсказку.

Я изменил код

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"); 
} 

в

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, new String("Strs")); 
} 

после этого изменения, времени работы с 4-мя нитями с массивом типа объекта уменьшается.

+0

Так что JVM не использует параллельную хэш-карту для интернированных строк? –

0

"sv = (String) _v;" делает разницу. Я также подтвердил, что тип литья не является фактором. Просто доступ к _v не может изменить ситуацию. Присвоение некоторого значения sv-полю делает разницу. Но я не могу объяснить, почему.

+0

отредактируйте свои форматы вопросов? – danielad

1

Согласно http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7

Для целей модели памяти языка программирования Java, один записи в энергонезависимую длинной или двойной значение обрабатывается как два отдельных записей: один для каждого 32-битной половины , Это может привести к ситуации, когда поток видит первые 32 бита 64-битного значения из одной записи, а второй 32 бита из другой записи.

Запись и чтение изменчивых длинных и двойных значений всегда являются атомарными.

Запись и чтение ссылок всегда являются атомарными, независимо от того, реализованы ли они как 32-разрядные или 64-битные значения.

Назначения ссылки всегда атомная, и двойной не является атомарным исключением случаев, когда оно определяется как изменчиво.

Проблема sv видна другими темами и ее назначение - атомарное. Следовательно, обертывание переменных-членов посетителя (i, iv, sv) с помощью ThreadLocal решит проблему.

Смежные вопросы