我正在尝试使用 Java 1.8 和 Ubuntu 在 AWS 上的 c4.large(具有两个内核的机器)实例中的两个线程中执行计算。添加第二个线程后,每个线程的计算速度从 26 秒减慢到 34 秒。我检查了内核的使用情况,添加第二个线程后,第二个内核的使用率为 100%。 在具有双核处理器的本地计算机上,两个线程不会减慢线程速度。
c4.large instance:
Thread 0 start
Thread 0 time: 26 seconds
Thread 1 start
Thread 0 time: 29 seconds
Thread 1 time: 34 seconds
Thread 0 time: 34 seconds
Thread 1 time: 34 seconds
Thread 0 time: 34 seconds
如何改进以下代码或更改系统配置以提高性能?
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();
}
}
}
}
最佳答案
在亚马逊上 instance types page你找到这张便条:
Each vCPU is a hyperthread of an Intel Xeon core except for T2.
由于您的 c4.large
实例有 2 个 vCPU,您真正得到的是单个 CPU 核心的两个超线程,而不是两个独立的核心。鉴于此,完全可以预期运行两个线程不会使吞吐量翻倍,因为两个线程都在争夺同一核心上的资源。添加第二个线程时,您看到吞吐量增加了约 53%,这实际上意味着这段代码对超线程非常友好,因为第二个超线程的平均加速通常被认为在 30% 的范围内。
您可以在本地重现此结果,但在我的 Skylake CPU 上,超线程惩罚显然要低得多。当我运行 slightly modified version0 TestCalculate
通过将它限制在我的 4 核、8 超线程上的两个不同的物理内核,如下所示:
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% 的 yield ! Skylake 有几个微架构改进,专门帮助超线程场景,这也许可以解释您的 c4.large
结果(Haswell 架构)和我的结果之间的差异。您可以尝试使用 EC2 C5 实例,因为它们使用 Skylake 架构:如果跌幅小得多,则可以证实这一理论。
0 进行了修改,使迭代时间缩短了 10 倍,并在使用单个线程进行 3 次迭代后确定性地启动第二个线程。
1 在我的机器上,逻辑 CPU 0 和 4、1 和 5 等属于同一个物理内核。
关于java - 在 c4.large AWS 实例中运行的 Java 应用程序性能低下,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49845511/