使用新的 java.time API 解析时区的速度非常慢

2022-09-01 11:05:14

我刚刚将一个模块从旧的java日期迁移到新的java.time API,并注意到性能大幅下降。它归结为解析带有时区的日期(我一次解析数百万个)。

解析没有时区()的日期字符串速度很快 - 比旧的java日期快2倍,在我的PC上每秒大约1.5M操作。yyyy/MM/dd HH:mm:ss

但是,当模式包含时区 () 时,使用新 API 的性能会下降约 15 倍,而使用旧 API,性能的速度与没有时区的速度一样快。请参阅下面的性能基准。yyyy/MM/dd HH:mm:ss zjava.time

有没有人知道我是否可以使用新的API以某种方式快速解析这些字符串?目前,作为一种解决方法,我正在使用旧的API进行解析,然后将转换为Instant,这并不是特别好。java.timeDate

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"));
    }
}

捆绑包中调用了一个类,该类在内部创建每个调用(via)中所有可用时区的集合的副本,这占区域解析所花费时间的99%。ZoneTextPrinterParserjava.timeparse()ZoneRulesProvider.getAvailableZoneIds()

好吧,那么答案可能是编写我自己的区域解析器,这也不会太好,因为那样我就无法构建via 。DateTimeFormatterappendPattern()


答案 1

如您的问题和我的注释中所述,每次需要解析时区时,都会创建一组所有可用时区的字符串表示形式(的键)。1 个ZoneRulesProvider.getAvailableZoneIds()static final ConcurrentMap<String, ZoneRulesProvider> ZONES

幸运的是,a 是一个被设计为子类化的类。该方法负责填充 。因此,如果子类提前知道要使用的所有时区,则该子类只能提供所需的时区。由于该类提供的条目少于默认提供程序(包含数百个条目),因此它可能会显著减少 的调用时间。ZoneRulesProviderabstractprotected abstract Set<String> provideZoneIds()ZONESgetAvailableZoneIds()

ZoneRulesProvider API 提供了有关如何注册一个指令的说明。请注意,提供程序不能取消注册,只能进行补充,因此删除默认提供程序并添加自己的提供程序并不是一件容易的事。系统属性定义默认提供程序。如果它返回(via ),则加载JVM的臭名昭着的提供者。使用一个可以提供自己的提供者,这是第2段中讨论的那个。java.time.zone.DefaultZoneRulesProvidernullSystem.getProperty("..."System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class")

最后,我建议:

  1. 子类abstract class ZoneRulesProvider
  2. 仅使用所需的时区实现 。protected abstract Set<String> provideZoneIds()
  3. 将系统属性设置为此类。

我自己没有这样做,但我相信它会因为某种原因而失败,认为它会起作用。


1 在问题的评论中表明,在1.8版本之间,调用的确切性质可能已经改变。

编辑:找到更多信息

上述缺省值位于 中。该类中的区域是从路径中读取的:(在我的情况下,它位于JDK的JRE中)。该文件确实包含许多区域,下面是一个片段:ZoneRulesProviderfinal class TzdbZoneRulesProviderjava.time.zoneJAVA_HOME/lib/tzdb.dat

 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

然后,如果找到一种方法来创建一个仅包含所需区域的类似文件并加载该文件,则性能问题可能无法肯定得到解决。


答案 2

此问题是由 每次复制时区集的哪个引起的。Bug JDK-8066291 跟踪了该问题,并且已在 Java SE 9 中修复。它不会向后移植到 Java SE 8,因为 bug 修复涉及指定更改(该方法现在返回不可变集而不是可变集)。ZoneRulesProvider.getAvailableZoneIds()

作为旁注,解析的其他一些性能问题已向后移植到 Java SE 8,因此请始终使用最新的更新版本。


推荐