Java string masking performance — Why you should stop worrying and rewrite everything in C

Chintana Wilamuna
5 min readApr 24, 2019

--

This is a story about 3 things,

  1. Why we should pay close attention to how we implement something (duh!)
  2. Why we should immediately switch to JDK 9 (for bad code to run faster) and…
  3. Why we should rewrite everything in C. (Sorry I had to throw in a C example for good measure)

I’m kidding on #2 and #3.

TL;DR — Look at table below.

Summary of the test

Problem

We’re getting a string, looks a lot like a UUID. We need to be able to see first 4 chars and mask the rest. Easy.

Solution

There are several ways of writing this. Let’s see 3 approaches.

Method 1

1st approach

Create a new string with StringBuilder, copy over first 4 characters. Then loop until length of the string and mask them with a * character. Not terribly exciting. A reviewer has commented on this saying if this piece of code is executing frequently, looping might adversely impact performance.

OK. Which brings us to the question of how do we refactor this. When you’re doing this inside a large code base, chances are there are existing libraries in place that allows you do so something similar. As it turns out, there’s exactly that with Apache Commons Lang. That’s going to be our 2nd approach.

Method 2

2nd approach

Let’s forget about magic numbers for the moment. Assuming a string length is bad to begin with.

At first glance it seems better. And it is. However, if you look at StringUtils that’s coming from Apache Commons Lang, the code is again doing a loop. Copied method below from here (commons lang source from github).

It’s worth noting that 3rd party libraries provide a great way to do things quickly. We have to be careful on how it’s going to affect performance as well as time added at compilation. If you’ve worked with a moderately large maven project, you know how couple of dependencies can download quarter of the internet as it tries to resolve transitive dependencies.

Can we do better? That brings us to the 3rd approach.

Method 3

3rd approach

No 3rd party dependencies here. We convert string to an char array and the use Arrays.fill to replace the chars.

Measuring performance

In order to understand how these 3 behave and why one is better than the other, we need to look at how these behave under repeated calls. We can do that by repeatedly calling these within a loop.

Integer.MAX_VALUE is 2147483647. Sufficiently large loop count.

With JDK 1.8

$ java -version
java version "1.8.0_181"
Java(TM) SE Runtime Environment (build 1.8.0_181-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.181-b13, mixed mode)

Method 1 — takes about 10 minutes

$ java Test1
java Test1 585.90s user 5.60s system 101% cpu 9:45.56 total

Method 2 — takes about 2 minutes

$ time java -cp commons-lang3-3.9.jar:. Test2
java -cp commons-lang3-3.9.jar:. Test2 145.74s user 1.71s system 101% cpu 2:25.72 total

Method 3 — takes about 50 seconds

$ time java Test3
java Test3 51.43s user 1.01s system 100% cpu 52.031 total

Massive performance differences with 3 different approaches. Now next test surprised me. Switched to JDK 9 and here are the results.

With JDK 9

$ java -version
java version "9.0.4"
Java(TM) SE Runtime Environment (build 9.0.4+11)
Java HotSpot(TM) 64-Bit Server VM (build 9.0.4+11, mixed mode)

Method 1 — takes about 4 minutes

$ time java Test1
java Test1 270.58s user 1.60s system 101% cpu 4:29.45 total

Method 2 — takes about 2 minutes

$ time java -cp commons-lang3-3.9.jar:. Test2
java -cp commons-lang3-3.9.jar:. Test2 139.44s user 1.55s system 101% cpu 2:18.31 total

Method 3 — takes about 50 seconds

$ time java Test3
java Test3 50.01s user 0.62s system 102% cpu 49.191 total

Observations

Method 3 performance is unchanged across JDK versions. JDK 9 is doing some massive optimizations on our 1st approach. Essentially gives more than a 50% bump in performance.

However we should pay close attention to what implications our code might have on frequently executed code blocks on possibly high request processing use cases.

Now on to C

As I mentioned this is just for good measure.

C example

C strings are terminated with the null character. So we’re checking for null to make sure we’re at end of the string. Resist the urge to use strlen here as it will slow down your programs massively.

Let’s see what that looks like,

$ cc Test4.c
$ time ./a.out
./a.out 185.07s user 0.37s system 99% cpu 3:05.75 total

3 minutes!? That’s because we have to let the compiler do its thang.

$ cc -O3 Test4.c
$ time ./a.out
./a.out 33.52s user 0.03s system 99% cpu 33.577 total

… and we’re down to 30 seconds!

May be there are faster ways of doing things. Hit me up on comments! Full source here on github.

--

--