Scala中Seq与List填充性能测试异常问题求助
解惑:为什么先调用的Seq/List填充方法总是更慢?
你观察到的这个现象完全是JVM运行时的预热开销导致的,很常见,别担心!我来拆解背后的原因,再给你几个改进测试的方案:
为什么先调用的方法耗时久?
当你第一次调用populateSeq()或者populateList()时,JVM需要完成一堆"初始化工作",这些额外开销都会算在第一次调用的耗时里:
- 类加载与初始化:JVM需要加载
Seq、List以及它们的底层实现类(比如Scala 2里Seq默认就是List,不过依然有类加载的开销),还有字符串常量池的初始化等。 - JIT编译延迟:JVM的即时编译器(JIT)一开始是用解释模式执行代码,只有当方法被多次调用后,才会把热点代码编译成高效的机器码。第一次调用时完全是解释执行,速度自然慢。
- 内存分配预热:第一次分配大量对象(比如上百万个字符串)时,JVM可能需要调整堆内存大小、分配新的内存页,甚至触发一次小型GC来整理空间,这些都是额外的耗时。
怎么得到准确的性能对比结果?
1. 手动添加预热步骤
在正式计时前,先调用几次测试方法让JVM完成预热:
object SeqVsList extends App with LazyLogging { private val numberOfElements = 1234567 // 预热:先调用几次,让JVM完成类加载和JIT编译 populateSeq() populateList() // 正式测试 populateSeq() populateList() def populateSeq(): Unit = { val seqStartTime = System.currentTimeMillis() val aSeq = Seq.fill(numberOfElements)("foo") logger.info(s"Populating Seq took ${System.currentTimeMillis() - seqStartTime} ms") } def populateList(): Unit = { val listStartTime = System.currentTimeMillis() val aList = List.fill(numberOfElements)("bar") logger.info(s"Populating List took ${System.currentTimeMillis() - listStartTime} ms") } }
这样你会发现后面的正式测试结果才是真实的性能对比。
2. 使用专业的基准测试框架(推荐)
手动计时很难处理所有JVM的细节,最靠谱的方式是用JMH(Java Microbenchmark Harness),它专门为JVM上的微基准测试设计,会自动处理预热、多次迭代、统计结果等问题。
简单的JMH示例大概是这样:
import org.openjdk.jmh.annotations._ import java.util.concurrent.TimeUnit @BenchmarkMode(Array(Mode.AverageTime)) @OutputTimeUnit(TimeUnit.MILLISECONDS) @Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @Fork(1) class SeqVsListBenchmark { private val numberOfElements = 1234567 @Benchmark def populateSeq(): Seq[String] = { Seq.fill(numberOfElements)("foo") } @Benchmark def populateList(): List[String] = { List.fill(numberOfElements)("bar") } }
运行这个基准测试,你会得到非常准确的平均耗时对比。
3. 补充:Scala中Seq和List的小知识
顺便提一句:在Scala 2中,Seq的默认实现就是List,所以Seq.fill底层其实和List.fill是一样的逻辑;如果是Scala 3,Seq的默认实现变成了ArraySeq,这时候两者的性能才会有明显差异。所以你的测试在Scala 2里可能结果差不多,Scala 3里会看到区别。
内容的提问来源于stack exchange,提问作者mvee
相关产品推荐
相关产品推荐

