Java Vector API求和Array[Double]性能异常问题咨询
Scala Array[Double]求和:向量化实现性能不如循环的疑问
一、实现的三个求和方法
我用Scala实现了三个针对Array[Double]的简易求和方法:
inline def sum2 = var sum: Double = 0.0 var i: Int = 0 inline val species = DoubleVector.SPECIES_256 while i < species.loopBound(vec.length) do val vec2: DoubleVector = DoubleVector.fromArray(species, vec, i) sum = sum + vec2.reduceLanes(VectorOperators.ADD) i += species.length() end while while i < vec.length do sum += vec(i) i += 1 end while sum end sum2 inline def sum: Double = var sum = 0.0 var i = 0; while i < vec.length do sum = sum + vec(i) i = i + 1 end while sum inline def sum3 = var i: Int = 0 val species = DoubleVector.SPECIES_256 var acc = DoubleVector.zero(species) while i < species.loopBound(vec.length) do val vec2: DoubleVector = DoubleVector.fromArray(species, vec, i) acc = acc.add(vec2) // 这行代码好像会导致JMH出错,但单元测试能过,编译也没问题,原因不明 i += species.length() end while var temp = acc.reduceLanes(VectorOperators.ADD) // var temp = 0.0 while i < vec.length do temp += vec(i) i += 1 end while temp end sum3
二、JMH基准测试代码
随后搭建JMH基准测试对比性能:
@BenchmarkMode(Array(Mode.Throughput)) @OutputTimeUnit(TimeUnit.SECONDS) @State(Scope.Thread) @Fork(value = 1) @Warmup(iterations = 3) @Measurement(iterations = 3) abstract class BLASBenchmark: private final val rand: Random = new Random(0); protected def randomDouble(): Double = return rand.nextDouble(); protected def randomDoubleArray(n: Int): Array[Double] = val res = new Array[Double](n); for i <- 0 until n do res(i) = rand.nextDouble(); end for return res; end randomDoubleArray protected def randomFloat(): Float = return rand.nextFloat(); protected def randomFloatArray(n: Int): Array[Float] = val res = new Array[Float](n); for i <- 0 until n do res(i) = rand.nextFloat(); end for return res; end randomFloatArray end BLASBenchmark @State(Scope.Thread) class SumBenchmark extends BLASBenchmark: @Param(Array("3", "10", "100", "10000", "100000")) var len: String = uninitialized; var arr: Array[Double] = uninitialized @Setup(Level.Trial) def setup: Unit = arr = randomDoubleArray(len.toInt); () end setup @Benchmark def sum_loop(bh: Blackhole) = val r = arr.sum bh.consume(r); end sum_loop @Benchmark def sum_vec(bh: Blackhole) = val r = arr.sum2 bh.consume(r); end sum_vec /** 这个方法跑不起来,不清楚原因 */ @Benchmark def sum_vec_alt(bh: Blackhole) = val r = arr.sum3 bh.consume(r); end sum_vec_alt end SumBenchmark
三、本地测试结果
Benchmark (len) Mode Cnt Score Error Units SumBenchmark.sum_loop 3 thrpt 3 650060202.104 ± 176059544.674 ops/s SumBenchmark.sum_loop 10 thrpt 3 422309397.044 ± 19968584.370 ops/s SumBenchmark.sum_loop 100 thrpt 3 30901788.878 ± 354896.502 ops/s SumBenchmark.sum_loop 10000 thrpt 3 116283.523 ± 4388.696 ops/s SumBenchmark.sum_loop 100000 thrpt 3 11487.085 ± 586.552 ops/s SumBenchmark.sum_vec 3 thrpt 3 607547613.267 ± 25376258.940 ops/s SumBenchmark.sum_vec 10 thrpt 3 48259596.149 ± 1205351.697 ops/s SumBenchmark.sum_vec 100 thrpt 3 4604684.325 ± 122538.788 ops/s SumBenchmark.sum_vec 10000 thrpt 3 45689.056 ± 1872.688 ops/s SumBenchmark.sum_vec 100000 thrpt 3 4729.203 ± 83.013 ops/s
疑问与问题
从结果可见,向量化实现比简单while循环慢很多,现咨询:
- 我使用JMH的测试方式是否正确?
- 我的Vector API实现是否存在明显性能损耗?
- 若以上均无问题,该情况是否符合预期?这似乎违背了Vector API的性能提升初衷。
解答
1. JMH测试方式的合理性
整体框架没问题,但有几个优化点:
@Fork(value = 1)仅fork一次容易受进程内其他因素干扰,建议设置为2-3次,降低测试随机性。len用String类型没必要,直接用Int更规范,避免多余的toInt转换。sum_loop调用的Scala标准库arr.sum,底层可能已被JVM自动向量化优化,让手动实现的对比环境不公平。
2. Vector API实现的性能损耗点
你的sum2实现有明显的性能问题:
- 每次循环都做
reduceLanes+标量累加:reduceLanes会把向量转成标量,破坏了向量并行的优势。正确做法是用向量累加(类似sum3的思路),最后再做一次reduceLanes,减少标量与向量的转换开销。 - 手动拆分循环:
species.loopBound拆分剩余元素的逻辑,可能让JVM难以进一步优化,不如用Vector API更简洁的循环结构。 sum3的acc.add(vec2)思路正确,但JMH跑不起来大概率是因为未开启Java 16+的Vector API支持(需要添加--add-modules jdk.incubator.vector启动参数)。
3. 该情况是否符合预期?
这种结果是正常的,原因如下:
- JVM自动向量化优化:Scala标准库的
sum字节码会被C2编译器自动向量化,生成的机器码可能比手动实现更优,JVM会做循环展开、寄存器分配等底层优化。 - 手动实现的额外开销:向量创建、
reduceLanes调用、标量累加等步骤带来的开销,抵消了向量并行的优势,尤其是在数组长度较小时(如3、10),向量初始化的成本远大于并行收益。 - Vector API的适用场景:它更适合计算密集型的复杂操作(如矩阵乘法、多元素数学运算),简单求和这类操作,JVM的自动优化已经足够高效,手动实现很难超越。
内容的提问来源于stack exchange,提问作者Simon
相关产品推荐
相关产品推荐

