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

2022-09-03 05:07:43

我正在测试在 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 我已经更改了下面的代码,以显示使用执行器服务线程池。我还安装了Java 7。

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

-XX:+UseConcMarkSweepGC -Xmx 6000

但它仍然没有让事情变得更好。代码之间的唯一区别是im使用下面的线程池和我们使用的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;
        }
    }
  }
}

答案 1

总结

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

原始响应

代码之间的唯一区别是im使用下面的线程池

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

您应该考虑使用 Java 自己的内置线程池。请参阅 ThreadPoolExecutor 和它所属的整个 java.util.concurrent 包。Executors 类具有方便的池静态工厂方法,并且是一个很好的高级接口。您所需要的只是JDK 1.5 +,尽管越新越好。其他海报提到的分叉/连接解决方案也是此软件包的一部分 - 如前所述,它们需要1.7 +。

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

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

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

总时间

这里的目标是线程安全的,原子的。+=TotalTime

// 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); 

最快记忆/最慢记忆

这里的目标是测试和更新,并且每个都在一个原子步骤中,因此任何线程都不能在测试和更新步骤之间滑入以导致争用情况。FastestMemorySlowestMemory

最简单的方法

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

// In Task
synchronized (PoolDemo.class) {
    if (Duration < FastestMemory) {
        FastestMemory = Duration;
    }

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

中间方法

您可能不喜欢为监视器获取整个类,或者使用类公开监视器等。您可以执行一个本身不包含 和 的单独监视器,但随后会遇到同步可见性问题。您可以使用关键字来解决此问题。FastestMemorySlowestMemoryvolatile

// 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.atomicsynchronized

// 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();
    }
}

让我知道这之后会发生什么。它可能无法解决您的问题,但是围绕跟踪您的表现的变量的争用条件太危险了,不容忽视。

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

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

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

我最近注意到,除非您添加.也就是说,必须终止位于该池中的非守护程序线程,否则主线程将永远不会退出。这让我想到,既然我们必须等待所有线程退出,为什么不在它们完成后比较它们的持续时间,从而完全绕过 等的并发更新呢?这更简单,可以更快;没有更多的锁定或CAS开销,无论如何,您已经在事情结束时进行了迭代。executorService.shutdown()FastestMemoryFileArray

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

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

// In PoolDemo
// This part is the same, just so you know where we are
for(int i = 0; i<Iterations; i++) {
    Task t = new Task(i);
    executor.execute(t);
}

// CHANGES BEGIN HERE
// Will block till all tasks finish. Required regardless.
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);

for(int j=0; j<FileArray.length; j++){
    long duration = FileArray[j];
    TotalTime += duration;

    if (duration < FastestMemory) {
        FastestMemory = duration;
    }

    if (duration > SlowestMemory) {
        SlowestMemory = duration;
    }

    new PrintStream(fout).println(FileArray[j] + ",");
}

. . . 

// In Task
// Ending of Task.run() now looks like this
long Finish = System.currentTimeMillis();
long Duration = Finish - Start;
FileArray[this.ID] = (int)Duration;
System.out.println("Individual Time " + this.ID + " \t: " + (Duration) + " ms");

也给这种方法一个机会。

您绝对应该检查 C# 代码是否存在类似的争用条件。


答案 2

...但是Java偶尔会花费2000ms...

    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


推荐