Hand-rolled linked list for Parcel performance.
While recently looking at pprof data, we noticed that Parcel.obtain()
and recycle() were some of the most heavily used methods inside the
system process. On the surface, this makes sense because these
methods are typically invoked at least twice for every Binder call.
Internally these methods had been maintaining a pool of cached Parcel
objects to avoid GC pressure, but unfortunately it was using an
array which resulted in O(n) scanning under heavy load, increasing
lock contention encountered by all Binder calls.
This change greatly reduces that contention by using a hand-rolled
linked list approach, mirroring android.os.Message. For a 1-thread
benchmark, this new approach has almost 2x throughput, and for a
16-thread benchmark it has almost 8x throughput.
As part of making this change we evaluated several approaches,
including using pure-GC with no pooling, a single AtomicReference,
and a pool of several AtomicReferences. To measure these we wrote
ParcelPoolBenchmark which simulates various levels of Binder load
using 1, 4 and 16 threads. Below are the relative benchmark results
compared to the previous approach before this CL:
1 thread 4 threads 16 threads
Pure GC 131.74% 32.76% 43.90%
Single AR 95.22% 25.54% 13.66%
Pooled AR 57.65% 16.21% 11.55%
Linked list 52.66% 18.06% 12.55%
On balance, the linked list approach performs well across the board,
and we bias towards it over the two AtomicReference approaches since
it performs slightly better on the single-threaded case, which is
the most representative of typical Binder load across all processes.
Bug: 165032569
Test: ./frameworks/base/libs/hwui/tests/scripts/prep_generic.sh little && atest CorePerfTests:android.os.ParcelObtainPerfTest
Change-Id: I190b1c8f7fd59855c3c2d36032512279691e2c04
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2016 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package android.os;
|
||||
|
||||
import androidx.test.InstrumentationRegistry;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class ParcelObtainPerfTest {
|
||||
private static final int ITERATIONS = 1_000_000;
|
||||
|
||||
@Test
|
||||
public void timeContention_01() throws Exception {
|
||||
timeContention(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeContention_04() throws Exception {
|
||||
timeContention(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeContention_16() throws Exception {
|
||||
timeContention(16);
|
||||
}
|
||||
|
||||
private static void timeContention(int numThreads) throws Exception {
|
||||
final long start = SystemClock.elapsedRealtime();
|
||||
{
|
||||
final ObtainThread[] threads = new ObtainThread[numThreads];
|
||||
for (int i = 0; i < numThreads; i++) {
|
||||
final ObtainThread thread = new ObtainThread(ITERATIONS / numThreads);
|
||||
thread.start();
|
||||
threads[i] = thread;
|
||||
}
|
||||
for (int i = 0; i < numThreads; i++) {
|
||||
threads[i].join();
|
||||
}
|
||||
}
|
||||
final long duration = SystemClock.elapsedRealtime() - start;
|
||||
|
||||
final Bundle results = new Bundle();
|
||||
results.putLong("duration", duration);
|
||||
InstrumentationRegistry.getInstrumentation().sendStatus(0, results);
|
||||
}
|
||||
|
||||
public static class ObtainThread extends Thread {
|
||||
public int iterations;
|
||||
|
||||
public ObtainThread(int iterations) {
|
||||
this.iterations = iterations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
while (iterations-- > 0) {
|
||||
final Parcel data = Parcel.obtain();
|
||||
final Parcel reply = Parcel.obtain();
|
||||
try {
|
||||
data.writeInt(32);
|
||||
reply.writeInt(32);
|
||||
} finally {
|
||||
reply.recycle();
|
||||
data.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,21 +158,6 @@ public class ParcelPerfTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeObtainRecycle() {
|
||||
// Use up the pooled instances.
|
||||
// A lot bigger than the actual size but in case someone increased it.
|
||||
final int POOL_SIZE = 100;
|
||||
for (int i = 0; i < POOL_SIZE; i++) {
|
||||
Parcel.obtain();
|
||||
}
|
||||
|
||||
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
|
||||
while (state.keepRunning()) {
|
||||
Parcel.obtain().recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeWriteException() {
|
||||
timeWriteException(false);
|
||||
|
||||
@@ -33,6 +33,8 @@ import android.util.SparseArray;
|
||||
import android.util.SparseBooleanArray;
|
||||
import android.util.SparseIntArray;
|
||||
|
||||
import com.android.internal.annotations.GuardedBy;
|
||||
|
||||
import dalvik.annotation.optimization.CriticalNative;
|
||||
import dalvik.annotation.optimization.FastNative;
|
||||
import dalvik.system.VMRuntime;
|
||||
@@ -222,9 +224,31 @@ public final class Parcel {
|
||||
*/
|
||||
private static boolean sParcelExceptionStackTrace;
|
||||
|
||||
private static final int POOL_SIZE = 6;
|
||||
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
|
||||
private static final Parcel[] sHolderPool = new Parcel[POOL_SIZE];
|
||||
private static final Object sPoolSync = new Object();
|
||||
|
||||
/** Next item in the linked list pool, if any */
|
||||
@GuardedBy("sPoolSync")
|
||||
private Parcel mPoolNext;
|
||||
|
||||
/** Head of a linked list pool of {@link Parcel} objects */
|
||||
@GuardedBy("sPoolSync")
|
||||
private static Parcel sOwnedPool;
|
||||
/** Head of a linked list pool of {@link Parcel} objects */
|
||||
@GuardedBy("sPoolSync")
|
||||
private static Parcel sHolderPool;
|
||||
|
||||
/** Total size of pool with head at {@link #sOwnedPool} */
|
||||
@GuardedBy("sPoolSync")
|
||||
private static int sOwnedPoolSize = 0;
|
||||
/** Total size of pool with head at {@link #sHolderPool} */
|
||||
@GuardedBy("sPoolSync")
|
||||
private static int sHolderPoolSize = 0;
|
||||
|
||||
/**
|
||||
* We're willing to pool up to 32 objects, which is sized to accommodate
|
||||
* both a data and reply Parcel for the maximum of 16 Binder threads.
|
||||
*/
|
||||
private static final int POOL_SIZE = 32;
|
||||
|
||||
// Keep in sync with frameworks/native/include/private/binder/ParcelValTypes.h.
|
||||
private static final int VAL_NULL = -1;
|
||||
@@ -420,22 +444,27 @@ public final class Parcel {
|
||||
*/
|
||||
@NonNull
|
||||
public static Parcel obtain() {
|
||||
final Parcel[] pool = sOwnedPool;
|
||||
synchronized (pool) {
|
||||
Parcel p;
|
||||
for (int i=0; i<POOL_SIZE; i++) {
|
||||
p = pool[i];
|
||||
if (p != null) {
|
||||
pool[i] = null;
|
||||
if (DEBUG_RECYCLE) {
|
||||
p.mStack = new RuntimeException();
|
||||
}
|
||||
p.mReadWriteHelper = ReadWriteHelper.DEFAULT;
|
||||
return p;
|
||||
}
|
||||
Parcel res = null;
|
||||
synchronized (sPoolSync) {
|
||||
if (sOwnedPool != null) {
|
||||
res = sOwnedPool;
|
||||
sOwnedPool = res.mPoolNext;
|
||||
res.mPoolNext = null;
|
||||
sOwnedPoolSize--;
|
||||
}
|
||||
}
|
||||
return new Parcel(0);
|
||||
|
||||
// When no cache found above, create from scratch; otherwise prepare the
|
||||
// cached object to be used
|
||||
if (res == null) {
|
||||
res = new Parcel(0);
|
||||
} else {
|
||||
if (DEBUG_RECYCLE) {
|
||||
res.mStack = new RuntimeException();
|
||||
}
|
||||
res.mReadWriteHelper = ReadWriteHelper.DEFAULT;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -446,19 +475,21 @@ public final class Parcel {
|
||||
if (DEBUG_RECYCLE) mStack = null;
|
||||
freeBuffer();
|
||||
|
||||
final Parcel[] pool;
|
||||
if (mOwnsNativeParcelObject) {
|
||||
pool = sOwnedPool;
|
||||
synchronized (sPoolSync) {
|
||||
if (sOwnedPoolSize < POOL_SIZE) {
|
||||
mPoolNext = sOwnedPool;
|
||||
sOwnedPool = this;
|
||||
sOwnedPoolSize++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mNativePtr = 0;
|
||||
pool = sHolderPool;
|
||||
}
|
||||
|
||||
synchronized (pool) {
|
||||
for (int i=0; i<POOL_SIZE; i++) {
|
||||
if (pool[i] == null) {
|
||||
pool[i] = this;
|
||||
return;
|
||||
synchronized (sPoolSync) {
|
||||
if (sHolderPoolSize < POOL_SIZE) {
|
||||
mPoolNext = sHolderPool;
|
||||
sHolderPool = this;
|
||||
sHolderPoolSize++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3496,22 +3527,27 @@ public final class Parcel {
|
||||
|
||||
/** @hide */
|
||||
static protected final Parcel obtain(long obj) {
|
||||
final Parcel[] pool = sHolderPool;
|
||||
synchronized (pool) {
|
||||
Parcel p;
|
||||
for (int i=0; i<POOL_SIZE; i++) {
|
||||
p = pool[i];
|
||||
if (p != null) {
|
||||
pool[i] = null;
|
||||
if (DEBUG_RECYCLE) {
|
||||
p.mStack = new RuntimeException();
|
||||
}
|
||||
p.init(obj);
|
||||
return p;
|
||||
}
|
||||
Parcel res = null;
|
||||
synchronized (sPoolSync) {
|
||||
if (sHolderPool != null) {
|
||||
res = sHolderPool;
|
||||
sHolderPool = res.mPoolNext;
|
||||
res.mPoolNext = null;
|
||||
sHolderPoolSize--;
|
||||
}
|
||||
}
|
||||
return new Parcel(obj);
|
||||
|
||||
// When no cache found above, create from scratch; otherwise prepare the
|
||||
// cached object to be used
|
||||
if (res == null) {
|
||||
res = new Parcel(obj);
|
||||
} else {
|
||||
if (DEBUG_RECYCLE) {
|
||||
res.mStack = new RuntimeException();
|
||||
}
|
||||
res.init(obj);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
private Parcel(long nativePtr) {
|
||||
|
||||
Reference in New Issue
Block a user