【发布时间】:2016-03-26 06:36:14
【问题描述】:
我刚刚将一个模块从旧的 java 日期迁移到新的 java.time API,并注意到性能大幅下降。它归结为使用时区解析日期(我一次解析数百万个日期)。
解析没有时区的日期字符串 (yyyy/MM/dd HH:mm:ss) 速度很快 - 比使用旧的 java 日期快大约 2 倍,在我的 PC 上每秒大约 150 万次操作。
但是,当模式包含时区 (yyyy/MM/dd HH:mm:ss z) 时,使用新的 java.time API 时性能会下降约 15 倍,而使用旧 API 时,它的速度与没有时区时差不多。请参阅下面的性能基准。
是否有人知道我是否可以使用新的java.time API 以某种方式快速解析这些字符串?目前,作为一种解决方法,我使用旧 API 进行解析,然后将 Date 转换为 Instant,这不是特别好。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(1)
@Fork(1)
@Warmup(iterations = 3)
@Measurement(iterations = 5)
@State(Scope.Thread)
public class DateParsingBenchmark {
private final int iterations = 100000;
@Benchmark
public void oldFormat_noZone(Blackhole bh, DateParsingBenchmark st) throws ParseException {
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
for(int i=0; i<iterations; i++) {
bh.consume(simpleDateFormat.parse("2000/12/12 12:12:12"));
}
}
@Benchmark
public void oldFormat_withZone(Blackhole bh, DateParsingBenchmark st) throws ParseException {
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z");
for(int i=0; i<iterations; i++) {
bh.consume(simpleDateFormat.parse("2000/12/12 12:12:12 CET"));
}
}
@Benchmark
public void newFormat_noZone(Blackhole bh, DateParsingBenchmark st) {
DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
.appendPattern("yyyy/MM/dd HH:mm:ss").toFormatter();
for(int i=0; i<iterations; i++) {
bh.consume(dateTimeFormatter.parse("2000/12/12 12:12:12"));
}
}
@Benchmark
public void newFormat_withZone(Blackhole bh, DateParsingBenchmark st) {
DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
.appendPattern("yyyy/MM/dd HH:mm:ss z").toFormatter();
for(int i=0; i<iterations; i++) {
bh.consume(dateTimeFormatter.parse("2000/12/12 12:12:12 CET"));
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder().include(DateParsingBenchmark.class.getSimpleName()).build();
new Runner(opt).run();
}
}
以及 100K 操作的结果:
Benchmark Mode Cnt Score Error Units
DateParsingBenchmark.newFormat_noZone avgt 5 61.165 ± 11.173 ms/op
DateParsingBenchmark.newFormat_withZone avgt 5 1662.370 ± 191.013 ms/op
DateParsingBenchmark.oldFormat_noZone avgt 5 93.317 ± 29.307 ms/op
DateParsingBenchmark.oldFormat_withZone avgt 5 107.247 ± 24.322 ms/op
更新:
我刚刚对 java.time 类进行了一些分析,实际上,时区解析器的实现似乎效率很低。只是解析一个独立的时区是造成所有缓慢的原因。
@Benchmark
public void newFormat_zoneOnly(Blackhole bh, DateParsingBenchmark st) {
DateTimeFormatter dateTimeFormatter = new DateTimeFormatterBuilder()
.appendPattern("z").toFormatter();
for(int i=0; i<iterations; i++) {
bh.consume(dateTimeFormatter.parse("CET"));
}
}
java.time 包中有一个名为 ZoneTextPrinterParser 的类,它在每个parse() 调用(通过ZoneRulesProvider.getAvailableZoneIds())中内部制作所有可用时区集的副本,这是负责99% 的时间花在区域解析上。
好吧,那么答案可能是编写我自己的区域解析器,这也不太好,因为那时我无法通过appendPattern() 构建DateTimeFormatter。
【问题讨论】:
-
我可以确认在
ZoneTextPrinterParser类中的第 3718 行调用了静态方法ZoneRulesProvider.getAvailableZoneIds(),即return new HashSet<>(ZONES.keySet());。但是,对于一个时区,每次parse调用仅调用一次。创建集合似乎很耗时,因为它包含数百个对象。 -
也许这段代码最近变了?我正在用 java 1.8.60 测试它。在那里,ZoneRulesProvider.getAvailableZoneIds() 在第 3715 行被调用,并且每次都无条件调用,而不仅仅是每个区域一次。
-
我相信所有版本的所有源代码都可以在线获得,并且很容易检查。只是现在我正在为潜在的解决方法写一个答案。顺便说一句,我正在看 1.8.25。
-
@OlivierGrégoire 这是劳力士,我很确定它是准确的......
-
OpenJDK bug 8066291 似乎相关。那里的投诉来自
ZoneIdPrinterParser,但似乎是同一个问题。
标签: java performance java-8 java-time jmh