Skip to content

Commit

Permalink
Protobuf Lite ArrayLists: Defer allocating backing array until we hav…
Browse files Browse the repository at this point in the history
…e some idea how much to allocate.

This avoids allocating a backing array of size 10 when we are adding >10 elements.
- If we are adding objects one at a time, inflate a Object[10] and continue.
- If we are adding objects from a Collection, assume this is the only collection we are adding, and inflate a `Object[collection.size()]`
- If the existing array is non-empty, resize the array by 1.5x exponential growth as usual.

There's another small change where if we're addAll(<10 elements), we allocate an exact-sized backing array (e.g. size=3), rather than rounding up to size 10. See android/LiteAllocationTest. I think this is good: this will save memory in the common case of just calling .addAll() once, and if we call addAll twice, we grow still exponentially. But we could decide to avoid this or split it out into its own change.

This change involves moving some logic out of GeneratedMessageLite. I think the default size of the backing array is better handled inside ProtobufArrayList than inside GeneratedMessageLite's wrapper function.

PiperOrigin-RevId: 673612155
  • Loading branch information
mhansen authored and copybara-github committed Sep 12, 2024
1 parent 4e8469c commit 05a8a40
Show file tree
Hide file tree
Showing 8 changed files with 89 additions and 47 deletions.
18 changes: 13 additions & 5 deletions java/core/src/main/java/com/google/protobuf/BooleanArrayList.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.google.protobuf;

import static com.google.protobuf.Internal.checkNotNull;
import static java.lang.Math.max;

import com.google.protobuf.Internal.BooleanList;
import java.util.Arrays;
Expand All @@ -22,7 +23,9 @@
final class BooleanArrayList extends AbstractProtobufList<Boolean>
implements BooleanList, RandomAccess, PrimitiveNonBoxingCollection {

private static final BooleanArrayList EMPTY_LIST = new BooleanArrayList(new boolean[0], 0, false);
private static final boolean[] EMPTY_ARRAY = new boolean[0];

private static final BooleanArrayList EMPTY_LIST = new BooleanArrayList(EMPTY_ARRAY, 0, false);

public static BooleanArrayList emptyList() {
return EMPTY_LIST;
Expand All @@ -39,7 +42,7 @@ public static BooleanArrayList emptyList() {

/** Constructs a new mutable {@code BooleanArrayList} with default capacity. */
BooleanArrayList() {
this(new boolean[DEFAULT_CAPACITY], 0, true);
this(EMPTY_ARRAY, 0, true);
}

/**
Expand Down Expand Up @@ -101,7 +104,8 @@ public BooleanList mutableCopyWithCapacity(int capacity) {
if (capacity < size) {
throw new IllegalArgumentException();
}
return new BooleanArrayList(Arrays.copyOf(array, capacity), size, true);
boolean[] newArray = capacity == 0 ? EMPTY_ARRAY : Arrays.copyOf(array, capacity);
return new BooleanArrayList(newArray, size, true);
}

@Override
Expand Down Expand Up @@ -258,6 +262,10 @@ void ensureCapacity(int minCapacity) {
if (minCapacity <= array.length) {
return;
}
if (array.length == 0) {
array = new boolean[max(minCapacity, DEFAULT_CAPACITY)];
return;
}
// To avoid quadratic copying when calling .addAllFoo(List) in a loop, we must not size to
// exactly the requested capacity, but must exponentially grow instead. This is similar
// behaviour to ArrayList.
Expand All @@ -269,8 +277,8 @@ void ensureCapacity(int minCapacity) {
}

private static int growSize(int previousSize) {
// Resize to 1.5x the size
return ((previousSize * 3) / 2) + 1;
// Resize to 1.5x the size, rounding up to DEFAULT_CAPACITY.
return max(((previousSize * 3) / 2) + 1, DEFAULT_CAPACITY);
}

/**
Expand Down
18 changes: 13 additions & 5 deletions java/core/src/main/java/com/google/protobuf/DoubleArrayList.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.google.protobuf;

import static com.google.protobuf.Internal.checkNotNull;
import static java.lang.Math.max;

import com.google.protobuf.Internal.DoubleList;
import java.util.Arrays;
Expand All @@ -22,7 +23,9 @@
final class DoubleArrayList extends AbstractProtobufList<Double>
implements DoubleList, RandomAccess, PrimitiveNonBoxingCollection {

private static final DoubleArrayList EMPTY_LIST = new DoubleArrayList(new double[0], 0, false);
private static final double[] EMPTY_ARRAY = new double[0];

private static final DoubleArrayList EMPTY_LIST = new DoubleArrayList(EMPTY_ARRAY, 0, false);

public static DoubleArrayList emptyList() {
return EMPTY_LIST;
Expand All @@ -39,7 +42,7 @@ public static DoubleArrayList emptyList() {

/** Constructs a new mutable {@code DoubleArrayList} with default capacity. */
DoubleArrayList() {
this(new double[DEFAULT_CAPACITY], 0, true);
this(EMPTY_ARRAY, 0, true);
}

/**
Expand Down Expand Up @@ -101,7 +104,8 @@ public DoubleList mutableCopyWithCapacity(int capacity) {
if (capacity < size) {
throw new IllegalArgumentException();
}
return new DoubleArrayList(Arrays.copyOf(array, capacity), size, true);
double[] newArray = capacity == 0 ? EMPTY_ARRAY : Arrays.copyOf(array, capacity);
return new DoubleArrayList(newArray, size, true);
}

@Override
Expand Down Expand Up @@ -258,6 +262,10 @@ void ensureCapacity(int minCapacity) {
if (minCapacity <= array.length) {
return;
}
if (array.length == 0) {
array = new double[max(minCapacity, DEFAULT_CAPACITY)];
return;
}
// To avoid quadratic copying when calling .addAllFoo(List) in a loop, we must not size to
// exactly the requested capacity, but must exponentially grow instead. This is similar
// behaviour to ArrayList.
Expand All @@ -269,8 +277,8 @@ void ensureCapacity(int minCapacity) {
}

private static int growSize(int previousSize) {
// Resize to 1.5x the size
return ((previousSize * 3) / 2) + 1;
// Resize to 1.5x the size, rounding up to DEFAULT_CAPACITY.
return max(((previousSize * 3) / 2) + 1, DEFAULT_CAPACITY);
}

/**
Expand Down
18 changes: 13 additions & 5 deletions java/core/src/main/java/com/google/protobuf/FloatArrayList.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.google.protobuf;

import static com.google.protobuf.Internal.checkNotNull;
import static java.lang.Math.max;

import com.google.protobuf.Internal.FloatList;
import java.util.Arrays;
Expand All @@ -22,7 +23,9 @@
final class FloatArrayList extends AbstractProtobufList<Float>
implements FloatList, RandomAccess, PrimitiveNonBoxingCollection {

private static final FloatArrayList EMPTY_LIST = new FloatArrayList(new float[0], 0, false);
private static final float[] EMPTY_ARRAY = new float[0];

private static final FloatArrayList EMPTY_LIST = new FloatArrayList(EMPTY_ARRAY, 0, false);

public static FloatArrayList emptyList() {
return EMPTY_LIST;
Expand All @@ -39,7 +42,7 @@ public static FloatArrayList emptyList() {

/** Constructs a new mutable {@code FloatArrayList} with default capacity. */
FloatArrayList() {
this(new float[DEFAULT_CAPACITY], 0, true);
this(EMPTY_ARRAY, 0, true);
}

/**
Expand Down Expand Up @@ -100,7 +103,8 @@ public FloatList mutableCopyWithCapacity(int capacity) {
if (capacity < size) {
throw new IllegalArgumentException();
}
return new FloatArrayList(Arrays.copyOf(array, capacity), size, true);
float[] newArray = capacity == 0 ? EMPTY_ARRAY : Arrays.copyOf(array, capacity);
return new FloatArrayList(newArray, size, true);
}

@Override
Expand Down Expand Up @@ -257,6 +261,10 @@ void ensureCapacity(int minCapacity) {
if (minCapacity <= array.length) {
return;
}
if (array.length == 0) {
array = new float[max(minCapacity, DEFAULT_CAPACITY)];
return;
}
// To avoid quadratic copying when calling .addAllFoo(List) in a loop, we must not size to
// exactly the requested capacity, but must exponentially grow instead. This is similar
// behaviour to ArrayList.
Expand All @@ -268,8 +276,8 @@ void ensureCapacity(int minCapacity) {
}

private static int growSize(int previousSize) {
// Resize to 1.5x the size
return ((previousSize * 3) / 2) + 1;
// Resize to 1.5x the size, rounding up to DEFAULT_CAPACITY.
return max(((previousSize * 3) / 2) + 1, DEFAULT_CAPACITY);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1496,8 +1496,7 @@ protected static IntList emptyIntList() {

protected static IntList mutableCopy(IntList list) {
int size = list.size();
return list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
return list.mutableCopyWithCapacity(size * 2);
}

protected static LongList emptyLongList() {
Expand All @@ -1506,8 +1505,7 @@ protected static LongList emptyLongList() {

protected static LongList mutableCopy(LongList list) {
int size = list.size();
return list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
return list.mutableCopyWithCapacity(size * 2);
}

protected static FloatList emptyFloatList() {
Expand All @@ -1516,8 +1514,7 @@ protected static FloatList emptyFloatList() {

protected static FloatList mutableCopy(FloatList list) {
int size = list.size();
return list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
return list.mutableCopyWithCapacity(size * 2);
}

protected static DoubleList emptyDoubleList() {
Expand All @@ -1526,8 +1523,7 @@ protected static DoubleList emptyDoubleList() {

protected static DoubleList mutableCopy(DoubleList list) {
int size = list.size();
return list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
return list.mutableCopyWithCapacity(size * 2);
}

protected static BooleanList emptyBooleanList() {
Expand All @@ -1536,8 +1532,7 @@ protected static BooleanList emptyBooleanList() {

protected static BooleanList mutableCopy(BooleanList list) {
int size = list.size();
return list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
return list.mutableCopyWithCapacity(size * 2);
}

protected static <E> ProtobufList<E> emptyProtobufList() {
Expand All @@ -1546,8 +1541,7 @@ protected static <E> ProtobufList<E> emptyProtobufList() {

protected static <E> ProtobufList<E> mutableCopy(ProtobufList<E> list) {
int size = list.size();
return list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
return list.mutableCopyWithCapacity(size * 2);
}

/**
Expand Down
18 changes: 13 additions & 5 deletions java/core/src/main/java/com/google/protobuf/IntArrayList.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.google.protobuf;

import static com.google.protobuf.Internal.checkNotNull;
import static java.lang.Math.max;

import com.google.protobuf.Internal.IntList;
import java.util.Arrays;
Expand All @@ -22,7 +23,9 @@
final class IntArrayList extends AbstractProtobufList<Integer>
implements IntList, RandomAccess, PrimitiveNonBoxingCollection {

private static final IntArrayList EMPTY_LIST = new IntArrayList(new int[0], 0, false);
private static final int[] EMPTY_ARRAY = new int[0];

private static final IntArrayList EMPTY_LIST = new IntArrayList(EMPTY_ARRAY, 0, false);

public static IntArrayList emptyList() {
return EMPTY_LIST;
Expand All @@ -39,7 +42,7 @@ public static IntArrayList emptyList() {

/** Constructs a new mutable {@code IntArrayList} with default capacity. */
IntArrayList() {
this(new int[DEFAULT_CAPACITY], 0, true);
this(EMPTY_ARRAY, 0, true);
}

/**
Expand Down Expand Up @@ -100,7 +103,8 @@ public IntList mutableCopyWithCapacity(int capacity) {
if (capacity < size) {
throw new IllegalArgumentException();
}
return new IntArrayList(Arrays.copyOf(array, capacity), size, true);
int[] newArray = capacity == 0 ? EMPTY_ARRAY : Arrays.copyOf(array, capacity);
return new IntArrayList(newArray, size, true);
}

@Override
Expand Down Expand Up @@ -257,6 +261,10 @@ void ensureCapacity(int minCapacity) {
if (minCapacity <= array.length) {
return;
}
if (array.length == 0) {
array = new int[max(minCapacity, DEFAULT_CAPACITY)];
return;
}
// To avoid quadratic copying when calling .addAllFoo(List) in a loop, we must not size to
// exactly the requested capacity, but must exponentially grow instead. This is similar
// behaviour to ArrayList.
Expand All @@ -268,8 +276,8 @@ void ensureCapacity(int minCapacity) {
}

private static int growSize(int previousSize) {
// Resize to 1.5x the size
return ((previousSize * 3) / 2) + 1;
// Resize to 1.5x the size, rounding up to DEFAULT_CAPACITY.
return max(((previousSize * 3) / 2) + 1, DEFAULT_CAPACITY);
}

/**
Expand Down
18 changes: 13 additions & 5 deletions java/core/src/main/java/com/google/protobuf/LongArrayList.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package com.google.protobuf;

import static com.google.protobuf.Internal.checkNotNull;
import static java.lang.Math.max;

import com.google.protobuf.Internal.LongList;
import java.util.Arrays;
Expand All @@ -22,7 +23,9 @@
final class LongArrayList extends AbstractProtobufList<Long>
implements LongList, RandomAccess, PrimitiveNonBoxingCollection {

private static final LongArrayList EMPTY_LIST = new LongArrayList(new long[0], 0, false);
private static final long[] EMPTY_ARRAY = new long[0];

private static final LongArrayList EMPTY_LIST = new LongArrayList(EMPTY_ARRAY, 0, false);

public static LongArrayList emptyList() {
return EMPTY_LIST;
Expand All @@ -39,7 +42,7 @@ public static LongArrayList emptyList() {

/** Constructs a new mutable {@code LongArrayList} with default capacity. */
LongArrayList() {
this(new long[DEFAULT_CAPACITY], 0, true);
this(EMPTY_ARRAY, 0, true);
}

/**
Expand Down Expand Up @@ -100,7 +103,8 @@ public LongList mutableCopyWithCapacity(int capacity) {
if (capacity < size) {
throw new IllegalArgumentException();
}
return new LongArrayList(Arrays.copyOf(array, capacity), size, true);
long[] newArray = capacity == 0 ? EMPTY_ARRAY : Arrays.copyOf(array, capacity);
return new LongArrayList(newArray, size, true);
}

@Override
Expand Down Expand Up @@ -257,6 +261,10 @@ void ensureCapacity(int minCapacity) {
if (minCapacity <= array.length) {
return;
}
if (array.length == 0) {
array = new long[max(minCapacity, DEFAULT_CAPACITY)];
return;
}
// To avoid quadratic copying when calling .addAllFoo(List) in a loop, we must not size to
// exactly the requested capacity, but must exponentially grow instead. This is similar
// behaviour to ArrayList.
Expand All @@ -268,8 +276,8 @@ void ensureCapacity(int minCapacity) {
}

private static int growSize(int previousSize) {
// Resize to 1.5x the size
return ((previousSize * 3) / 2) + 1;
// Resize to 1.5x the size, rounding up to DEFAULT_CAPACITY.
return max(((previousSize * 3) / 2) + 1, DEFAULT_CAPACITY);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3577,9 +3577,7 @@ private int parseRepeatedField(
ProtobufList<?> list = (ProtobufList<?>) UNSAFE.getObject(message, fieldOffset);
if (!list.isModifiable()) {
final int size = list.size();
list =
list.mutableCopyWithCapacity(
size == 0 ? AbstractProtobufList.DEFAULT_CAPACITY : size * 2);
list = list.mutableCopyWithCapacity(size * 2);
UNSAFE.putObject(message, fieldOffset, list);
}
switch (fieldType) {
Expand Down
Loading

0 comments on commit 05a8a40

Please sign in to comment.