4

I stumbled upon a bizarre performance issue related to the StringBuilder's append method. I noticed what appeared to be a foolish mistake - string concatenation during the use of the StringBuilder constructor (this even shows up as a warning in the NetBeans IDE).

Version 1

int hash = -1; //lazily computed, also present in Version 2 int blockID = ... //0 to 1000, also present in Version 2 int threadID = ... //0 to 1000, also present in Version 2 boolean hashed = false; //also present in Version 2 @Override public int hashCode(){ if(!hashed){ StringBuilder s = new StringBuilder(blockID+":"+threadID); hash = s.toString().hashCode(); hashed= true; } return hash; } 

Millions of these objects are created during runtime, so I thought by making the following change, it would provide some speedup:

Version 2

@Override public int hashCode(){ if(!hashed){ StringBuilder s = new StringBuilder(blockID); s.append(":"); s.append(threadID); hash = s.toString().hashCode(); hashed = true; } return hash; } 

WRONG! Turns out, Version 2 is literally 100X slower than Version 1. Why???

Additional Info

I am compiling against Java 6 (customer requirement), and I am using Oracle's JVM.

My performance test involves creating a million of these objects and throwing them into a HashMap. It takes half a second to do it using Version 1, but almost 50 seconds to do it using Version 2.

6
  • By creating a million of these objects and throwing them into a HashSet and timing it. It takes half a second to do in Version 1, but almost 50 seconds to do it in Version 2. Commented Jan 14, 2015 at 22:40
  • 3
    would be interesting to see the generated bytecode difference for those. if the perf thing were true (dubious until I see how you profiled this), it would imply this entire block would make more sense as just hash = (blockID+":"+threadID).hashCode(); Commented Jan 14, 2015 at 22:41
  • 1
    Why use a StringBuilder in the first place? What's wrong with blockID + ":" + threadID? And why building a String to compute the hashCode of 2 integers? Let the IDE generate the code for you: it will be much more efficient. Commented Jan 14, 2015 at 22:42
  • I must admit that I'm a little surprised by the factor 100 but why should we assume that the JVM itself handles string concatenation in a less efficient manner than provided by a class from the standard library? Commented Jan 14, 2015 at 22:42
  • I too have seen + to be faster than using StringBuilder - even in a loop where using a StringBuilder is supposed to be faster. Commented Jan 14, 2015 at 22:47

2 Answers 2

14

Because you're inadvertently setting the initial capacity of the StringBuilder instead of appending blockID to it. See constructor documentation here.

public StringBuilder(int capacity)

Constructs a string builder with no characters in it and an initial capacity specified by the capacity argument.

Try this instead:

StringBuilder s = new StringBuilder(9); s.append(blockID).append(':').append(threadID); 
Sign up to request clarification or add additional context in comments.

2 Comments

Upvoted, because this definitely makes sense, but it turned out not to be StringBuilder at all - JUnit was introducing some REALLY strange performance behavior for reasons I still can't figure out. Isolating my test from JUnit fixed the perceived issue in performance.
I believe my blockID stays small enough in the test that the overhead of allocating too much or too little StringBuilder initial capacity doesn't overcome the bigger issues present when using JUnit to run the test. Even so ... my usage of the constructor was erroneous, so I appreciate the help!
1

You need to check your test as your first case is actually doing.

public int hashCode(){ if(!hashed){ StringBuilder s = new StringBuilder( new StringBuilder(blockID).append(":").append(threadID).toString()); hash = s.toString().hashCode(); hashed= true; } return hash; } 

In other words it is doing everything in the second case and more so it will be slower.

In short, I suspect your test is wrong, not that you are getting better performance.

1 Comment

I suspect you're right about there being something wrong with the test itself. I was using JUnit to run this test. When I duplicated the test code and ran it apart from JUnit, the performance differences disappeared. There must be something asymptotically slower in some of the assert calls I was making when compared to the simple if statements I used to replace them in the non-JUnit version of the test. Very odd ... good catch!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.