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
The post JSR 310 – Java 8 Date/Time library performance (as well as Joda Time 2.3 and j.u.Calendar) appeared first on Java Performance Tuning Guide.