【问题标题】:Java is scaling much worse than C# over many cores?Java 在许多内核上的扩展性比 C# 差得多?
【发布时间】:2012-04-12 02:51:42
【问题描述】:

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

在 1、2、4、8 和 16 个并发线程时,Java 的速度至少是 C# 的两倍。然而,随着线程数量的增加,差距缩小,C# 的平均运行时间几乎相同,增加了 32 个线程,但 Java 偶尔需要 2000 毫秒(而两种语言通常运行大约 400 毫秒)。 Java 开始变得更糟,每个线程迭代所花费的时间大幅增加。

编辑这是 Windows Server 2008

EDIT2 我已更改以下代码以显示使用执行器服务线程池。我还安装了 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;
        }
    }
  }
}

【问题讨论】:

  • 什么操作系统?视窗?我认为可能是,如果您使用 Mono,您可能会说,但仍然......
  • 您为什么要尝试比较这两种语言的性能? java语言的主要目标是企业应用程序,如果你需要性能就用c++。
  • 您的应用实际使用了多少个内核?
  • -Xmx 6000 是非法选项(不应该有空格,不带后缀的大小以字节为单位,应该是1024的倍数,并且大于2MB),那怎么办你真的在用吗?
  • 你从哪里得到那个 java 线程池?尝试另一个或自己创建线程以消除任何可能的问题可能是有意义的。

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


【解决方案1】:

总结

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

原始回复

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

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

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

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

您有围绕FastestMemorySlowestMemoryTotalTime 设置的竞争条件。对于前两个,您正在进行&lt;&gt; 测试,然后进行不止一步的设置。这不是原子的;另一个线程肯定有可能在测试和设置之间更新这些值。 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); 

FastestMemory / SlowestMemory

这里的目标是在原子步骤中测试和更新FastestMemorySlowestMemory,因此没有线程可以在测试和更新步骤之间滑入导致竞争条件。

最简单的方法

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

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

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

中间方法

您可能不喜欢为监视器使用整个类,或者通过使用该类来暴露监视器等。您可以做一个单独的监视器,它本身不包含FastestMemorySlowestMemory,但是您将运行同步可见性问题。您可以使用 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 方法执行得更好。试试看。

// 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 - 以更简单的方式解决竞争条件

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

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

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

// 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# 代码是否存在类似的竞争条件。

【讨论】:

  • 刚刚使用了 Executor 服务,我仍然得到相同的时间。我的代码基于此:bobah.net/d4d/source-code/misc/…
  • 您在FastestMemorySlowestMemoryTotalTime 的设置周围存在竞争条件。对于前两个,您正在进行&lt;&gt; 测试,然后设置多个步骤。这不是原子的,肯定有另一个线程会在测试和设置之间更新这些值。所以包装&lt; 测试并设置在synchronized 块中 - 一个代表最快,一个代表最慢。同时,TotalTime+= 设置也是非原子的。 synchronize 该操作或尝试AtomicLong
  • 使用synchronized (PoolDemo.monitor) 会做两件事:第一:你序列化线程——这没关系,也是有意的。第二:JVM 保证离开同步块的新状态 monitor 将对其他 CPU 可见。无法保证其他变量对其他 CPU 可见。在您的情况下,FastestMemory & co 无法保证。您要么必须对它们进行同步,要么将它们放入监视器对象中,或者为它们使用 AtomicXYZ。
  • 啊,你说得对。这都是因为我想避免在类本身上同步(因为一切都是static)。如果我将它们设为volatile,同步解决方案也可以工作 - 这将解决可见性问题。无论如何,是时候编辑我的帖子了——谢谢你看到这个!
  • @sparc_spread:关于“更新 2”:一个更简单的解决方案是放弃 Runnable,使用 Callable 代替它返回所有相关数据。这消除了用户代码中的所有同步。
【解决方案2】:

...但是 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。因此,此时您的高度“并发”代码将被很好地序列化。只需将其删除并重新测试。

【讨论】:

  • +1 我同意这可能是一个因素。一个有趣的实验是将list* 变量从run() 的局部变量更改为Task 的实例变量。然后,在实例化时将每个 Task t 保留在 List 中。这里的目标是将list* 块保留在参考图中,直到程序结束。这意味着直到最后都没有gc。看看那个时候时代是否变得更具确定性会很有趣。当然,您还必须确保堆足够大才能执行此操作。
  • @sparc_spread 我希望 GC 尝试释放一些内存,不断失败然后分配新的来自操作系统的 RAM。这肯定不是最快的方法。那么这样的数据点会提供什么价值呢? IMO 没有。
  • 区分实际成功收集的成本与您描述的颠簸成本会很有趣,我同意这可能会在我设置的场景中开始发生。
【解决方案3】:

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

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

请注意,HotSpot JVM 在客户端模式下任何函数的前 1.5k 次迭代和服务器模式下的 10k 次迭代都在 解释 模式下工作。 HotSpot JVM 自动将具有这么多内核的计算机视为“服务器”。

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

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

【讨论】:

  • 这是否意味着一个只运行一次但具有数百万次迭代的紧密循环的函数不会被 JIT 处理?这听起来像是一个奇怪的选择。
  • +1 我认为您在这里有所作为。对这些类型的测试和你描述的 JIT 行为进行“热身”时间总是很好的,这似乎是必不可少的。我很确定你的回答 + @A.H. 的回答 regc 行为占了大部分发生的事情。我只是在尝试修复比赛条件,但我不确定它们是否是原始海报所描述的原因。
  • @CodeInChaos 这实际上是一个非常慎重的选择。 JIT 成本高;因此,他们为运行的 20% 的代码执行此操作(所有这些引导代码?无需 JIT)。此外,由于他们花时间,他们有机会检查有关函数使用情况的统计数据,并更好地进行 JIT。
  • @AviadBenDov 我没有批评基本思想,但我认为方法粒度太粗略了。一旦其中的一段代码被执行n 次,而不是在方法被调用n 次后,JIT 方法会更有意义。其他包含紧密循环(需要很长时间才能完成)但仅被调用几次的方法不会被 JITed。
  • @CodeInChaos 我明白你现在的意思了。你是对的,但据我所知,JIT 只能在整个方法上执行——因此是限制。不过,在我看来,当我尝试考虑这样一种情况时,我只能想象格式错误的代码,即 80% 的应用程序时间都在运行一个具有非常长的循环且没有循环内方法调用的方法.. :-)跨度>
【解决方案4】:

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

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

【讨论】:

  • 我只是要快速安装版本 7
  • 使用 java 7 您可以使用 fork 框架来提高性能。这里有一个stackoverflow线程正在讨论它stackoverflow.com/questions/7926864/…
  • 网站上的当前版本是 1.7.0_3 但这可能是最近的版本(我认为 Java 7 的早期版本有一些问题,所以他们可能没有将它们直接下载) .
猜你喜欢
  • 2021-07-14
  • 2016-04-12
  • 2014-02-10
  • 2021-05-19
  • 2012-04-23
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
  • 2020-11-29
相关资源
最近更新 更多