JSR 310 – Java 8 Date/Time library performance (as well as Joda Time 2.3 and j.u.Calendar)

by Mikhail Vorontsov

Introduction

This is the third date/time article in this blog. I advice you to look at the other two as well: java.util.Date, java.util.Calendar and java.text.SimpleDateFormat and Joda Time library performance.

This article is a short overview of the new Java 8 date/time implementation also known as JSR-310. I will compare JSR-310 implementation and performance with their counterparts from Joda Time library as well as with the good old java.util.GregorianCalendar. This review was written and tested on Java 8 ea b121.

All new Java 8 classes are implemented around the human time concept – separate fields for years, months, days, hours, minutes, seconds, and, in line with the current fashion, nanoseconds. Their counterpart is a machine time – number of milliseconds since epoch, which you may obtain, for example, via System.currentTimeMillis() call. In order to convert time between 2 these systems you will need to know which timezone to use. A timezone defines the offset from UTC used in conversions. Offset calculation may require the use of transition table or transition rules defining when the daylight savings changes happen. Sometime it may become a performance bottleneck.

JSR-310 implementation was inspired by a Joda Time library – both libraries have the similar interface, but their implementations differ greatly – Java 8 classes are built around the human time, but Joda Time is using machine time inside. As a result, if you are looking for the fastest implementation, I would recommend you to use Java 8 classes except the situations when:

  • You can’t use Java 8 (yeah, not many people can use it before the first official release…)
  • You work strictly with the machine time inside a few day range (in this case manual long/int based implementation will be faster).
  • You have to parse timestamps including offset/zone id.

Tests

If you are reading this article, you are probably interested in the performance comparison of all these classes. There will be 2 tables for my local Australia/Sydney timezone – one for dates in 2000 and another for the 2014 dates. The reason behind it is a different performance of a few Joda classes on the current dates (dates after the Joda Time library release). I wanted to ensure that JDK implementations are not affected by the same problem.

Both year and timezone are the arguments for the test suite in the NewDateTest.java file from the article test suite (see a link at the end of an article). I do not recommend to run it on any computers which CPU is likely to throttle under a load – I was not able to get the stable results on my ultrabook, so I ran it on my workstation with Xeon E5-2650 CPU (8 physical, 16 logical cores @ 2-2.8Ghz) with 128 Gb RAM.

The following tests were written:

  • createFromNow – create an instance of a given class from the current timestamp
  • createFromMillis – take a timestamp at “year.01.10T00:00:00” in the given timezone, use it to construct instances
  • createFromYYMMDD – create all dates in the given year in the given timezone
  • createFromHHMMSS – create every second (86400 timestamp per day) for the 1st January of a given year in the given timezone
  • plusDays – create a 1st January of a given year, add 1 day 365 times (in a loop)
  • plusSeconds – “year.01.10T00:00:00” in the given timezone, add 1 second 360 times (in a loop)
  • parse – parse “year-12-03T10:15:30” for datetime classes, “year-12-03” for date classes or “10:15:30” for time classes. The exception are 3 timezone-based Java 8 classes – they parse a value specified in the Javadoc of their parse methods.
  • format – convert a value from a given year in a given timezone into string. The format is specified in the “parse” test description above (this time without exceptions).

Test results

All test results show a number of operations per second. Dash means that a given operation is not supported for the given class. createFromHHMMSS for datetime classes actually creates an instance out of 6 components – year, month, day, hour, min, sec.

Year: 2000; timezone = Australia/Sydney

  createFromNow createFromMillis createFromYYMMDD createFromHHMMSS plusDays plusSeconds parse format
Java 8 LocalDate 6,887,052 57,485,029 21,407,624 1,908,396 3,345,600
Java 8 LocalDateTime 6,476,683 10,729,613 33,168,805 27,359,088 19,010,416 23,778,071 604,047 1,122,586
Java 8 LocalTime 8,298,755 42,708,848 35,294,117 998,302 1,437,194
Java 8 OffsetDateTime 5,263,157 10,214,504 26,698,450 27,648,000 16,493,447 17,069,701 86,935 1,035,196
Java 8 OffsetTime 6,207,324 14,858,841 38,348,868 27,149,321 125,505 1,467,566
Java 8 ZonedDateTime 5,646,527 9,615,384 8,383,233 8,275,862 9,530,026 10,449,927 70,891 1,078,167
Java GregorianCalendar 4,288,164 2,571,355 3,502,371 3,578,380 6,937,844 8,670,520 481,741 1,730,702
Joda DateTime 21,276,595 34,013,605 10,020,876 10,502,005 12,547,267 29,149,797 896,619 2,088,991
Joda MutableDateTime 20,618,556 31,847,133 10,014,903 10,658,771 13,685,789 41,618,497 893,176 2,080,299
Joda LocalDate 16,051,364 21,276,595 42,775,302 19,353,128 1,841,959 2,811,357
Joda LocalDateTime 23,584,905 34,364,261 23,260,643 25,829,596 48,796,791 48,979,591 954,289 1,893,222
Joda LocalTime 13,245,033 15,797,788 6,400,474 19,501,625 1,539,645 1,650,982

Year: 2014; timezone = Australia/Sydney

  createFromNow createFromMillis createFromYYMMDD createFromHHMMSS plusDays plusSeconds parse format
Java 8 LocalDate 6,756,756 56,328,583 21,196,283 1,900,418 3,377,237
Java 8 LocalDateTime 6,329,113 7,209,805 32,526,621 37,861,524 18,689,196 23,047,375 608,679 1,135,460
Java 8 LocalTime 8,244,023 42,624,568 35,608,308 1,005,025 1,435,750
Java 8 OffsetDateTime 5,162,622 7,067,137 26,561,264 26,494,940 17,152,255 19,448,946 89,837 1,068,490
Java 8 OffsetTime 6,540,222 8,920,606 41,759,304 29,459,901 213,944 1,406,271
Java 8 ZonedDateTime 5,396,654 6,540,222 8,463,476 8,023,774 6,823,705 6,908,462 70,609 1,083,423
Java GregorianCalendar 4,286,326 2,770,850 3,525,708 3,616,121 7,168,106 8,651,766 492,975 1,686,909
Joda DateTime 20,746,887 33,112,582 2,320,441 1,952,189 2,439,513 27,906,976 711,237 2,085,505
Joda MutableDateTime 20,533,880 32,154,340 2,323,731 1,944,194 2,530,329 40,494,938 721,240 2,097,315
Joda LocalDate 16,000,000 21,231,422 43,104,554 19,312,169 1,831,837 2,866,972
Joda LocalDateTime 23,529,411 34,482,758 24,163,969 27,067,669 48,731,642 48,780,487 923,872 1,924,927
Joda LocalTime 13,297,872 15,822,784 6,386,753 19,966,722 1,575,299 1,690,617

All Java 8 and Joda classes internal representation at a glance

This table will show you the internal structure of all date/time classes described in this article, except GregorianCalendar (it is too large and rather unclear for such reference table).

Class name Internal structure
Java 8 LocalDate (immutable) int year, short month, short day
Java 8 LocalTime (immutable) byte hour, byte min, byte sec, int nanos
Java 8 LocalDateTime (immutable) LocalDate date, LocalTime time
Java 8 OffsetDateTime (immutable) LocalDateTime dateTime, ZoneOffset offset
Java 8 OffsetTime (immutable) LocalTime time, ZoneOffset offset
Java 8 ZonedDateTime (immutable) LocalDateTime dateTime, ZoneOffset offset, ZoneId zone
Joda DateTime, LocalDateTime, LocalTime (all immutable) long iMillis, Chronology iChronology
Joda MutableDateTime long iMillis, Chronology iChronology, DateTimeField iRoundingField, int iRoundingMode
Joda LocalDate (immutable) long iMillis, Chronology iChronology, int iHash

Java 8 / Joda classes implementations details

Here are some of my implementation detail notes describing why some operations are running faster than others.

Java 8 LocalDate

Based on 3 separate fields – year(int), month, day (both short).

LocalDate.of(y,m,d) – checks validity of components and sets them. Very fast.

LocalDate.plusDays – converts to epoch date, adds the delta and converts from epoch date. There is a lot of space
left for optimization here.

Java 8 LocalTime

Based on 4 separate fields – hour, min, sec (all byte) and nanos (int).

LocalTime.now/LocalTime(millis) – calculates the actual time in the given timezone at the given moment.

LocalTime.of(h,m[,s[,n]]) – range checks all the components and sets them in the constructor. Very fast.

LocalTime.plusSeconds – adds the given amount to the number of seconds inside the given object (which is built of hours, minutes and seconds), wraps around midnight, so adding more than 24 * 60 * 60 seconds using this method does not make any sense. This method uses quite a lot of div and mod operations, so it is a little slow.

Java 8 LocalDateTime

Based on LocalDate and LocalTime fields. It means that operations affecting both date and time will be slower than corresponding LocalDate or LocalTime operations.

LocalDateTime.now – gets an Instant and uses its millis since epoch/nanos getters to obtain values for the LocalDateTime.ofEpochSecond (see below).

LocalDateTime.ofEpochSecond – this is one of the ways how to convert millis since epoch into LocalDateTime. Another way is to construct an Instant out of millis and call LocalDateTime.ofInstant, which in turn, will call this method. Calling this method on its own is not too convenient, because you have to figure out the ZoneOffset for your millis and timezone – LocalDateTime.ofInstant does it for you. This method splits number of seconds into day sine epoch (used for LocalDate construction) and remaining number of seconds in a day (used for LocalTime).

LocalDateTime.ofInstant(Instant, ZoneId) – the easiest way to convert millis since epoch and timezone into LocalDateTime (see description above).

LocalDateTime.of(y,m,d,h,m[,s,[,n]]) – constructs the internal objects based on the provided components. Very fast.

LocalDateTime.plusDate (plus method for date components) – uses LocalDate.plus*

LocalDateTime.plusTime (plus methods for time components) – convert existing time and provided adjustment to nanoseconds, add them roll over into date component if required. Reasonably fast purely arithmetical method without loops.

Java 8 OffsetDateTime

Combines LocalDateTime for storing date/time components with ZoneOffset for storing offset (note that the same offset may belong to the different timezones at the same time). Offset is used only for human-to-machine (millis since epoch) time conversion.

All constructions methods create an instance of LocalDateTime and combine it with ZoneOffset. Millis since epoch constructors are all ZoneId based – it allows to obtain the ZoneOffset for the given instant.

OffsetDateTime.plus* – delegates a call to the same method in LocalDateTime, combines result with the current offset.

Java 8 OffsetTime

Combines LocalTime for storing time components with ZoneOffset for storing offset. Offset is used only for human-to-machine (millis since epoch) time conversion. All implementation details are absolutely similar to OffsetDateTime.

Java 8 ZonedDateTime

Combines LocalDateTime with ZoneId (for timezone information) and ZoneOffset (for human-to-machine time transformations). The implementation is similar to OffsetDateTime, but in this case we have to calculate ZoneOffset in every constructor. In essence, it means that we have to implicitly convert the human time to machine time for every constructed object (there is no other way to calculate the offset).

Joda MutableDateTime

Based on millis since epoch long field.

MutableDateTime(y,m,d,h,m,s) – same fast code used for LocalDateTime(year, month, day, hour, min, sec) followed by adjustment from local to UTC time.

MutableDateTime.addDays – converts UTC to local, adds 86400 * 1000 * days, converts from local to UTC (including offset calculation)

MutableDateTime.addSeconds – converts UTC to local, adds 1000 * seconds, converts from local to UTC (excluding offset calculation)

Joda LocalDate

Based on millis since epoch long field. Truncated to 00:00:00 on every construction and at a few other points in code. It makes it generally slower than Joda LocalDateTime.

LocalDate(long) – truncates to 00:00:00, similar logic is used in LocalTime, where date is truncated instead of time.

LocalDate(year, month, day) – calculates year info once (cached), gets first millisecond from it, adds precalculated month first millisecond, adds days. A little faster than 6 component constructor of Joda LocalDateTime because less calculations have to be done here.

LocalDate.plusDays(int); now() – adds number of millis a day, because all objects are truncated to 00:00:00 on construction (UTC time, so division by 86400 * 1000 is safe), then roundFloor (mod) twice – it makes it slower than the same method in Joda LocalDateTime.

Joda LocalDateTime

Based on millis since epoch long field.

LocalDateTime(long) – cheapest possible constructor, just sets the timestamp.

LocalDateTime(year, month, day, hour, min, sec) – uses LocalDate constructor logic for calculating the date component, adds time components.

LocalDateTime.plus* – adds a number of millis in a given unit multiplied by count.

Joda LocalTime

Based on millis since epoch long field. All timestamps (millis) are truncated in the constructors by a number of millis a day, (requires div and mod operations), so these constructors are slower than Joda LocalDateTime constructors.

LocalTime(long) – date component is truncated.

LocalTime(hour, min, sec) – slower than other Joda Local* classes because for some reason constructor is written like it is trying to adjust the existing timestamp (0) with time units provided in the constructor. All back and forth conversion logic requires extra processing.

LocalTime.plusSeconds – cheaply adds seconds, but then calls LocalTime(long) constructor, which truncates date component (div and mod operations), which makes it slower than the same Joda LocalDateTime method.

Offset parsing performance issue in Java 8 ea b121

As you can see from the table, parsing speed is more or less consistent across all implementations. But there is a weird drop in performance (10-20 times slower) for three Java 8 classes: OffsetDateTime, OffsetTime and ZonedDateTime. I have used the default pattern for all of them (see parse method JavaDoc of those classes for examples). All these patterns share one common field – offset. As it turned out, it is not consumed by the new JDK date/time classes during parsing process – it will be actually calculated based on the datetime components and timezone.

As a result, you will end up in java.time.format.Parsed.crossCheck() method, which in turn calls crossCheck(TemporalAccessor target) method, which has the following code:

1
2
3
4
5
6
long val1;
try {
    val1 = target.getLong(field);
} catch (RuntimeException ex) {
    continue;
}
long val1;
try {
    val1 = target.getLong(field);
} catch (RuntimeException ex) {
    continue;
}

target.getLong is implemented by 3 underlying classes: LocalDate/LocalTime/LocalDateTime. If you have read the article, you will now realize that none of them implements “OffsetSeconds” field… So, as a result, you will have 3 exceptions thrown per parsing for OffsetDateTime/ZonedDateTime default patterns (one call per date/time/datetime) and 1 exception for OffsetTime.

As I have already written in Throwing an exception is very slow in Java article, you must not throw exceptions for likely to happen events – filling an exception stack trace takes a really long time. I was a little surprised to see it in the JDK code. Anyway, I hope it will be fixed before the final Java 8 release (I plan to review all Java 8 articles after that).

ZonedDateTime parsing – yet another exception…

After removing an offset from a ZonedDateTime parser I thought that its speed will be similar to the other parsers. Surprisingly, parsing got only 50% faster (and I expected ~10 times faster). As it turned out, there was another exception to be thrown! Here is ZonedDateTime.from(TemporalAccessor) method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static ZonedDateTime from(TemporalAccessor temporal) {
    if (temporal instanceof ZonedDateTime) {
        return (ZonedDateTime) temporal;
    }
    try {
        ZoneId zone = ZoneId.from(temporal);
        try {
            long epochSecond = temporal.getLong(INSTANT_SECONDS);
            int nanoOfSecond = temporal.get(NANO_OF_SECOND);
            return create(epochSecond, nanoOfSecond, zone);
 
        } catch (DateTimeException ex1) {
            LocalDateTime ldt = LocalDateTime.from(temporal);
            return of(ldt, zone);
        }
    } catch (DateTimeException ex) {
        throw new DateTimeException("Unable to obtain ZonedDateTime from TemporalAccessor: " +
                temporal + " of type " + temporal.getClass().getName(), ex);
    }
}
public static ZonedDateTime from(TemporalAccessor temporal) {
    if (temporal instanceof ZonedDateTime) {
        return (ZonedDateTime) temporal;
    }
    try {
        ZoneId zone = ZoneId.from(temporal);
        try {
            long epochSecond = temporal.getLong(INSTANT_SECONDS);
            int nanoOfSecond = temporal.get(NANO_OF_SECOND);
            return create(epochSecond, nanoOfSecond, zone);

        } catch (DateTimeException ex1) {
            LocalDateTime ldt = LocalDateTime.from(temporal);
            return of(ldt, zone);
        }
    } catch (DateTimeException ex) {
        throw new DateTimeException("Unable to obtain ZonedDateTime from TemporalAccessor: " +
                temporal + " of type " + temporal.getClass().getName(), ex);
    }
}

As you can see, it tries to access INSTANT_SECONDS and NANO_OF_SECOND parsed fields. Of course we don’t have them! As a result, we will populate a LocalDateTime from parsed fields instead and combine it with a zone. Unfortunately, it is not possible for the client code to avoid an exception in this method during datetime parsing using Java 8ea b 121. Again, this should be hopefully fixed before the final Java 8 release.

By the way, the similar exception-based branching is also used in OffsetDateTime.from(TemporalAccessor) method.

Summary

  • Java 8 date/time classes are built on top of human time – year/month/day/hour/minute/second/nanos. It makes them fast for human datetime arithmetics/conversion. Nevertheless, if you are processing computer time (a.k.a. millis since epoch), especially computer time in a short date range (a few days), a manual implementation based on int/long values would be much faster.
  • Date/time component getters like getDayOfMonth have O(1) complexity in Java 8 implementation. Joda getters require the computer-to-human time calcualtion on every getter call, which makes Joda a bottleneck in such scenarios.
  • Parsing of OffsetDateTime/OffsetTime/ZonedDateTime is very slow in Java 8 ea b121 due to exceptions thrown and caught internally in the JDK.

Test source code

Java 8 date/time test source code


One thought on “JSR 310 – Java 8 Date/Time library performance (as well as Joda Time 2.3 and j.u.Calendar)

  1. Pingback: Joda Time library performance - Java Performance Tuning Guide

Comments are closed.