使用新 java.time API 解析时区非常慢
Extremely slow parsing of time zone with the new java.time API
我刚刚将一个模块从旧的 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 classes 进行了一些分析,实际上,时区解析器的实现似乎效率很低。仅仅解析一个独立的时区是造成所有缓慢的原因。
@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
的 class,它在每个 parse()
调用中在内部复制所有可用时区的集合(通过 ZoneRulesProvider.getAvailableZoneIds()
),这占区域解析时间的 99%。
嗯,一个答案可能是编写我自己的区域解析器,这也不太好,因为那样我就无法通过 appendPattern()
.[=26= 构建 DateTimeFormatter
]
如您的问题和我的评论所述,每次时区需要时,ZoneRulesProvider.getAvailableZoneIds()
都会创建一组新的所有可用时区的字符串表示(static final ConcurrentMap<String, ZoneRulesProvider> ZONES
的键)待解析。1
幸运的是,ZoneRulesProvider
是一个 abstract
class,它被设计为被子class编辑。方法 protected abstract Set<String> provideZoneIds()
负责填充 ZONES
。因此,如果 subclass 提前知道要使用的 all 个时区,则它可以仅提供所需的时区。由于 class 将提供比包含数百个条目的默认提供程序更少的条目,因此它有可能显着减少 getAvailableZoneIds()
.
的调用时间
ZoneRulesProvider API 提供了有关如何注册的说明。请注意,提供者无法注销,只能补充,因此删除默认提供者并添加您自己的提供者并不是一件简单的事情。系统 属性 java.time.zone.DefaultZoneRulesProvider
定义默认提供者。如果它 returns null
(通过 System.getProperty("..."
),那么 JVM 臭名昭著的提供程序将被加载。使用 System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class")
可以提供他们自己的供应商,这是第 2 段中讨论的供应商。
最后,我建议:
- 分class
abstract class ZoneRulesProvider
- 仅使用所需时区实施
protected abstract Set<String> provideZoneIds()
。
- 将系统 属性 设置为此 class。
我自己没有这样做,但我确信它会因为某种原因失败认为它会起作用。
1 问题的评论中建议调用的确切性质可能在 1.8 版本之间发生了变化。
编辑: 找到更多信息
上述默认ZoneRulesProvider
是final class TzdbZoneRulesProvider
位于java.time.zone
。 class 中的区域是从路径 JAVA_HOME/lib/tzdb.dat
中读取的(在我的例子中,它在 JDK 的 JRE 中)。该文件确实包含许多区域,这里是一个片段:
TZDB 2014cJ Africa/Abidjan Africa/Accra Africa/Addis_Ababa Africa/Algiers
Africa/Asmara
Africa/Asmera
Africa/Bamako
Africa/Bangui
Africa/Banjul
Africa/Bissau Africa/Blantyre Africa/Brazzaville Africa/Bujumbura Africa/Cairo Africa/Casablanca Africa/Ceuta Africa/Conakry Africa/Dakar Africa/Dar_es_Salaam Africa/Djibouti
Africa/Douala Africa/El_Aaiun Africa/Freetown Africa/Gaborone
Africa/Harare Africa/Johannesburg Africa/Juba Africa/Kampala Africa/Khartoum
Africa/Kigali Africa/Kinshasa Africa/Lagos Africa/Libreville Africa/Lome
Africa/Luanda Africa/Lubumbashi
Africa/Lusaka
Africa/Malabo
Africa/Maputo
Africa/Maseru Africa/Mbabane Africa/Mogadishu Africa/Monrovia Africa/Nairobi Africa/Ndjamena
Africa/Niamey Africa/Nouakchott Africa/Ouagadougou Africa/Porto-Novo Africa/Sao_Tome Africa/Timbuktu Africa/Tripoli Africa/Tunis Africa/Windhoek America/Adak America/Anchorage America/Anguilla America/Antigua America/Araguaina America/Argentina/Buenos_Aires America/Argentina/Catamarca America/Argentina/ComodRivadavia America/Argentina/Cordoba America/Argentina/Jujuy America/Argentina/La_Rioja America/Argentina/Mendoza America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia
America/Aruba America/Asuncion America/Atikokan America/Atka
America/Bahia
然后,如果找到一种方法来创建仅包含所需区域的类似文件并加载该文件,性能问题可能不会肯定会得到解决。
这个问题是由于ZoneRulesProvider.getAvailableZoneIds()
每次都复制时区集造成的。 Bug JDK-8066291 跟踪了这个问题,它已在 Java SE 9 中修复。它不会被移植到 Java SE 8,因为 Bug 修复涉及规范更改(方法现在 returns 一个不可变的集合而不是一个可变的集合。
附带说明,其他一些与解析相关的性能问题已被移植到 Java SE 8,因此请始终使用最新的更新版本。
我刚刚将一个模块从旧的 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 classes 进行了一些分析,实际上,时区解析器的实现似乎效率很低。仅仅解析一个独立的时区是造成所有缓慢的原因。
@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
的 class,它在每个 parse()
调用中在内部复制所有可用时区的集合(通过 ZoneRulesProvider.getAvailableZoneIds()
),这占区域解析时间的 99%。
嗯,一个答案可能是编写我自己的区域解析器,这也不太好,因为那样我就无法通过 appendPattern()
.[=26= 构建 DateTimeFormatter
]
如您的问题和我的评论所述,每次时区需要时,ZoneRulesProvider.getAvailableZoneIds()
都会创建一组新的所有可用时区的字符串表示(static final ConcurrentMap<String, ZoneRulesProvider> ZONES
的键)待解析。1
幸运的是,ZoneRulesProvider
是一个 abstract
class,它被设计为被子class编辑。方法 protected abstract Set<String> provideZoneIds()
负责填充 ZONES
。因此,如果 subclass 提前知道要使用的 all 个时区,则它可以仅提供所需的时区。由于 class 将提供比包含数百个条目的默认提供程序更少的条目,因此它有可能显着减少 getAvailableZoneIds()
.
ZoneRulesProvider API 提供了有关如何注册的说明。请注意,提供者无法注销,只能补充,因此删除默认提供者并添加您自己的提供者并不是一件简单的事情。系统 属性 java.time.zone.DefaultZoneRulesProvider
定义默认提供者。如果它 returns null
(通过 System.getProperty("..."
),那么 JVM 臭名昭著的提供程序将被加载。使用 System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class")
可以提供他们自己的供应商,这是第 2 段中讨论的供应商。
最后,我建议:
- 分class
abstract class ZoneRulesProvider
- 仅使用所需时区实施
protected abstract Set<String> provideZoneIds()
。 - 将系统 属性 设置为此 class。
我自己没有这样做,但我确信它会因为某种原因失败认为它会起作用。
1 问题的评论中建议调用的确切性质可能在 1.8 版本之间发生了变化。
编辑: 找到更多信息
上述默认ZoneRulesProvider
是final class TzdbZoneRulesProvider
位于java.time.zone
。 class 中的区域是从路径 JAVA_HOME/lib/tzdb.dat
中读取的(在我的例子中,它在 JDK 的 JRE 中)。该文件确实包含许多区域,这里是一个片段:
TZDB 2014cJ Africa/Abidjan Africa/Accra Africa/Addis_Ababa Africa/Algiers
Africa/Asmara
Africa/Asmera
Africa/Bamako
Africa/Bangui
Africa/Banjul
Africa/Bissau Africa/Blantyre Africa/Brazzaville Africa/Bujumbura Africa/Cairo Africa/Casablanca Africa/Ceuta Africa/Conakry Africa/Dakar Africa/Dar_es_Salaam Africa/Djibouti
Africa/Douala Africa/El_Aaiun Africa/Freetown Africa/Gaborone
Africa/Harare Africa/Johannesburg Africa/Juba Africa/Kampala Africa/Khartoum
Africa/Kigali Africa/Kinshasa Africa/Lagos Africa/Libreville Africa/Lome
Africa/Luanda Africa/Lubumbashi
Africa/Lusaka
Africa/Malabo
Africa/Maputo
Africa/Maseru Africa/Mbabane Africa/Mogadishu Africa/Monrovia Africa/Nairobi Africa/Ndjamena
Africa/Niamey Africa/Nouakchott Africa/Ouagadougou Africa/Porto-Novo Africa/Sao_Tome Africa/Timbuktu Africa/Tripoli Africa/Tunis Africa/Windhoek America/Adak America/Anchorage America/Anguilla America/Antigua America/Araguaina America/Argentina/Buenos_Aires America/Argentina/Catamarca America/Argentina/ComodRivadavia America/Argentina/Cordoba America/Argentina/Jujuy America/Argentina/La_Rioja America/Argentina/Mendoza America/Argentina/Rio_Gallegos America/Argentina/Salta America/Argentina/San_Juan America/Argentina/San_Luis America/Argentina/Tucuman America/Argentina/Ushuaia
America/Aruba America/Asuncion America/Atikokan America/Atka
America/Bahia
然后,如果找到一种方法来创建仅包含所需区域的类似文件并加载该文件,性能问题可能不会肯定会得到解决。
这个问题是由于ZoneRulesProvider.getAvailableZoneIds()
每次都复制时区集造成的。 Bug JDK-8066291 跟踪了这个问题,它已在 Java SE 9 中修复。它不会被移植到 Java SE 8,因为 Bug 修复涉及规范更改(方法现在 returns 一个不可变的集合而不是一个可变的集合。
附带说明,其他一些与解析相关的性能问题已被移植到 Java SE 8,因此请始终使用最新的更新版本。