Análisis extremadamente lento de la zona horaria con el nuevo java.API de tiempo


Estaba migrando un módulo de las antiguas fechas de java al nuevo java.tiempo API, y notó una gran caída en el rendimiento. Se reducía al análisis de fechas con zona horaria (analizo millones de ellas a la vez).

El análisis de la cadena de fecha sin una zona horaria (yyyy/MM/dd HH:mm:ss) es rápido, aproximadamente 2 veces más rápido que con la antigua fecha de Java, aproximadamente 1,5 M de operaciones por segundo en mi PC.

Sin embargo, cuando el patrón contiene una zona horaria (yyyy/MM/dd HH:mm:ss z), el rendimiento disminuye aproximadamente 15 veces con la nueva API java.time, mientras que con la antigua API es casi tan rápido como sin una zona horaria. Consulte el punto de referencia de rendimiento a continuación.

¿Alguien tiene una idea si de alguna manera puedo analizar estas cadenas rápidamente usando la nueva API java.time? Por el momento, como solución alternativa, estoy usando la antigua API para analizar y luego convertir el Date a Instantáneo, lo cual no es particularmente agradable.

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

Y los resultados de las operaciones de 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

ACTUALIZACIÓN:

Acabo de hacer algunos perfiles de java.clases de tiempo, y de hecho, el analizador de zona horaria parece ser implementado de manera bastante ineficiente. Solo analizar una zona horaria independiente es responsable de toda la lentitud.

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

Hay una clase llamada ZoneTextPrinterParser en el paquete java.time, que está haciendo internamente una copia del conjunto de todas las zonas horarias disponibles en cada llamada parse() (a través de ZoneRulesProvider.getAvailableZoneIds()), y esto es responsable del 99% del tiempo pasado en el análisis de la zona.

Bueno, una respuesta entonces podría ser escribir mi propia parser de zona, que tampoco sería muy bueno, porque entonces no podría construir el DateTimeFormatter a través de appendPattern().

Author: user1803551, 2015-12-19

2 answers

Como se señaló en su pregunta y en mi comentario, ZoneRulesProvider.getAvailableZoneIds() crea un nuevo conjunto de todas las representaciones de cadenas de las zonas horarias disponibles (las claves de static final ConcurrentMap<String, ZoneRulesProvider> ZONES) cada vez que se necesita analizar una zona horaria.1

Afortunadamente, una ZoneRulesProvider es una clase abstract que está diseñada para ser subclase. El método protected abstract Set<String> provideZoneIds() es responsable de rellenar ZONES. Por lo tanto, una subclase puede proporcionar solo las zonas horarias necesarias si conoce con anticipación todas las zonas horarias que se utilizarán. Puesto que la clase proporcionar menos entradas que el proveedor predeterminado, que contiene cientos de entradas, tiene el potencial de reducir significativamente el tiempo de invocación de getAvailableZoneIds().

La API ZoneRulesProvider proporciona instrucciones sobre cómo registrar una. Tenga en cuenta que los proveedores no pueden ser dados de baja, solo complementados, por lo que no es una simple cuestión de eliminar el proveedor predeterminado y agregar el suyo propio. La propiedad system java.time.zone.DefaultZoneRulesProvider define el proveedor predeterminado. Si devuelve null (vía System.getProperty("...") entonces el proveedor notorio de la JVM está cargado. Usando System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class") uno puede suministrar su propio proveedor, que es el que se discute en el párrafo 2.

Para concluir, sugiero:

  1. Subclase el abstract class ZoneRulesProvider
  2. Implementa el protected abstract Set<String> provideZoneIds() con solo las zonas horarias necesarias.
  3. Establezca la propiedad system en esta clase.

No lo hice yo mismo, pero estoy seguro de que fallará por alguna razón creo que funcionará.


1 Lo es sugirió en los comentarios de la pregunta que la naturaleza exacta de la invocación podría haber cambiado entre las versiones 1.8.

Editar: más información encontrada

El valor predeterminado antes mencionado ZoneRulesProvider se encuentra final class TzdbZoneRulesProvider en java.time.zone. Las regiones de esa clase se leen desde la ruta: JAVA_HOME/lib/tzdb.dat (en mi caso está en el JRE del JDK). Ese archivo de hecho contiene muchas regiones, aquí hay un fragmento:

 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

Entonces si uno encuentra una manera de crear un archivo similar con solo lo necesario en su lugar, los problemas de rendimiento probablemente no seguramente se resolverán.

 10
Author: user1803551,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-06 08:17:55

Este problema es causado por ZoneRulesProvider.getAvailableZoneIds() que copió el conjunto de zonas horarias cada vez. Bug JDK-8066291 rastreó el problema, y se ha solucionado en Java SE 9. No se retroportará a Java SE 8 porque la corrección del error implicó un cambio de especificación (el método ahora devuelve un conjunto inmutable en lugar de uno mutable).

Como nota al margen, algunos otros problemas de rendimiento con el análisis se han retroadaptado a Java SE 8, por lo que siempre use la última versión de la actualización.

 3
Author: JodaStephen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-16 09:49:04