From 95e32f0f9a6c65b4e9089608d4ff7e468200cbeb Mon Sep 17 00:00:00 2001 From: Jeff Sharkey Date: Mon, 17 Aug 2020 15:49:46 -0600 Subject: [PATCH] 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 --- .../src/android/os/ParcelObtainPerfTest.java | 88 +++++++++++++ .../core/src/android/os/ParcelPerfTest.java | 15 --- core/java/android/os/Parcel.java | 118 ++++++++++++------ 3 files changed, 165 insertions(+), 56 deletions(-) create mode 100644 apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java diff --git a/apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java b/apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java new file mode 100644 index 0000000000000..760ae12bcc07c --- /dev/null +++ b/apct-tests/perftests/core/src/android/os/ParcelObtainPerfTest.java @@ -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(); + } + } + } + } +} diff --git a/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java b/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java index 4db9262f7fe36..be2f9d72663ee 100644 --- a/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java +++ b/apct-tests/perftests/core/src/android/os/ParcelPerfTest.java @@ -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); diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 415e5a60a1406..dce474fd8654a 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -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