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 version 2.1.

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 contain a timezone.
  • LocalTime – immutable version of DateTime containing only time fields. This class does not contain a timezone.
  • LocalDateTime – immutable version of DateTime not containing 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. Compare this to 24 bytes occupied by java.util.Date and 448 bytes occupied by java.util.Calendar.

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. On the opposite, it is expensive to create them using year/month/day/hour/minute/second/millis constructor.

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
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;
    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( 2013, 1, 1, 11, 0, 0 );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    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( 2013, 1, 1, 11, 0, 0 );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    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;
    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( 2013, 1, 1, 11, 0, 0 );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    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( 2013, 1, 1, 11, 0, 0 );
        sum += ( res != null ? 1 : 0 );
    }
    final long time = System.currentTimeMillis() - start;
    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):

Joda DateTime (millis) Joda DateTime (components) JDK GregorianCalendar
0.131 sec 14.699 sec 4.469 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. On the other hand, it is still better to stick to long timestamps if you can. Note, that it is 3 times more expensive to create a Joda DateTime out of date/time components rather than to create a JDK GregorianCalendar. This logic is definitely worth optimizing in Joda Time.

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, time addition/subtraction works much faster than date (days/months/years) operations. The following method and its minor modifications were used for testing. Again, 10M datetime operations were made.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private static void testJodaDateUpdatesImmutable( final int cnt )
{
    final int outerIters = cnt / 200;
    DateTime date = new DateTime( 2013, 3, 1, 10, 0, 0 );
    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;
    System.out.println( "Time for immutable DateTime date arithmetic = " + time / 1000.0 + " sec");
    System.out.println( date );
}
private static void testJodaDateUpdatesImmutable( final int cnt )
{
    final int outerIters = cnt / 200;
    DateTime date = new DateTime( 2013, 3, 1, 10, 0, 0 );
    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;
    System.out.println( "Time for immutable DateTime date arithmetic = " + time / 1000.0 + " sec");
    System.out.println( date );
}
Joda DateTime (days) Joda DateTime (hours) Joda MutableDateTime (days) JDK GregorianCalendar (days) JDK GregorianCalendar (hours)
12.265 sec 1.033 sec 11.89 sec 3.525 sec 3.191 sec

As you can see, general case date arithmetic operations are 3.5 times faster in the JDK Calendar class rather than in Joda DateTime class. Though, if you need only time manipulations, Joda Time works faster (and still correctly handles daytime savings rollover in both directions).

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 works approximately at the same speed as JDK SimpleDateFormat parsing. Though, Joda Time parsing has an important advantage over JDK parsing: creating 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, “2012.01.01 13:34:47″ was parsed 10M times using Joda and JDK parsers. 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 created).

Joda DateTimeFormatter (cached) Joda DateTimeFormatter (10M parsers) JDK SimpleDateFormat (cached) JDK SimpleDateFormat (10M parsers)
48.775 sec 48.897 sec 49.859 sec 98.971 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
private final static String FORMAT = "yyyy'.'MM'.'dd' 'HH':'mm':'ss";
 
private static DateTimeFormatter getJodaFormat()
{
    return DateTimeFormat.forPattern( FORMAT );
}
 
private static void testSimpleJodaParsing( final int cnt ) throws ParseException, IllegalAccessException {
    final String date = "2012.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;
    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 = "2012.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;
    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( TimeZone.getTimeZone("UTC") ); //UTC is rather commonly used
    sdf.setLenient( false ); //strict parsing
    return sdf;
}
 
private static void testSimpleJdkParsing( final int cnt ) throws ParseException {
    final String date = "2012.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;
    System.out.println( "Time for caching 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 = "2012.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;
    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 );
}

private static void testSimpleJodaParsing( final int cnt ) throws ParseException, IllegalAccessException {
    final String date = "2012.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;
    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 = "2012.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;
    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( TimeZone.getTimeZone("UTC") ); //UTC is rather commonly used
    sdf.setLenient( false ); //strict parsing
    return sdf;
}

private static void testSimpleJdkParsing( final int cnt ) throws ParseException {
    final String date = "2012.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;
    System.out.println( "Time for caching 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 = "2012.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;
    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 still too generic and rather slow. 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 see a faster date parser.

Summary

  • All Joda Time date/time objects are built on top of a long timestamp, so it is cheap to create those objects from a long.
  • In Joda Time ver 2.1 creating a date/time object from date/time components (year, month, day, hour, min, sec) is ~3.5 time slower than the same operation for JDK GregorianCalendar.
  • Date components addition/subtraction is 3.5 times slower in Joda Time rather than in GregorianCalendar. On the contrary, time components operations are about the same 3.5 times faster than GregorianCalendar implementation.
  • Date/time parsing is working at about the same speed as in JDK SimpleDateFormat. The advantage of Joda parsing is that creating a parser – DateTimeFormatter object is extremely cheap, unlike an expensive SimpleDateFormat, so you don’t have to cache parsers anymore.