java performance java-8 java-time jmh

Análisis de la zona horaria extremadamente lento con la nueva API java.time



performance java-8 (2)

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

Afortunadamente, un ZoneRulesProvider es una clase abstract diseñada para ser subclasificada. El método protected abstract Set<String> provideZoneIds() es responsable de protected abstract Set<String> provideZoneIds() ZONES . Por lo tanto, una subclase puede proporcionar solo las zonas horarias necesarias si sabe de antemano de todas las zonas horarias que se utilizarán. Como 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 de 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. La propiedad del sistema java.time.zone.DefaultZoneRulesProvider define el proveedor predeterminado. Si devuelve un null (a través de System.getProperty("..." ), se carga el notorio proveedor de JVM. Al usar System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class") se puede proporcionar su propio proveedor, que es el discutido en el párrafo segundo.

Para concluir, sugiero:

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

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

1 En los comentarios de la pregunta se sugiere que la naturaleza exacta de la invocación podría haber cambiado entre las versiones 1.8.

Edición: más información encontrada

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

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

Luego, si uno encuentra la manera de crear un archivo similar con solo las zonas necesarias y carga ese, los problemas de rendimiento probablemente no se resolverán.

Acababa de migrar un módulo de las antiguas fechas de Java a la nueva API de java.time y noté una gran caída en el rendimiento. Se redujo a analizar las fechas con la 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 fecha de Java anterior, aproximadamente 1,5 millones 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 API antigua es tan rápido como sin una zona horaria. Vea la prueba de rendimiento a continuación.

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

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 para 100K operaciones:

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

ACTUALIZAR:

Acabo de hacer un perfil de las clases java.time y, de hecho, el analizador de zona horaria parece implementarse de manera bastante ineficiente. El simple análisis de 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 realiza una copia interna del conjunto de todas las zonas horarias disponibles en cada llamada parse() (a través de ZoneRulesProvider.getAvailableZoneIds() ), y esto es responsable del 99% de El tiempo empleado en la zona de análisis.

Bueno, una respuesta podría ser escribir mi propio analizador de zona, lo cual tampoco sería demasiado agradable, porque entonces no podría crear el DateTimeFormatter mediante appendPattern() .


Este problema es causado por ZoneRulesProvider.getAvailableZoneIds() que copió el conjunto de zonas horarias cada vez. El error JDK-8066291 hizo un seguimiento del problema y se solucionó en Java SE 9. No se realizará una copia de seguridad a Java SE 8 porque la corrección de errores 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 vuelto a portar a Java SE 8, por lo que siempre use la última versión actualizada.