2010-07-15 77 views
4

我正在写一个在Nehalem处理器上运行的多线程Java应用程序。然而,我有一个问题,从4个线程开始,我几乎没有看到我的应用程序中的加速。多线程的内存访问

我做了一些简单的测试。我创建了一个线程,它只分配一个大数组并访问数组中的随机条目。所以当我运行线程数时,运行时间不应该改变(假设我不超过可用CPU内核的数量)。但是我观察到,运行1或2个线程的时间几乎相同,但运行4或8个线程的速度要慢得多。因此,在尝试解决我的应用程序中的算法和同步问题之前,我想了解一下我可以实现的最大可能并行化。

我已经使用了-XX:+UseNUMA JVM选项,所以数组应该分配在相应线程的内存中。

P.S.如果线程正在进行简单的数学计算,那么对于4线程甚至8线程就没有时间延迟,所以我得出结论,当线程访问内存时,我遇到了一些问题。

任何帮助或想法表示赞赏,谢谢。


编辑

感谢大家的回复。我看到我没有足够的自我解释。

在尝试消除我的应用程序中的同步问题之前,我做了一个简单的测试,检查可能实现的最佳可能并行化。代码如下:

public class TestMultiThreadingArrayAccess { 
    private final static int arrSize = 40000000; 

    private class SimpleLoop extends Thread { 
     public void run() { 
      int array[] = new int[arrSize]; 
      for (long i = 0; i < arrSize * 10; i++) { 
       array[(int) ((i * i) % arrSize)]++; // randomize a bit the access to the array 
      } 
      long sum = 0; 
      for (int i = 0; i < arrSize; i++) 
       sum += array[i]; 
     } 
    } 

    public static void main(String[] args) { 
     TestMultiThreadingArrayAccess test = new TestMultiThreadingArrayAccess(); 
     for (int threadsNumber : new int[] { 1, 2, 4, 8 }) { 
      Statistics timer = new Statistics("Executing " + threadsNumber+ " threads"); // Statistics is a simple helper class that measures the times 
      timer.start(); 
      test.doTest(threadsNumber); 
      timer.stop(); 
      System.out.println(timer.toString()); 
     } 
    } 

    public void doTest(int threadsNumber) { 
     Thread threads[] = new Thread[threadsNumber]; 
     for (int i = 0; i < threads.length; i++) { 
      threads[i] = new SimpleLoop(); 
      threads[i].start(); 
     } 

     for (int i = 0; i < threads.length; i++) 
      try { 
       threads[i].join(); 
      } catch (InterruptedException e) { 
      }; 
    } 
} 

所以你看到没有同步在此MINITEST所有,也是阵列的分配线程内,因此应该放置在内存块,可以快速访问。此代码中也没有内存争用。仍然对于4个线程,运行时间下降30%,而8个线程运行速度下降两次。从代码中我可以等到所有线程完成他们的工作,并且由于他们的工作是独立的线程数量不应该影响执行所花费的总时间。

在机器上安装了2个四核超线程Nehalem处理器(总共16个CPU),因此每8个线程可以独占CPU。

当我试图用更小的数组(20K条目)运行此测试时,4个线程的执行时间下降了7%,8个线程下降了14%,这是令人满意的。但是当我尝试在大型阵列(40M条目)上随机访问时进行操作时,运行时间显着增加,所以我认为存在大块内存(因为它们不适合高速缓存内存?)在非有效的方法。

有没有任何想法如何解决这个问题?

希望能够以更好的方式阐明问题,再次感谢。

+0

你可能想尝试一个更好的随机数发生器。然后您可以将数组大小与高速缓存大小绑定。 – 2010-07-15 12:38:52

+0

看起来你的每个线程都在创建一个本地数组并在其上进行工作。这意味着当您增加测试中的线程数时,您可以增加正在完成的工作量,而不是增加处理相同数量数据的资源量。这就是为什么你看到缓慢下降。随着您减小阵列大小,执行时间的下降会减少,因为您正在减少通过添加新线程而创建的工作负载的增加。 – SpaceghostAli 2010-07-15 16:08:55

+0

我并不是等待运行时间提高,我只是希望它对于不同数量的线程是相同的,因为它们中的每一个都应该使其在免费的CPU上工作。 – jutky 2010-07-16 06:11:20

回答

2

在测试的瓶颈是在CPU到存储器带宽。即使本地内存可用,它也会被一些线程共享。 (内存是一个节点本地的,而不是特定的内核。)一旦CPU可以轻松地超过可用带宽来进行简单的循环(比如上面的测试),那么在这样的测试中增加线程并不会提高性能,并且会恶化性能由于缓存一致性恶化。

只是一个理智的测试,你也使用并行收集器? -XX:+UseParallelGC。 UseNUMA仅在此时生效。

+0

不,我使用'-XX:+ UseConcMarkSweepGC',我会尝试并行GC,感谢您的建议。 – jutky 2010-07-16 06:20:40

+0

非常感谢。这大大提高了运行时间。现在对于4个线程来说,它需要10%的时间,对于8个线程来说,它需要25%的时间。 – jutky 2010-07-18 11:40:17

1

不知道你到底在做什么,你试图解决什么问题。它看起来像你的代码有很大的同步性,因为它可能是没有足够的可扩展性的主要原因。过度同步会导致任何加速,一旦它使您的应用程序几乎串行。所以我对你的建议是检查你的实现并试图弄清楚。

ADD。

在你添加了你正在做的事情的实现之后。性能下降可以通过大量和大量的内存访问来解释。一旦你运行了所有你的线程,并且他们需要访问内存控制器,因为它们不是缓存数据,因为它们运行在不同的CPU上,所以内存控制器可以防止CPU同时执行它,这意味着每次缓存未命中时硬件级别都有同步。在你的情况下,它几乎相当于你正在运行10个不同的独立程序。我想如果你会发布10(你可以用任何大数字代替10)复制你的网页浏览器,你会看到相同的效果,但这并不意味着浏览器的实现是无效的,你只是创造了巨大的负担计算机内存。

+0

我已经添加了实现的一个片段。在那里你可以看到根本没有同步。 – jutky 2010-07-15 11:53:27

+0

添加了扩展答案。 – 2010-07-15 16:42:18

0

正如Artem所指出的那样,您可能会有不必要的同步。但我首先要确定事实。您的应用程序是否真的像描述的那样运行速度较慢?

下面是关于这个问题的见地的文章:http://codeidol.com/java/java-concurrency/Testing-Concurrent-Programs/Avoiding-Performance-Testing-Pitfalls/

它实际上是相当艰难写有用的微基准测试,尤其是当你正在处理的并发代码。例如,您可以使用“死代码消除”,其中编译器会优化您认为正在执行的代码。猜测垃圾收集何时运行也很困难。热点的运行时优化也使测量更加困难。在线程的情况下,您需要考虑用于创建它们的时间。所以你可能需要使用`CyclicBarrier`等进行精确的测量。这样的事情..

说了这么多,我觉得你很难在访问内存时遇到问题,如果你只是在阅读。如果您可以发布代码,我们可能会更好地帮助您...

+0

谢谢,我会阅读文章。代码被添加。 – jutky 2010-07-15 11:59:42

0

有两个明显的潜在问题值得思考。

  • 使用更多的线程分配更多的阵列,其中突发缓存。访问主内存或较低级别的缓存要慢得多。
  • 如果您使用随机数生成器的实例的相同源,那么线程将争夺对它的访问。它可能不是完全同步,而是使用无锁算法的内存障碍。通常,无锁算法虽然速度通常很快,但在高度竞争下会变得更慢。
+0

CPU的每个内核的高级别现金是不是分开的?如果是这种情况,线程的数量不应该有问题,因为它们中的每一个都会将它的数组放在CPU的高级别现金中。 要生成随机数,我只使用((i^2)%array_size),所以这不是瓶颈。 – jutky 2010-07-15 11:56:52

+0

@jutky当然,这取决于架构。当然,有多个硬件线程共享相同的最高级别缓存是很常见的。 – 2010-07-15 12:31:28

+0

,正如我在这里看到的:http://en.wikipedia.org/wiki/Nehalem_(microarchitecture)在Nehalem处理器中,每个核心都有高水平的现金。然而它很小,所以这可以解释我没有为小阵列运行时间放慢的原因。谢谢。 – jutky 2010-07-16 06:17:25

0

除了并发问题,缓慢的最可能的原因是内存缓存争用。

如果所有线程都访问相同的存储器,那么当您想要访问它时,在其他处理器内存缓存中可能会有这样的机会。

如果存储是“只读”的,您可以为每个线程提供自己的副本,这将允许JVM &处理器优化内存访问。

+0

我已经添加了我运行测试的代码片段。正如你看到每个线程正在访问它自己的数组,所以不应该有内存争用问题。 – jutky 2010-07-15 11:58:08

0

我用我发布的文章的建议修改了你的测试。在我的2核心机器上(这是我现在所拥有的)结果似乎是合理的(注意,我为每个线程号码运行了2次测试):

也许你可以试试这个? (请注意,我必须稍微修改您的测试(请参阅评论),因为在我可怜的硬件上运行需要很长时间)

另请注意,我使用-server选项运行此测试。

Test with threadNum 1 took 2095717473 ns 
Test with threadNum 1 took 2121744523 ns 
Test with threadNum 2 took 2489853040 ns 
Test with threadNum 2 took 2465152974 ns 
Test with threadNum 4 took 5044335803 ns 
Test with threadNum 4 took 5041235688 ns 
Test with threadNum 8 took 10279012556 ns 
Test with threadNum 8 took 10347970483 ns 

代码:

import java.util.concurrent.*; 

public class Test{ 
    private final static int arrSize = 20000000; 

    public static void main(String[] args) throws Exception { 
     int[] nums = {1,1,2,2,4,4,8,8};//allow hotspot optimization 
     for (int threadNum : nums) { 
      final CyclicBarrier gate = new CyclicBarrier(threadNum+1); 
      final CountDownLatch latch = new CountDownLatch(threadNum); 
      ExecutorService exec = Executors.newFixedThreadPool(threadNum); 
      for(int i=0; i<threadNum; i++){ 
       Runnable test = 
        new Runnable(){ 
        public void run() { 
         try{ 
          gate.await(); 
         }catch(Exception e){ 
          throw new RuntimeException(e); 
         } 
         int array[] = new int[arrSize]; 
         //arrSize * 10 took very long to run so made it 
         // just arrSize. 
         for (long i = 0; i < arrSize; i++) { 
          array[(int) ((i * i) % arrSize)]++; 
         }//for 
         long sum = 0; 
         for (int i = 0; i < arrSize; i++){ 
           sum += array[i]; 
         } 
         if(new Object().hashCode()==sum){ 
           System.out.println("oh"); 
         }//if 
         latch.countDown(); 
         }//run 
        };//test 
       exec.execute(test); 
      }//for 
      gate.await(); 
      long start = System.nanoTime(); 
      latch.await(); 
      long finish = System.nanoTime(); 
      System.out.println("Test with threadNum " + 
       threadNum +" took " + (finish-start) + " ns "); 
      exec.shutdown(); 
      exec.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);   
     }//for 
    }//main 

}//Test 
+0

双核机器并不能真正帮助您调查NUMA类型的问题。 – 2012-11-29 01:09:48