Reputation: 6123
Consider this method (just for illustration):
boolean isSmallNumber(String s) {
return (n in ["one", "two", "three", "four"]);
}
That, of course, is not Java, but it could be in your favourite alternative language that supports collection literals, such as Groovy or Kotlin. The expression is succinct, and, just like string literals, the compiler is allowed to put the collection literal in some static storage area(maybe even "intern()"
it).
Now enter Java 9:
boolean isSmallNumber(String s) {
return Set.of("one", "two", "three", "four").contains(s);
}
That's also succinct, but unfortunately it allocates a new Set on the heap every time you call it, and then immediately makes it available for garbage collection.
You can, of course, define a collection constant:
private static final Set<String> SMALL_NUMBERS = Set.of(...);
But then this definition could be a thousand lines away from the method definition in a large class, and you might not be able to think of a good descriptive name for it whereas a literal might be clearer (in this hypothetical case).
So, if I use Set.of(...)
inside the method, will the JIT compiler optimize away the creation of a new object each time the method is called?
Upvotes: 10
Views: 340
Reputation: 39576
I've crafted a simple JMH benchmark:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class Temp {
private Object value;
@Setup
public void setUp() {
value = 50;
}
@Benchmark
public boolean list1() {
return List.of("one").contains(value);
}
@Benchmark
public boolean list2() {
return List.of("one", "two").contains(value);
}
@Benchmark
public boolean list3() {
return List.of("one", "two", "three").contains(value);
}
@Benchmark
public boolean list4() {
return List.of("one", "two", "three", "four").contains(value);
}
@Benchmark
public boolean set1() {
return Set.of("one").contains(value);
}
@Benchmark
public boolean set2() {
return Set.of("one", "two").contains(value);
}
@Benchmark
public boolean set3() {
return Set.of("one", "two", "three").contains(value);
}
@Benchmark
public boolean set4() {
return Set.of("one", "two", "three", "four").contains(value);
}
}
After running the benchmark with -prof gc
, I can make the following conclusion: JIT optimizes list1
, list2
, set1
, set2
, but not list3
, list4
, set3
, set4
[1]
This seems totally reasonable because for N >= 3
listN
/setN
create more complex List
/Set
implementations than for N <= 2
.
List
implementation for 2 elements:
static final class List2<E> extends AbstractImmutableList<E> {
private final E e0;
private final E e1;
...
}
List
implementation for 3 or more elements:
static final class ListN<E> extends AbstractImmutableList<E> {
private final E[] elements;
...
}
ListN
contains another level of indirection (an array) which apparently makes escape analysis much harder.
JMH output (slightly changed to fit the page):
Benchmark Mode Cnt Score Error Units
list1 avgt 5 3,075 ? 1,165 ns/op
list1:·gc.alloc.rate avgt 5 0,131 ? 1,117 MB/sec
list1:·gc.alloc.rate.norm avgt 5 ? 10?? B/op
list1:·gc.count avgt 5 ? 0 counts
list2 avgt 5 3,161 ? 0,543 ns/op
list2:·gc.alloc.rate avgt 5 0,494 ? 3,065 MB/sec
list2:·gc.alloc.rate.norm avgt 5 0,001 ? 0,003 B/op
list2:·gc.count avgt 5 ? 0 counts
list3 avgt 5 33,094 ? 4,402 ns/op
list3:·gc.alloc.rate avgt 5 6316,970 ? 750,240 MB/sec
list3:·gc.alloc.rate.norm avgt 5 64,016 ? 0,089 B/op
list3:·gc.count avgt 5 169,000 counts
list3:·gc.time avgt 5 154,000 ms
list4 avgt 5 32,718 ? 3,657 ns/op
list4:·gc.alloc.rate avgt 5 6403,487 ? 729,235 MB/sec
list4:·gc.alloc.rate.norm avgt 5 64,004 ? 0,017 B/op
list4:·gc.count avgt 5 165,000 counts
list4:·gc.time avgt 5 146,000 ms
set1 avgt 5 3,218 ? 0,822 ns/op
set1:·gc.alloc.rate avgt 5 0,237 ? 1,973 MB/sec
set1:·gc.alloc.rate.norm avgt 5 ? 10?? B/op
set1:·gc.count avgt 5 ? 0 counts
set2 avgt 5 7,087 ? 2,029 ns/op
set2:·gc.alloc.rate avgt 5 0,647 ? 4,755 MB/sec
set2:·gc.alloc.rate.norm avgt 5 0,001 ? 0,010 B/op
set2:·gc.count avgt 5 ? 0 counts
set3 avgt 5 88,460 ? 16,834 ns/op
set3:·gc.alloc.rate avgt 5 3565,506 ? 687,900 MB/sec
set3:·gc.alloc.rate.norm avgt 5 96,000 ? 0,001 B/op
set3:·gc.count avgt 5 143,000 counts
set3:·gc.time avgt 5 108,000 ms
set4 avgt 5 118,652 ? 41,035 ns/op
set4:·gc.alloc.rate avgt 5 2887,359 ? 920,180 MB/sec
set4:·gc.alloc.rate.norm avgt 5 104,000 ? 0,001 B/op
set4:·gc.count avgt 5 136,000 counts
set4:·gc.time avgt 5 94,000 ms
[1] Java HotSpot(TM) 64-Bit Server VM (build 9+181, mixed mode)
Upvotes: 15