You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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循环慢很多,现咨询:

  1. 我使用JMH的测试方式是否正确?
  2. 我的Vector API实现是否存在明显性能损耗?
  3. 若以上均无问题,该情况是否符合预期?这似乎违背了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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 10:05:16