Jan*_*rek 30 java performance java-8 jmh java-time
我只是将模块从旧的Java日期迁移到新的java.time API,并注意到性能大幅下降.它归结为用时区解析日期(我一次解析数百万个).
解析没有时区(yyyy/MM/dd HH:mm:ss)的日期字符串很快 - 比旧的Java日期快2倍,在我的电脑上每秒约1.5M操作.
但是,当模式包含时区(yyyy/MM/dd HH:mm:ss z)时,使用新java.timeAPI 的性能下降约15倍,而使用旧API时,性能与没有时区的速度一样快.请参阅下面的性能基准.
有没有人知道我是否能以某种方式使用新的java.timeAPI 快速解析这些字符串?目前,作为一种解决方法,我使用旧的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();
}
}
Run Code Online (Sandbox Code Playgroud)
以及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
Run Code Online (Sandbox Code Playgroud)
更新:
我刚刚对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"));
}
}
Run Code Online (Sandbox Code Playgroud)
ZoneTextPrinterParser在java.timebundle中调用了一个类,它在内部制作每个parse()调用(via ZoneRulesProvider.getAvailableZoneIds())中所有可用时区的集合的副本,这对于区域解析花费的99%的时间负责.
那么,答案可能是编写我自己的区域解析器,这也不会太好,因为那时我无法构建DateTimeFormatter通道appendPattern().
use*_*551 10
如您的问题和我的评论中所述,每次需要解析时区时,ZoneRulesProvider.getAvailableZoneIds()都会创建一组新的所有可用时区的字符串表示形式(其中的键static final ConcurrentMap<String, ZoneRulesProvider> ZONES).1
幸运的是,a ZoneRulesProvider是一个abstract设计为子类的类.该方法protected abstract Set<String> provideZoneIds()负责填充ZONES.因此,如果子类提前知道要使用的所有时区,则子类只能提供所需的时区.由于该类提供的条目少于默认提供程序(包含数百个条目),因此它有可能显着减少调用时间getAvailableZoneIds().
该ZoneRulesProvider API提供了如何注册一个指令.请注意,提供程序无法取消注册,只能进行补充,因此删除默认提供程序并添加自己的提供程序并不是一件简单的事情.system属性java.time.zone.DefaultZoneRulesProvider定义默认提供程序.如果它返回null(via System.getProperty("...")则加载JVM的臭名昭着的提供者.使用System.setProperty("...", "fully-qualified name of a concrete ZoneRulesProvider class")一个可以提供他们自己的提供者,这是第2段中讨论的提供者.
最后,我建议:
abstract class ZoneRulesProviderprotected abstract Set<String> provideZoneIds()只用所需的时区.我自己没有这样做,但我确信它会因为某些原因而失败,认为它会起作用.
1在问题的评论中建议,调用的确切性质可能在1.8版本之间发生了变化.
编辑:找到更多信息
上述默认ZoneRulesProvider值final class TzdbZoneRulesProvider位于java.time.zone.从路径中读取该类中的区域:( 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
Run Code Online (Sandbox Code Playgroud)
然后,如果找到一种方法来创建仅包含所需区域的类似文件并加载该区域,则可能无法确定性能问题.
这个问题是由于ZoneRulesProvider.getAvailableZoneIds()每次都复制时区集造成的。Bug JDK-8066291跟踪了该问题,并已在 Java SE 9 中修复。它不会向后移植到 Java SE 8,因为该 bug 修复涉及规范更改(该方法现在返回一个不可变集而不是可变集)。
附带说明一下,解析方面的一些其他性能问题已向后移植到 Java SE 8,因此请始终使用最新的更新版本。
| 归档时间: |
|
| 查看次数: |
2195 次 |
| 最近记录: |