EDIT2: rephrasing the question due to more details.
This got me curious. Profiling this code:
public class ProfilerTest {
private static int loops = 1_000_000_000;
private static int listSize = 10;
public static void main(String[] args) {
for (int i = 0; i < loops; i++) {
var list = new ArrayList<Integer>(listSize);
for (int j = 0; j < listSize; j++) {
list.add(j);
}
assert list.size() == listSize;
}
}
}
Profiler says grow()
gets called by add(Object)
. Looking at the source, my JDKs implementation of ArrayList.add(Object)
calls this helper:
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
which should only call grow()
when exceeding the size (s
).
Even when I increase the initialCapacity
to 2*listSize
, grow()
gets called. What am I missing here?
I have found through profiling, that one of my methods is sometimes calling ArrayList.grow()
. The thing is, the method should not call grow()
at all since I know the desired size. Modifications to the size can happen in other methods though.
Surprised by this, I did a little benchmark. The results seem strange to me. Not only is MatchingCapacity
performing much worse, its variability is also much higher, at least for small sizes, which, in my use case are the most common.
The questions I have:
grow()
when you fill an ArrayList to its exact amount, or might that just be due to profiler inaccuracies (i.e. subroutine calls grow()
but sometimes ends up getting measured in parent caller?)initialCapacity
slower in the benchmark, is it somehow breaking java compiler optimizations?unfortunately I am getting an error when trying to profile the JMH benchmark for more insights..
EDIT: as was pointed out in the comments I was reading the numbers all wrong, assuming I was looking at time instead of throughput, so the remaining question is about the profiler.
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 1, time = 3)
@Measurement(iterations = 3, time = 3)
@Fork(value=2)
public class PerformanceTest {
private int size = 30;
@Benchmark
public ArrayList<Integer> MatchingCapacity() {
var list = new ArrayList<Integer>(size);
for (int i = 0; i < size; i++) {
list.add(i);
}
return list;
}
@Benchmark
public ArrayList<Integer> MismatchingCapacity() {
var list = new ArrayList<Integer>(size-1);
for (int i = 0; i < size; i++) {
list.add(i);
}
return list;
}
@Benchmark
public ArrayList<Integer> OversizedCapacity() {
var list = new ArrayList<Integer>(size + size/2);
for (int i = 0; i < size; i++) {
list.add(i);
}
return list;
}
@Benchmark
public ArrayList<Integer> DefaultCapacity() {
var list = new ArrayList<Integer>();
for (int i = 0; i < size; i++) {
list.add(i);
}
return list;
}
}
# JMH version: 1.35
# VM version: JDK 17.0.3.1, Java HotSpot(TM) 64-Bit Server VM, 17.0.3.1+2-LTS-6
size=3
PerformanceTest.DefaultCapacity thrpt 6 37681589.763 � 10696982.725 ops/s
PerformanceTest.MatchingCapacity thrpt 6 54285199.864 � 20674575.714 ops/s
PerformanceTest.MismatchingCapacity thrpt 6 21430358.674 � 11202223.852 ops/s
PerformanceTest.OversizedCapacity thrpt 6 50519838.911 � 19575866.550 ops/s
size=30
PerformanceTest.DefaultCapacity thrpt 6 4253362.573 � 120027.530 ops/s
PerformanceTest.MatchingCapacity thrpt 6 8071248.285 � 5138133.875 ops/s
PerformanceTest.MismatchingCapacity thrpt 6 5108706.433 � 562235.033 ops/s
PerformanceTest.OversizedCapacity thrpt 6 6867041.239 � 5561825.025 ops/s
size=300
PerformanceTest.DefaultCapacity thrpt 6 466885.040 � 486083.752 ops/s
PerformanceTest.MatchingCapacity thrpt 6 693234.753 � 268499.890 ops/s
PerformanceTest.MismatchingCapacity thrpt 6 644182.306 � 324785.510 ops/s
PerformanceTest.OversizedCapacity thrpt 6 773775.315 � 26808.161 ops/s
size=3000
PerformanceTest.DefaultCapacity thrpt 6 58673.411 � 23803.125 ops/s
PerformanceTest.MatchingCapacity thrpt 6 64863.127 � 33194.262 ops/s
PerformanceTest.MismatchingCapacity thrpt 6 58921.951 � 21346.733 ops/s
PerformanceTest.OversizedCapacity thrpt 6 62658.432 � 32061.866 ops/s
The code in your sample does not cause the internal elementArray
to be resized for the ArrayList
s referenced by list
. To demonstrate that, I've used nasty reflection tricks to look into list
:
private static final int LOOPS = 1_000_000;
private static final int LIST_SIZE = 10;
public static void main(String[] args) throws Exception {
Field elementData = ArrayList.class.getDeclaredField("elementData");
elementData.setAccessible(true);
for (int i = 0; i < LOOPS; i++) {
var list = new ArrayList<Integer>(LIST_SIZE);
var initialLength = ((Object[]) elementData.get(list)).length;
for (int j = 0; j < LIST_SIZE; j++) {
list.add(j);
}
var endLength = ((Object[]) elementData.get(list)).length;
if (initialLength != endLength) {
throw new Exception("array was resized!");
}
if (list.size() != LIST_SIZE) {
throw new Exception("Unexpected list size!");
}
}
}
This needs to be run with --add-opens=java.base/java.util=ALL-UNNAMED
on recent JDKs, since it's doing ugly stuff that no production code should do.
What it does however is demonstrate that the elementData
of the list
has the same size before and after adding objects to it.