2014-02-17 39 views
1

更多更新的Java多線程的可擴展性問題

,與所選答案解釋,問題出在JVM的垃圾回收算法。 JVM使用卡片標記算法跟蹤對象字段中的修改引用。對於每個字段的引用分配,它將卡中的相關位標記爲真 - 這會導致錯誤共享,從而阻止縮放。細節在本文中有詳細描述:https://blogs.oracle.com/dave/entry/false_sharing_induced_by_card

選項-XX:+ UseCondCardMark(在Java 1.7u40及更高版本中)緩解了問題並使其幾乎完美地縮放。


更新

我發現了(從公園Eung菊暗示),該分配對象插入字段變量產生變化。如果我刪除了作業,它可以完美地縮放。 我認爲它可能與Java內存模型有關 - 例如,對象引用必須在可見之前指向有效地址,但我不完全確定。 double和Object引用(很可能)在64位機器上有8個字節大小,所以在我看來,分配一個double值和一個Object引用在同步方面應該是相同的。

任何人都有合理的解釋?


這裏我有一個奇怪的Java多線程可伸縮性問題。

我的代碼只是遍歷一個數組(使用visitor模式)來計算簡單的浮點運算並將結果賦值給另一個數組。沒有數據依賴性,也沒有同步,因此它應該線性縮放(2線程更快,4線程更快4倍)。

使用原始(double)數組時,它可以非常好地縮放。當(例如字符串)陣列用於對象類型,它完全不縮放(即使字符串數組的值不是在所有使用...)

這裏就是整個源代碼:

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),但它們是一樣的。

當使用arg.type = double或arg.type = Object選項時,代碼對使用XXX和YYY的註釋進行了描述。

你能弄清楚在這裏傳遞的字符串類型參數是怎麼回事?

+1

請在您的問題中包含代碼;外部鏈接可能會被刪除。 – GreySwordz

+0

你能否執行一個配置文件來查看是否有相當多的GC活動?第126行的'(String)'似乎也是一個潛在的問題。你可以嘗試直接接受'String'來檢查嗎? – hexafraction

+0

我用GarbageCollectorMXBean測量了GC時間;這並不重要(在測量時間之前,我也強制運行GC)。類型轉換不是一個問題 - 我沒有進行鑄造測試,情況也是如此。 對於包含代碼,它包含的時間太長。一旦我有足夠數量的答案,我會嘗試包括它。 – Jiwon

回答

1

HotSpot VM爲引用類型putfield字節碼生成以下程序集。

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) 

putfield操作是在指令1完成。 但還有更多的說明如下。

他們是「卡片標記」說明。 (http://www.ibm.com/developerworks/library/j-jtp11253/

將參考字段寫入卡片中的每個對象(512字節),將值存儲在相同的存儲器地址中。

我想,存儲到同一個內存地址從多個內核搞砸了緩存和管道。

只需添加

byte[] garbage = new byte[600]; 

到MyVisitor定義。

然後每個MyVisitor實例間隔不足以共享卡片標記位,您將看到程序縮放。

1

這不是一個完整的答案,但可能會爲您提供一個提示。

這種變化,運行時間與4個線程與對象類型的陣列降低後我已經改變您的代碼

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

+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位半。這可能會導致線程看到來自一次寫入的64位值的前32位和來自另一次寫入的第二次32位。

對volatile和double值的寫入和讀取總是原子的。

寫入和讀取引用始終是原子的,無論它們是以32位還是64位值實現的。

分配引用總是原子, 和是不是當它被定義爲揮發性除了原子。

問題是sv可以被其他線程看到,它的賦值是原子的。因此,使用ThreadLocal打包訪問者的成員變量(i,iv,sv)將解決此問題。