我尝试使用Java 1.8和Ubuntu在AWS上的c4.large(具有两个核心的机器)实例内的两个线程中执行计算。添加第二个线程后,计算速度从26秒减慢到每个线程34个。我检查了核心的使用情况,在添加第二个核心后,第二个核心的使用率为100% 在具有两个核心处理器的本地计算机上,两个线程不会减慢线程速度。
c4.large instance:
线程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();
}
}
}
}
答案 0 :(得分:2)
在亚马逊instance types page上,您会看到以下注释:
除了T2之外,每个vCPU都是Intel Xeon核心的超线程。
由于您的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
也就是说,大约有#34;完美&#34;添加第二个线程时进行缩放,当每个线程可以在不同的核心上运行时:每个线程的性能与两个小数位相同。
另一方面,当我运行限制于相同物理核心 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等属于同一物理核心。