在c4.large AWS实例中运行的Java应用程序性能低下

3
我正在尝试在AWS上的c4.large实例(具有两个核心的机器)上使用Java 1.8和Ubuntu在两个线程中执行计算。在添加第二个线程后,每个线程的计算速度从26秒放慢到34秒。我检查了核心的使用情况,添加第二个线程后,第二个核心的使用率达到了100%。 在具有双核处理器的本地计算机上,两个线程不会减慢线程速度。

c4.large实例:
线程0启动
线程0时间:26秒
线程1启动
线程0时间:29秒
线程1时间:34秒
线程0时间 :34秒
线程1时间:34秒
线程0时间:34秒

如何改进下面的代码或更改系统配置以提高性能?
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.DoubleUnaryOperator;
import java.util.stream.DoubleStream;

public class TestCalculate {

    private Random rnd = ThreadLocalRandom.current();

    private DoubleStream randomPoints(long points, double a, double b) {
        return  rnd.doubles(points)
                .limit(points)
                        .map(d -> a + d * (b - a));
    }

    public static void main(String[] args) throws SecurityException, IOException {
        DoubleUnaryOperator du = x -> (x * Math.sqrt(23.35 * x * x) / Math.sqrt(34.54653324234324 * x) / Math.sqrt(213.3123)) * Math.sqrt(1992.34513213124 / x) / 88392.3 * x + 3.234324;


        for (int i=0 ; i < 2; i++){
            int j = i ;
            new Thread(() -> {
                TestCalculate test = new TestCalculate();
                int x = 0;
                System.out.println("Thread "+j+" start");
                long start = System.currentTimeMillis();
                while (x++ < 4) {
                    double d = test.randomPoints(500_000_000l, 2, 10).map(du).sum();
                    long end = (System.currentTimeMillis() - start) / 1000;
                    System.out.println("Thread "+j+" time: "+end+" seconds, result: "+d);
                    start = System.currentTimeMillis();
                }
            }).start();

            try {
                Thread.sleep(40_000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        }
    }
}
1个回答

2
在亚马逊实例类型页面上,你会发现这个注释:
每个 vCPU 都是一个 Intel Xeon 核的超线程,不包括 T2。
因此,由于你的实例有 2 个 vCPU,你实际上得到的是单个 CPU 核心的两个超线程,而不是两个独立的核心。鉴于这一点,当运行两个线程时,各个线程将会争夺同一核心上的资源,所以并不能使吞吐量翻倍。当添加第二个线程时,你看到的吞吐量增加约53%,这实际上意味着该代码非常适合使用超线程技术,因为第二个超线程的平均加速比通常在30%左右。
你可以在本地复制这个结果,但是在我的Skylake CPU上,超线程惩罚显然要低得多。当我在我的四核八超线程上通过将其限制在两个不同的物理内核上运行稍微修改过的版本0 TestCalculate,就可以观察到这一点。
taskset -c 0,1 java stackoverflow.TestCalculate

我得到了以下结果:
Thread 0 start
Thread 0: time:  2.21 seconds, result: 161774948.858291
Thread 0: time:  2.18 seconds, result: 161774943.838121
Thread 0: time:  2.18 seconds, result: 161774946.789039
Thread 1 start
Thread 1: time:  2.18 seconds, result: 161774945.535877
Thread 0: time:  2.18 seconds, result: 161774947.073892
Thread 1: time:  2.18 seconds, result: 161774937.356786
Thread 0: time:  2.18 seconds, result: 161774940.460682
Thread 1: time:  2.18 seconds, result: 161774944.699141
Thread 0: time:  2.18 seconds, result: 161774941.643486
Thread 0 stop
Thread 1: time:  2.18 seconds, result: 161774943.018521
Thread 1: time:  2.18 seconds, result: 161774941.866168
Thread 1: time:  2.18 seconds, result: 161774944.035612
Thread 1 stop

换句话说,当添加第二个线程时,如果每个线程可以在不同的内核上运行,则近似地实现了“完美”缩放:每个线程的性能相同,精确到小数点后两位。

另一方面,当我将进程限制在相同的物理内核1上运行时:

taskset -c 0,4 java stackoverflow.TestCalculate

我得到了以下结果:
Thread 0 start
Thread 0: time:  2.22 seconds, result: 161774949.278913
Thread 0: time:  2.19 seconds, result: 161774932.329415
Thread 0: time:  2.18 seconds, result: 161774943.604470
Thread 1 start
Thread 0: time:  2.31 seconds, result: 161774951.630203
Thread 1: time:  2.31 seconds, result: 161774951.695466
Thread 0: time:  2.31 seconds, result: 161774939.631680
Thread 1: time:  2.31 seconds, result: 161774943.523282
Thread 0: time:  2.32 seconds, result: 161774948.153244
Thread 0 stop
Thread 1: time:  2.32 seconds, result: 161774956.985513
Thread 1: time:  2.18 seconds, result: 161774950.335522
Thread 1: time:  2.18 seconds, result: 161774941.739148
Thread 1: time:  2.18 seconds, result: 161774946.275329
Thread 1 stop

因此,当在相同的核心上运行时,速度会减慢6%。这意味着该代码非常适合超线程,因为6%的减速意味着添加超线程可获得94%的收益! Skylake具有多个微架构改进,特别有助于超线程场景,这或许可以解释您的c4.large结果(Haswell架构)与我的结果之间的差异。您可以尝试在EC2 C5实例上运行,因为它们使用Skylake架构:如果下降幅度小得多,则可以证实这个理论。

0 修改后使迭代时间缩短了10倍,并在单个线程的3次迭代后确定性地启动第二个线程。

1 在我的计算机上,逻辑CPU 0和4、1和5等属于同一个物理核心。


创建了TestCalculate类用于每个线程(每个线程都有其他的Random对象),但一个线程仍然需要26秒,两个线程需要33秒。 - Mariusz Chachuła
@MariuszChachuła - 你能否更新你的问题中的代码,使用一个单独的TestCalculate对象来处理每个线程的新版本? - BeeOnRope
感谢您的帮助。这是一个问题。我编辑了代码并为每个线程添加了TestCalculate。 - Mariusz Chachuła

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接