TL;DR section
Good news
Your measurement does expose a real effect.
Bad news
It does so mostly by chance because your benchmark has many technical flaws, and the effect it exposes is probably not the one you have in mind.
The new Character()
approach is faster if and only if HotSpot's Escape Analysis succeeds in proving that the resulting instance can be safely allocated on the stack instead of heap. Therefore the effect is not nearly as general as implied in your question.
Explanation of effect
The reason why new Character()
is faster is locality of reference: your instance is on the stack and all access to it is via CPU cache hits. When you reuse a cached instance, you must
- access a remote
static
field; - dereference it into a remote array;
- dereference an array entry into a remote
Character
instance; - acces the
char
contained in that instance.
Each dereference is a potential CPU cache miss. Furthermore, it forces a part of the cache to be redirected towards those remote locations, causing more cache misses on the input string and/or the stack locations.
DETAILS
I have run this code with jmh
:
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class Chars {
static String string = "12345678901234567890"; static {
for (int i = 0; i < 10; i++) string += string;
}
@GenerateMicroBenchmark
public void newChar() {
int len = string.length();
for (int i = 0; i < len; i++) new Character(string.charAt(i));
}
@GenerateMicroBenchmark
public void justChar() {
int len = string.length();
for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
}
}
This keeps the essence of your code, but eliminates some systematic errors like warmup and compilation times. These are the results:
Benchmark Mode Thr Cnt Sec Mean Mean error Units
o.s.Chars.justChar avgt 1 3 5 39.062 6.587 usec/op
o.s.Chars.newChar avgt 1 3 5 19.114 0.653 usec/op
And this would be my best guess at what's going on:
in
newChar
you are creating a fresh instance ofCharacter
. HotSpot's Escape Analysis can prove the instance never escapes, therefore it allows stack allocation, or, in the special case ofCharacter
, could eliminate the allocation altogether because the data from it is provably never used;in
justChar
you involve lookup into theCharacter
cache array, which has some cost.
UPDATE
In response to Aleks's criticism, I added some more methods to the benchmark. The main effect remains stable, but we get even more fine-grained details about the lesser optimization effects.
@GenerateMicroBenchmark
public int newCharUsed() {
int len = string.length(), sum = 0;
for (int i = 0; i < len; i++) sum += new Character(string.charAt(i));
return sum;
}
@GenerateMicroBenchmark
public int justCharUsed() {
int len = string.length(), sum = 0;
for (int i = 0; i < len; i++) sum += Character.valueOf(string.charAt(i));
return sum;
}
@GenerateMicroBenchmark
public void newChar() {
int len = string.length();
for (int i = 0; i < len; i++) new Character(string.charAt(i));
}
@GenerateMicroBenchmark
public void justChar() {
int len = string.length();
for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i));
}
@GenerateMicroBenchmark
public void newCharValue() {
int len = string.length();
for (int i = 0; i < len; i++) new Character(string.charAt(i)).charValue();
}
@GenerateMicroBenchmark
public void justCharValue() {
int len = string.length();
for (int i = 0; i < len; i++) Character.valueOf(string.charAt(i)).charValue();
}
DESCRIPTION:
- the base versions are
justChar
andnewChar
; ...Value
methods add thecharValue
call to the base version;...Used
methods add both thecharValue
call (implicitly) and use the value to preclude any Dead Code Elimination.
RESULTS:
Benchmark Mode Thr Cnt Sec Mean Mean error Units
o.s.Chars.justChar avgt 1 3 1 246.847 5.969 usec/op
o.s.Chars.justCharUsed avgt 1 3 1 370.031 26.057 usec/op
o.s.Chars.justCharValue avgt 1 3 1 296.342 60.705 usec/op
o.s.Chars.newChar avgt 1 3 1 123.302 10.596 usec/op
o.s.Chars.newCharUsed avgt 1 3 1 172.721 9.055 usec/op
o.s.Chars.newCharValue avgt 1 3 1 123.040 5.095 usec/op
- there is evidence of some Dead Code Elimination (DCE) both in
justChar
andnewChar
variants, but it is only partial; - with
newChar
variant, addingcharValue
has no effect so apparently it was DCE'd; - with
justChar
,charValue
does have an effect, so seems not to have been eliminated; - DCE has a minor overall effect, as witnessed by the stable difference between
newCharUsed
andjustCharUsed
.