venable/software

Java Instance Performance

I recently wrapped up a project to enrich data with geographic locations in Data Prepper. We added a geoip processor which locates the geographic location of various events using an IP address from those events. The popular and free MaxMind GeoLite2 database provides this capability and we used it.

One of the requirements of using this particular database is that we cease using the data and destroy the old database as required by the EULA. To keep in compliance, we added a check in Data Prepper on the age of the database. If it is older than 30 days old, it is invalid, and we will not use it.

During performance benchmarking of this feature I noticed a lot of CPU cycles spent on this database check. And most of that was on getting the current time.

I was surprised to see one of the major components of my flame graph was in Java’s VM.getNanoTimeAdjustment(localOffset). The Instant.now() method was calling this, and it took quite a bit of time.

The solution to this particular problem turned out to be easy. The code was checking the time for every record in a batch of records. So all I had to do to solve it was cache the results as I did in this PR.

But, I was curious to understand this a little better. Since Instant.now() was trying to get nanosecond resolution, and this was taking a long time, I wanted to find out how this compares to just getting millisecond resolution.

Benchmarking with JMH

To find how Instant.now() performs, I used the powerful Java Microbenchmark Harness (JMH) tool.

I wanted to get benchmarks against three options:

  1. Using the Instant.now() method. This returns the current time as a Java Instant. It is easy to write and commonly used.
  2. Getting an Instant object from the current system time in milliseconds. The Instant.ofEpochMilli() method provides this.
  3. As a baseline, just get the currentTimeMillis as a long.

The code for these benchmarks is quite straightforward.

@Benchmark
public Temporal measure_Instant_now() {
   return Instant.now();
}

@Benchmark
public Temporal measure_Instant_ofEpochMillis() {
   return Instant.ofEpochMilli(System.currentTimeMillis());
}

@Benchmark
public long measure_currentTimeMillis() {
   return System.currentTimeMillis();
}

I configured JMH to run for 2 iterations, each running for one second. Here are the raw results which JMH output.

i.v.e.time.JavaTimeNow.measure_Instant_now                    thrpt    6  24433505.138 ± 2956748.268  ops/s
i.v.e.time.JavaTimeNow.measure_Instant_ofEpochMillis          thrpt    6  28888338.858 ± 7473531.463  ops/s
i.v.e.time.JavaTimeNow.measure_currentTimeMillis              thrpt    6  34590964.288 ± 3281426.719  ops/s

Each of these is in operations per second. That is, how many times the benchmark method was called in the second. So a higher number is better for performance.

As expected, Instant.now() was able to run the fewest operations. Using ofEpochMillis() allowed for 18% more throughput. That is certainly significant. But, it wasn’t quite as much as I expected since my flame graphs had showed so much time in VM.getNanoTimeAdjustment(localOffset).

Using an Instant at all has overhead. This is not surprising at all since it must allocate a new object. The throughput of using no Instant was 20% higher than using an Instant with the current time in milliseconds.

Conclusion

There are some good applications here. First, if you know you don’t need nanosecond resolution, then use Instant.ofEpochMillis(). This is a rather easy decision to make because you get a good performance benefit with no real loss. You still have all the features of Instant.

Another application does require some consideration. If you don’t need an Instant you can use System.currentTimeMillis(). But, I say this with some hesitation because using an Instant provides other benefits. You can write cleaner code with it. Other developers (and your future self) know what they are getting from an Instant. You also don’t know how the use of public methods can expand. So providing long may have some impact on your code that you don’t want. But, if you are doing this all in a local method, you are probably fine avoiding the use of Instant.

You will have the balance between writing clean code and performance when making this particular decision.

At the end of the day, this isn’t going to make a massive difference unless you are calling it regularly. So consider the frequency of your usage as well.

Now you have a little more data at your hand to help you when dealing with times in Java.


Share