Java在许多内核上的扩展比C#差得多?

时间:2012-04-04 12:20:42

标签: c# java .net eclipse jvm-hotspot

我正在测试在用于Java和C#的32核服务器上运行相同功能的许多线程的产生。我使用函数的1000次迭代运行应用程序,使用线程池对1,2,4,8,16或32个线程进行批处理。

在1,2,4,8和16个并发线程中,Java至少是C#的两倍。但是,随着线程数量的增加,间隙关闭,32个线程C#的平均运行时间几乎相同,但Java偶尔需要2000ms(而两种语言通常运行时间约为400ms)。在每次线程迭代所花费的时间内,Java开始变得更糟。

编辑这是Windows Server 2008

EDIT2我已经使用Executor Service线程池更改了以下代码。我还安装了Java 7。

我在热点虚拟机中设置了以下优化:

-XX:+ UseConcMarkSweepGC -Xmx 6000

但它仍然没有让事情变得更好。代码之间的唯一区别是我使用下面的线程池和我们使用的C#版本:

http://www.codeproject.com/Articles/7933/Smart-Thread-Pool

有没有办法让Java更优化? Perhaos你能解释为什么我看到这种性能的大幅下降吗?

是否有更高效的Java线程池?

(请注意,我不是指更改测试功能)

import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;

public class PoolDemo {

    static long FastestMemory = 2000000;
    static long SlowestMemory = 0;
    static long TotalTime;
    static int[] FileArray;
    static DataOutputStream outs;
    static FileOutputStream fout;
    static Byte myByte = 0;

  public static void main(String[] args) throws InterruptedException, FileNotFoundException {

        int Iterations = Integer.parseInt(args[0]);
        int ThreadSize = Integer.parseInt(args[1]);

        FileArray = new int[Iterations];
        fout = new FileOutputStream("server_testing.csv");

        // fixed pool, unlimited queue
        ExecutorService service = Executors.newFixedThreadPool(ThreadSize);
        ThreadPoolExecutor executor = (ThreadPoolExecutor) service;

        for(int i = 0; i<Iterations; i++) {
          Task t = new Task(i);
          executor.execute(t);
        }

        for(int j=0; j<FileArray.length; j++){
            new PrintStream(fout).println(FileArray[j] + ",");
        }
      }

  private static class Task implements Runnable {

    private int ID;

    public Task(int index) {
      this.ID = index;
    }

    public void run() {
        long Start = System.currentTimeMillis();

        int Size1 = 100000;
        int Size2 = 2 * Size1;
        int Size3 = Size1;

        byte[] list1 = new byte[Size1];
        byte[] list2 = new byte[Size2];
        byte[] list3 = new byte[Size3];

        for(int i=0; i<Size1; i++){
            list1[i] = myByte;
        }

        for (int i = 0; i < Size2; i=i+2)
        {
            list2[i] = myByte;
        }

        for (int i = 0; i < Size3; i++)
        {
            byte temp = list1[i];
            byte temp2 = list2[i];
            list3[i] = temp;
            list2[i] = temp;
            list1[i] = temp2;
        }

        long Finish = System.currentTimeMillis();
        long Duration = Finish - Start;
        TotalTime += Duration;
        FileArray[this.ID] = (int)Duration;
        System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");


        if(Duration < FastestMemory){
            FastestMemory = Duration;
        }
        if (Duration > SlowestMemory)
        {
            SlowestMemory = Duration;
        }
    }
  }
}

4 个答案:

答案 0 :(得分:19)

摘要

以下是原始响应,更新1和更新2.更新1讨论了使用并发结构处理测试统计变量周围的竞争条件。更新2是处理竞争条件问题的一种更简单的方法。希望没有更多来自我的更新 - 抱歉响应的长度,但多线程编程很复杂!

原始回复

  

代码之间的唯一区别是即时通讯使用以下内容   线程池

我想说这是一个绝对巨大的差异。当两个语言的线程池实现完全不同的代码块(用户空间编写)时,很难比较两种语言的性能。线程池实现可能会对性能产生巨大影响。

您应该考虑使用Java自己的内置线程池。请参阅ThreadPoolExecutor以及它所属的整个java.util.concurrent包。 Executors类为池提供了方便的静态工厂方法,是一个很好的高级接口。你需要的只是JDK 1.5+,虽然越新越好。其他海报提到的fork / join解决方案也是这个包的一部分 - 如上所述,它们需要1.7 +。

更新1 - 使用并发结构解决竞争条件

您有FastestMemorySlowestMemoryTotalTime设置的竞争条件。对于前两个,您正在进行<>测试,然后执行多个步骤中的设置。这不是原子的;当然,另一个线程有可能在测试和设置之间更新这些值。 +=的{​​{1}}设置也是非原子的:测试并伪装设置。

以下是一些建议的修复方法。

<强> TOTALTIME

此处的目标是TotalTime的线程安全原子+=

TotalTime

FastestMemory / SlowestMemory

此处的目标是在原子步骤中测试和更新每个// At the top of everything import java.util.concurrent.atomic.AtomicLong; ... // In PoolDemo static AtomicLong TotalTime = new AtomicLong(); ... // In Task, where you currently do the TotalTime += piece TotalTime.addAndGet (Duration); FastestMemory,因此在测试和更新步骤之间没有任何线程可以介入以导致竞争条件。

最简单的方法

使用类本身作为监视器来保护变量的测试和设置。我们需要一个包含变量的监视器,以保证同步可见性(感谢@ A.H。用于捕获它。)我们必须使用类本身,因为一切都是SlowestMemory

static

中级方法

您可能不喜欢将整个班级用于监视器,或者通过使用类等来暴露监视器。您可以执行单独的监视器,其本身不包含// In Task synchronized (PoolDemo.class) { if (Duration < FastestMemory) { FastestMemory = Duration; } if (Duration > SlowestMemory) { SlowestMemory = Duration; } } FastestMemory,但是然后,您将遇到同步可见性问题。您可以使用SlowestMemory关键字来解决此问题。

volatile

高级方法

这里我们使用// In PoolDemo static Integer _monitor = new Integer(1); static volatile long FastestMemory = 2000000; static volatile long SlowestMemory = 0; ... // In Task synchronized (PoolDemo._monitor) { if (Duration < FastestMemory) { FastestMemory = Duration; } if (Duration > SlowestMemory) { SlowestMemory = Duration; } } 类而不是监视器。在激烈争论下,这应该比java.util.concurrent.atomic方法表现更好。试试吧,看看。

synchronized

让我知道此后会发生什么。它可能无法解决您的问题,但围绕跟踪您的表现的变量的竞争条件太危险而无法忽略。

我最初将此更新发布为评论但是将其移到此处以便我有空间显示代码。此更新已经过几次迭代 - 感谢A.H.捕获我在早期版本中遇到的错误。此更新中的任何内容都将取代评论中的任何内容。

最后但同样重要的是,涵盖所有这些材料的优秀资源是Java Concurrency in Practice,这是关于Java并发的最佳书籍,也是最好的Java书籍之一。

更新2 - 以更简单的方式解决竞争条件

我最近注意到,除非您添加// At the top of everything import java.util.concurrent.atomic.AtomicLong; . . . . // In PoolDemo static AtomicLong FastestMemory = new AtomicLong(2000000); static AtomicLong SlowestMemory = new AtomicLong(0); . . . . . // In Task long temp = FastestMemory.get(); while (Duration < temp) { if (!FastestMemory.compareAndSet (temp, Duration)) { temp = FastestMemory.get(); } } temp = SlowestMemory.get(); while (Duration > temp) { if (!SlowestMemory.compareAndSet (temp, Duration)) { temp = SlowestMemory.get(); } } ,否则您当前的代码永远不会终止。也就是说,必须终止生成在该池中的非守护程序线程,否则主线程将永远不会退出。这让我想到,既然我们必须等待所有线程退出,为什么不在它们完成后比较它们的持续时间,从而完全绕过executorService.shutdown()等的并发更新?这更简单,可以更快;没有更多的锁定或CAS开销,并且你已经在事情的最后做了FastestMemory的迭代。

我们可以利用的另一件事是,FileArray的并发更新是完全安全的,因为每个线程都写入一个单独的单元格,并且因为在此期间没有读取FileArray写它。

这样,您可以进行以下更改:

FileArray

也可以尝试这种方法。

对于类似的竞争条件,你一定要查看你的C#代码。

答案 1 :(得分:5)

  

...但Java偶尔需要2000毫秒......

并且

    byte[] list1 = new byte[Size1];
    byte[] list2 = new byte[Size2];
    byte[] list3 = new byte[Size3];

hickups将是清理阵列的垃圾收集器。如果你真的想调整它,我建议你为数组使用某种缓存。

修改

这一个

   System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

在内部执行一个或多个synchronized。所以你的高度“并发”代码在这一点上会很好地序列化。只需将其删除并重新测试即可。

答案 2 :(得分:4)

虽然@sparc_spread的答案很棒,但我注意到的另一件事是:

  

我运行应用程序1000次迭代的函数

请注意,HotSpot JVM正在为解释模式工作,用于客户端模式下任何函数的前1.5k次迭代,以及服务器模式下的10k次迭代。具有多个核心的计算机将被HotSpot JVM自动视为“服务器”。

这意味着C#会在Java之前执行JIT(并在机器代码中运行),并且有可能在函数运行时获得更好的性能。尝试将迭代次数增加到20,000,并从10k迭代开始计算。

这里的基本原理是JVM收集有关如何最好地执行JIT的统计数据。它相信你的函数会随着时间的推移而运行很多,所以它需要一个“慢速引导”机制来实现更快的整体运行时间。或者用他们的话来说“20%的功能在80%的时间内都运行”,那么为什么要JIT呢?

答案 3 :(得分:2)

您使用的是java6吗? Java 7具有提高并行编程性能的功能:

http://www.oracle.com/technetwork/articles/java/fork-join-422606.html