Joda Time library performance

by Mikhail Vorontsov

Joda Time library is an attempt to create a cleaner and more developer-friendly date/time library than an existing JDK Date/Calendar/DateFormat ecosystem. This article provides a top level overview of Joda Time library from performance point of view. It may be also useful as a short introduction to Joda Time. I recommend reading java.util.Date, java.util.Calendar and java.text.SimpleDateFormat article before reading this one in order to better understand the performance of built-in JDK classes.

This article discusses Joda Time library versions 2.1 – 2.3.


26 Jan 2014: This is a major article rewrite – a major performance issue was found in Joda Time implementation.

Date/time storage classes

There are five general purpose date/time classes in this library:

  • DateTime – full replacement of java.util.Calendar, supporting time zones and date/time arithmetic. This class is immutable.
  • MutableDateTime – mutable version of DateTime.
  • LocalDate – immutable version of DateTime containing only date fields. This class does not use a timezone after construction.
  • LocalTime – immutable version of DateTime containing only time fields. This class does not use a timezone after construction.
  • LocalDateTime – immutable version of DateTime not using a timezone.

All these classes contain 2 mandatory fields – long with millis since epoch and a Chronology object, containing all timezone and calendar system (Gregorian, Buddhist, etc.) related logic. Some of these classes contain 1-2 more fields. An instance of these classes occupy from 24 to 40 bytes.

Since these classes are based on the “machine” time – millis since epoch, we may expect the better performance on from/to long conversions and worse performance on to/from date components (human time) conversions. In reality, Joda developers have made a series of clever optimizations which make it really fast even on the human time calculations.

Joda Time timezone offset calculation performance bug (ver 2.3)

Let’s start an updated version of this article from this point :) When I wrote an original version of this article in late 2012, I noticed the poor performance of timezone based logic in Joda Time, but I thought that it was the common property of that library, so I wrote that “this logic is definitely worth optimizing in Joda Time”.

One year later I have written a more comprehensive set of tests which includes the new Java 8 date/time implementation (JSR-310). This test set has highlighted some weird inconsistencies between various date/time implementations. In particular I have noticed that creating a Joda MutableDateTime from date components was 3-4 times faster than the same operation on date+time components. Nevertheless both were using the identical client logic. But there was a difference – date tests were using years from 1981 to 2000 (in a loop). DateTime tests were using 2013. This turned out to be a key to the problem.

MutableDateTime, as well as some other Joda Time classes are calculating timezone offset for the given “millis since epoch” values from time to time. It may be calculated more than once per client API call. Deep under the hood there is a org.joda.time.tz.DateTimeZoneBuilder$PrecalculatedZone class with getOffset method. This method looks up the transition table for the given timezone using binary search. If your timestamp is less or equal to the biggest timestamp in the table – you get it. Otherwise org.joda.time.tz.DateTimeZoneBuilder$DSTZone.getOffset method is called for every offset you calculate. It uses daylight savings transition rules to calculate the latest transition and use it for offset calculation. Calculated values are not cached on this branch.

I have noticed this difference between years 2008 and 2009 in “Australia/Sydney” timezone. After that I ran the same test on all available timezones and found a list of zones in/around Australia and New Zealand with the same performance issue – offsets in 2009 were calculated much slower than in 2008. At the same time I have noticed that European timezones were slow in both 2008 and 2009. This led me to the conclusion.

Joda time ships with timezone rule source files in the src/main/java/org/joda/time/tz/src directory. If you’ll take a look at “australasia” file and look for the “New South Wales” rule, you will see that 2 its last lines are:

Rule	AN	2008	max	-	Apr	Sun>=1	2:00s	0	-
Rule	AN	2008	max	-	Oct	Sun>=1	2:00s	1:00	-

This is the last fast year – 2008 (and I used 1st January for testing). After that I got more suspicious and opened “europe” file and got really worried. For example, Netherlands (Europe/Amsterdam) last rule belongs back to 1945:

Rule	Neth	1945	only	-	Apr	 2	2:00s	1:00	S
Rule	Neth	1945	only	-	Sep	16	2:00s	0	-
    

The longer a country lives on a stable daylight saving rule, the more it gets penalized by Joda Time: it takes ~3.6 seconds to create 10M MutableDateTime objects for year 2008 in “Europe/Amsterdam” timezone, ~3 seconds to create 10M MutableDateTime objects for year 2010 in “Australia/Sydney” timezone (which also has to calculate its daylight savings transitions), but only ~1.2 seconds for year 2008 in Sydney (precalculated table).


So, I would like to ask Joda Time maintainers to consider prebuilding such transition tables during the timezone construction (at runtime) up to at least the current year plus a few years more.


Updated tests

The remaining part of this article will keep the original structure, but the contents were changed. By default I will use year = 2000 and timezone = Australia/Sydney for my tests. Both of them are defined as top level constants in the article test source code, so you can play with them.

Creating a date/time object

Being long-based classes, all these date/time classes are cheap to create from a long timestamp – millis since epoch. They simply set a class field and then adjust the timestamp by the timezone (if necessary).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private static void testDateMillisCreate(final int cnt)
{
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTime res = new DateTime(  155 );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for new DateTime(long) = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
 
private static void testDateComponentCreate(final int cnt)
{
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTime res = new DateTime( year, 1, 1, 11, 0, 0, JODA_TIMEZONE );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for new DateTime(y,d,m,h,m,s) = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
 
private static void testJdkDateComponentCreate(final int cnt)
{
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final Calendar res = new GregorianCalendar( JDK_TIMEZONE );
        res.set(year, 1, 1, 11, 0, 0);
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for new GregorianCalendar(y,d,m,h,m,s) = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
private static void testDateMillisCreate(final int cnt)
{
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTime res = new DateTime(  155 );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for new DateTime(long) = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

private static void testDateComponentCreate(final int cnt)
{
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTime res = new DateTime( year, 1, 1, 11, 0, 0, JODA_TIMEZONE );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for new DateTime(y,d,m,h,m,s) = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

private static void testJdkDateComponentCreate(final int cnt)
{
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final Calendar res = new GregorianCalendar( JDK_TIMEZONE );
        res.set(year, 1, 1, 11, 0, 0);
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for new GregorianCalendar(y,d,m,h,m,s) = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

Each of these methods was called with cnt = 10_000_000 (and with previous warm-up cnt = 20_000, of course). Unfortunately, GregorianCalendar test iteration include initialization of 2 instants – current one and the given one. But the point of the test is to check the performance of the same task – create an object in the given timezone with the given time.

Year Joda DateTime (millis) Joda DateTime (components) JDK GregorianCalendar
2000 0.214 sec 0.838 sec 2.52 sec
2014 0.214 sec 4.634 sec 2.52 sec

As you can see, if you already have a long timestamp, Joda classes could be good wrappers over long timestamp, providing on-demand rich conversion method set. Unfortunately, Joda timezone issue makes it slow on the most needed timestamps – current year+.

Date/time addition/subtraction

The second obvious consequence from storing a timestamp internally as long is that date/time arithmetic should take longer than in case of storing each date/time component separately. Surprisingly, it works faster than the GregorianCalendar implementation (for the “right” year, of course). Again, 10M datetime operations were made.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
private static void testJodaDateUpdatesImmutable( final int cnt )
{
    final int outerIters = cnt / 200;
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date = date.plusDays(1);
        for ( int j = 0; j < 100; ++j )
            date = date.minusDays(1);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for immutable DateTime date arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() );
}
 
private static void testJodaTimeUpdatesImmutable( final int cnt )
{
    final int outerIters = cnt / 200;
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date = date.plusHours(1);
        for ( int j = 0; j < 100; ++j )
            date = date.minusHours(1);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for immutable DateTime time arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() );
}
 
private static void testJodaDateUpdatesMutable(final int cnt)
{
    final int outerIters = cnt / 200;
    MutableDateTime date = new MutableDateTime( year, 3, 1, 10, 0, 0, 0, JODA_TIMEZONE );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date.addDays( 1 );
        for ( int j = 0; j < 100; ++j )
            date.addDays( -1 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for MutableDateTime date arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() );
}
 
private static void testJdkDateUpdates( final int cnt )
{
    final int outerIters = cnt / 200;
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.DAY_OF_MONTH, 1 );
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.DAY_OF_MONTH, -1 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar date arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() );
}
 
private static void testJdkTimeUpdates( final int cnt )
{
    final int outerIters = cnt / 200;
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.HOUR_OF_DAY, 1 );
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.HOUR_OF_DAY, -1 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar time arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() );
}
private static void testJodaDateUpdatesImmutable( final int cnt )
{
    final int outerIters = cnt / 200;
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date = date.plusDays(1);
        for ( int j = 0; j < 100; ++j )
            date = date.minusDays(1);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for immutable DateTime date arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() );
}

private static void testJodaTimeUpdatesImmutable( final int cnt )
{
    final int outerIters = cnt / 200;
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date = date.plusHours(1);
        for ( int j = 0; j < 100; ++j )
            date = date.minusHours(1);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for immutable DateTime time arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() );
}

private static void testJodaDateUpdatesMutable(final int cnt)
{
    final int outerIters = cnt / 200;
    MutableDateTime date = new MutableDateTime( year, 3, 1, 10, 0, 0, 0, JODA_TIMEZONE );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date.addDays( 1 );
        for ( int j = 0; j < 100; ++j )
            date.addDays( -1 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for MutableDateTime date arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() );
}

private static void testJdkDateUpdates( final int cnt )
{
    final int outerIters = cnt / 200;
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.DAY_OF_MONTH, 1 );
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.DAY_OF_MONTH, -1 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar date arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() );
}

private static void testJdkTimeUpdates( final int cnt )
{
    final int outerIters = cnt / 200;
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < outerIters; ++i )
    {
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.HOUR_OF_DAY, 1 );
        for ( int j = 0; j < 100; ++j )
            date.add( Calendar.HOUR_OF_DAY, -1 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar time arithmetic = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() );
}
Year Joda DateTime (days) Joda DateTime (hours) Joda MutableDateTime (days) JDK GregorianCalendar (days) JDK GregorianCalendar (hours)
2000 0.622 sec 0.314 sec 0.551 sec 1.231 sec 0.967 sec
2014 3.867 sec 0.311 sec 3.882 sec 1.213 sec 0.983 sec

As you can see, general case date/time arithmetic operations are 2 times faster in Joda Time rather than in JDK GregorianCalendar if you are using a precomputed year in Joda. Unfortunately, date operations slow down considerably in case of current year (and all other non-precomputed years) – this time GregorianCalendar is performing 3 times faster than Joda.

Date/time field getters

Unfortunately, the decision to use computer time (millis since epoch) behind Joda classes has its disadvantage – you have to perform more or less expensive calculations every time when you need to obtain the human time components – year, month, day, hour, minute, second. It may consume all the advantage of faster Joda Time objects construction.

The following test creates a Joda/GregorianCalendar object (once) and then calls year/month/day getters for date tests and hour/min/sec getters for time tests 10M times (30M calls in total). GregorianCalendar has a huge advantage in this tests – it will compute/normalize all these fields once and then just return them ( O(1) amortized complexity ). Joda has to calculate them on every call, because Joda objects have no dedicated field storage.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private static void testJodaDateGetters( final int cnt )
{
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.getYear() + date.getMonthOfYear() + date.getDayOfMonth();
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for DateTime date getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() + sum );
}
 
private static void testJodaTimeGetters( final int cnt )
{
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.getHourOfDay() + date.getMinuteOfHour() + date.getSecondOfMinute();
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for DateTime time getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() + sum );
}
 
private static void testJdkDateGetters( final int cnt )
{
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.get(Calendar.YEAR) + date.get(Calendar.MONTH) + date.get(Calendar.DAY_OF_MONTH);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar date getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() + sum );
}
 
private static void testJdkTimeGetters( final int cnt )
{
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.get(Calendar.HOUR_OF_DAY) + date.get(Calendar.MINUTE) + date.get(Calendar.SECOND);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar time getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() + sum );
}
private static void testJodaDateGetters( final int cnt )
{
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.getYear() + date.getMonthOfYear() + date.getDayOfMonth();
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for DateTime date getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() + sum );
}

private static void testJodaTimeGetters( final int cnt )
{
    DateTime date = new DateTime( year, 3, 1, 10, 0, 0, JODA_TIMEZONE );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.getHourOfDay() + date.getMinuteOfHour() + date.getSecondOfMinute();
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for DateTime time getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333) System.out.println( date.toString() + sum );
}

private static void testJdkDateGetters( final int cnt )
{
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.get(Calendar.YEAR) + date.get(Calendar.MONTH) + date.get(Calendar.DAY_OF_MONTH);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar date getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() + sum );
}

private static void testJdkTimeGetters( final int cnt )
{
    Calendar date = new GregorianCalendar( JDK_TIMEZONE );
    date.set( year, Calendar.MARCH, 1, 10, 0, 0 );
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        sum += date.get(Calendar.HOUR_OF_DAY) + date.get(Calendar.MINUTE) + date.get(Calendar.SECOND);
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for GregorianCalendar time getters = " + time / 1000.0 + " sec");
    if ( cnt == 13333 ) System.out.println( date.toString() + sum );
}
Year Joda DateTime (date) Joda DateTime (time) JDK GregorianCalendar (date) JDK GregorianCalendar (time)
2000 0.772 sec 1.266 sec 0.063 sec 0.063 sec
2014 0.706 sec 1.274 sec 0.063 sec 0.063 sec

Date/time parsing

Datetime parsing looks slightly different from JDK in Joda Time. First, you obtain a DateTimeFormatter object using static DateTimeFormat.forPattern static factory or one of predefined DateTimeFormat static factories like fullDate, longTime, etc. After that you call DateTimeFormatter.parseDateTime method to obtain a DateTime object or one of other parse* methods in order to obtain other Joda Time objects. All standard DateTimeFormatter objects are immutable (which means they are thread-safe as well). You can obtain a new formatter with a modified property using one of with* methods (usually you will need to call withLocale for date->string conversion i18n and withZone in order to parse a date in the required timezone).

Parsing a date/time in Joda Time is a little faster than in JDK SimpleDateFormat (though, the difference is getting smaller for non-precomputed years). Besides that, Joda Time parsing has an important advantage over JDK parsing: constructing a DateTimeFormatter is extremely cheap in Joda Time (it is as expensive as parsing itself in case of JDK SimpleDateFormat). You don’t have to cache a parser object anymore unlike JDK common practice of storing DateFormat instances in a ThreadLocal.

In order to test parsing speed, “YEAR.01.01 13:34:47″ was parsed 10M times using Joda and JDK parsers (where YEAR is a year used by all tests). For both parsers 2 separate tests were used: first one which creates a single instance of a parser and reuses it for all parsed strings and a second one which creates a parser for every string (10M parser objects were constructed).

Year Joda DateTimeFormatter (cached) Joda DateTimeFormatter (10M parsers) JDK SimpleDateFormat (cached) JDK SimpleDateFormat (10M parsers)
2000 11.802 sec 11.96 sec 18.68 sec 36.146 sec
2014 15.87 sec 15.958 sec 18.458 sec 35.683 sec
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
private final static String FORMAT = "yyyy'.'MM'.'dd' 'HH':'mm':'ss";
 
private static DateTimeFormatter getJodaFormat()
{
    return DateTimeFormat.forPattern( FORMAT ).withZone(JODA_TIMEZONE);
}
 
private static void testSimpleJodaParsing( final int cnt ) throws ParseException, IllegalAccessException {
    final String date = year + ".01.01 13:34:47";
    final DateTimeFormatter df = getJodaFormat();
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTime res = df.parseDateTime(date);
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for DateTimeFormatter.parseDateTime = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
 
private static void testSimpleJodaParsingNoFormatCaching( final int cnt ) throws ParseException {
    final String date = year + ".01.01 13:34:47";
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTimeFormatter df = getJodaFormat();
        final DateTime res = df.parseDateTime(date);
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "No cache: Time for DateTimeFormatter.parseDateTime = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
 
private static DateFormat getJdkFormat()
{
    final SimpleDateFormat sdf = new SimpleDateFormat( FORMAT );
    sdf.setTimeZone( JDK_TIMEZONE ); //UTC is rather commonly used
    sdf.setLenient(false); //strict parsing
    return sdf;
}
 
private static void testSimpleJdkParsing( final int cnt ) throws ParseException {
    final String date = year + ".01.01 13:34:47";
    final DateFormat df = getJdkFormat();
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final Date res = df.parse( date );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for cached DateFormat.parse = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
 
private static void testNoCacheJdkParsing( final int cnt ) throws ParseException {
    final String date = year + ".01.01 13:34:47";
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateFormat df = getJdkFormat();
        final Date res = df.parse( date );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for not caching DateFormat.parse = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}
private final static String FORMAT = "yyyy'.'MM'.'dd' 'HH':'mm':'ss";

private static DateTimeFormatter getJodaFormat()
{
    return DateTimeFormat.forPattern( FORMAT ).withZone(JODA_TIMEZONE);
}

private static void testSimpleJodaParsing( final int cnt ) throws ParseException, IllegalAccessException {
    final String date = year + ".01.01 13:34:47";
    final DateTimeFormatter df = getJodaFormat();
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTime res = df.parseDateTime(date);
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for DateTimeFormatter.parseDateTime = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

private static void testSimpleJodaParsingNoFormatCaching( final int cnt ) throws ParseException {
    final String date = year + ".01.01 13:34:47";
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateTimeFormatter df = getJodaFormat();
        final DateTime res = df.parseDateTime(date);
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "No cache: Time for DateTimeFormatter.parseDateTime = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

private static DateFormat getJdkFormat()
{
    final SimpleDateFormat sdf = new SimpleDateFormat( FORMAT );
    sdf.setTimeZone( JDK_TIMEZONE ); //UTC is rather commonly used
    sdf.setLenient(false); //strict parsing
    return sdf;
}

private static void testSimpleJdkParsing( final int cnt ) throws ParseException {
    final String date = year + ".01.01 13:34:47";
    final DateFormat df = getJdkFormat();
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final Date res = df.parse( date );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for cached DateFormat.parse = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

private static void testNoCacheJdkParsing( final int cnt ) throws ParseException {
    final String date = year + ".01.01 13:34:47";
    int sum = 0;
    final long start = System.currentTimeMillis();
    for ( int i = 0; i < cnt; ++i )
    {
        final DateFormat df = getJdkFormat();
        final Date res = df.parse( date );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    if ( cnt > 20000 )
        System.out.println( "Time for not caching DateFormat.parse = " + time / 1000.0 + " sec");
    if ( sum != cnt ) System.out.println( "Not null count = " + sum );
}

As you can see, Joda Time parsing logic is faster then the JDK SimpleDateFormatter, but it is still not sufficiently fast :) If you need to process a lot of (millions) of timestamps, it worth reading java.util.Date, java.util.Calendar and java.text.SimpleDateFormat article in order to get the ideas for a faster date parser.

Summary

  • All Joda Time date/time objects are built on top of a long timestamp, so it is cheap to construct those objects from a long.
  • Joda Time ver 2.1-2.3 is affected by a performance issue in a timezone offset calculation logic – all years after the last daylight savings rule change in the given timezone use a slow calculation path (European timezones are affected particularly badly). In essence it means that all zones will perform badly in all years after Joda Time release you are using.
  • Date/time objects construction and date/time arithmetics in Joda work 1.5-3 times faster than GregorianCalendar for the years not affected by an above mentioned performance issue. For affected years date operations performance in Joda plummets and could be 4 times slower than in GregorianCalendar.
  • Joda does not keep the human time – year/month/day/hour/min/second inside its objects (unlike GregorianCalendar). It means that accessing human time on Joda objects is more expensive if you need to get more than one field.
  • Date/time parsing in Joda is working a little faster than in JDK SimpleDateFormat. The advantage of Joda parsing is that constructing a parser – DateTimeFormatter object is extremely cheap, unlike an expensive SimpleDateFormat, so you don’t have to cache parsers anymore.

Tests source code

JodaTimeTests source code


6 thoughts on “Joda Time library performance

  1. Alexei Shpikat

    Hi Mikhail,
    There is no code for Date/time addition/subtraction for JDK Calendar, could you attach it, please? I’d like to run more tests in order to find out the roots of this kind of difference between DAYS and HOURS calculations.
    Thank you!

    Reply
    1. admin Post author

      Hi Alexey!

      I am finishing with the article comparing Java 8 date/times, j.u.Calendar and Joda 2.3 performance. I hope there will be all code you need in that article. This article (Joda) is supposed to be an introduction to Joda library only.

      Regards,
      Mikhail

      Reply
      1. Alexei

        What I wanted to see mainly, was whether you call the get() method after the addition/subtraction, as some calculations are postponed for JDK Calendar.

        Reply
        1. admin Post author

          Hi Alexei,

          I’ll attach the full source code to the article in a few minutes. I have added explicit ‘get’ calls after every date/time update in all tests, but GregorianCalendar is still faster than Joda in date operations and slower in time operations.

          The full performance table I am preparing for the next article is more interesting, but I have some unexpectedly slow results, so I will have to investigate a little longer.

          Reply
  2. Yeroc

    It would be really interesting to see how the new JSR 310 implementation (inspired by the JodaTime API) that’s coming in Java 8 compares. Have you considered running a test against the early access JDK8 builds?

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code lang=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre lang="" extra="">