2014-02-17 38 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)将解决此问题。