Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7a38dcc9 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

CharsetUtils alternatives that avoid allocations.

Internally String.getBytes() calls libcore.util.CharsetUtils methods
for a handful of common charsets, but that path requires new memory
allocations for every call.

This change introduces alternative versions of those methods which
attempt to encode data directly into an already-allocated memory
region.  If the destination is to small, callers can detect and pivot
back to calling String.getBytes().

The included benchmarks reveal these raw performance improvements,
in addition to the reduced GC load which is harder to measure:

    timeLocal_LargeBuffer[simple]_mean: 424
    timeLocal_SmallBuffer[simple]_mean: 511
    timeUpstream[simple]_mean: 800

    timeLocal_LargeBuffer[complex]_mean: 977
    timeLocal_SmallBuffer[complex]_mean: 1266
    timeUpstream[complex]_mean: 1468

Bug: 171832118
Test: atest CorePerfTests:android.util.CharsetUtilsPerfTest
Test: atest FrameworksCoreTests:android.util.CharsetUtilsTest
Change-Id: Iac1151e7cb8e88bf82339cada64b0936e1a7578b
parent 32d2258c
Loading
Loading
Loading
Loading
+91 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.util;

import android.perftests.utils.BenchmarkState;
import android.perftests.utils.PerfStatusReporter;

import androidx.test.filters.LargeTest;

import dalvik.system.VMRuntime;

import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;

@LargeTest
@RunWith(Parameterized.class)
public class CharsetUtilsPerfTest {
    @Rule
    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();

    @Parameterized.Parameter(0)
    public String mName;
    @Parameterized.Parameter(1)
    public String mValue;

    @Parameterized.Parameters(name = "{0}")
    public static Collection<Object[]> getParameters() {
        return Arrays.asList(new Object[][] {
                { "simple", "com.example.typical_package_name" },
                { "complex", "從不喜歡孤單一個 - 蘇永康/吳雨霏" },
        });
    }

    @Test
    public void timeUpstream() {
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            mValue.getBytes(StandardCharsets.UTF_8);
        }
    }

    /**
     * Measure performance of writing into a small buffer where bounds checking
     * requires careful measurement of encoded size.
     */
    @Test
    public void timeLocal_SmallBuffer() {
        final byte[] dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 64);
        final long destPtr = VMRuntime.getRuntime().addressOf(dest);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            CharsetUtils.toUtf8Bytes(mValue, destPtr, 0, dest.length);
        }
    }

    /**
     * Measure performance of writing into a large buffer where bounds checking
     * only needs a simple worst-case 4-bytes-per-char check.
     */
    @Test
    public void timeLocal_LargeBuffer() {
        final byte[] dest = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, 1024);
        final long destPtr = VMRuntime.getRuntime().addressOf(dest);

        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            CharsetUtils.toUtf8Bytes(mValue, destPtr, 0, dest.length);
       }
    }
}
+64 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.util;

import android.annotation.NonNull;

import dalvik.annotation.optimization.FastNative;

/**
 * Specializations of {@code libcore.util.CharsetUtils} which enable efficient
 * in-place encoding without making any new allocations.
 * <p>
 * These methods purposefully accept only non-movable byte array addresses to
 * avoid extra JNI overhead.
 *
 * @hide
 */
public class CharsetUtils {
    /**
     * Attempt to encode the given string as UTF-8 into the destination byte
     * array without making any new allocations.
     *
     * @param src string value to be encoded
     * @param dest destination byte array to encode into
     * @param destOff offset into destination where encoding should begin
     * @param destLen length of destination
     * @return the number of bytes written to the destination when encoded
     *         successfully, otherwise {@code -1} if not large enough
     */
    public static int toUtf8Bytes(@NonNull String src,
            long dest, int destOff, int destLen) {
        return toUtf8Bytes(src, src.length(), dest, destOff, destLen);
    }

    /**
     * Attempt to encode the given string as UTF-8 into the destination byte
     * array without making any new allocations.
     *
     * @param src string value to be encoded
     * @param srcLen exact length of string to be encoded
     * @param dest destination byte array to encode into
     * @param destOff offset into destination where encoding should begin
     * @param destLen length of destination
     * @return the number of bytes written to the destination when encoded
     *         successfully, otherwise {@code -1} if not large enough
     */
    @FastNative
    private static native int toUtf8Bytes(@NonNull String src, int srcLen,
            long dest, int destOff, int destLen);
}
+1 −0
Original line number Diff line number Diff line
@@ -134,6 +134,7 @@ cc_library_shared {
                "android_service_DataLoaderService.cpp",
                "android_util_AssetManager.cpp",
                "android_util_Binder.cpp",
                "android_util_CharsetUtils.cpp",
                "android_util_MemoryIntArray.cpp",
                "android_util_Process.cpp",
                "android_media_AudioDeviceAttributes.cpp",
+2 −0
Original line number Diff line number Diff line
@@ -105,6 +105,7 @@ namespace android {
 */
extern int register_android_app_admin_SecurityLog(JNIEnv* env);
extern int register_android_content_AssetManager(JNIEnv* env);
extern int register_android_util_CharsetUtils(JNIEnv* env);
extern int register_android_util_EventLog(JNIEnv* env);
extern int register_android_util_Log(JNIEnv* env);
extern int register_android_util_MemoryIntArray(JNIEnv* env);
@@ -1449,6 +1450,7 @@ static const RegJNIRec gRegJNI[] = {
        REG_JNI(register_com_android_internal_os_RuntimeInit),
        REG_JNI(register_com_android_internal_os_ZygoteInit_nativeZygoteInit),
        REG_JNI(register_android_os_SystemClock),
        REG_JNI(register_android_util_CharsetUtils),
        REG_JNI(register_android_util_EventLog),
        REG_JNI(register_android_util_Log),
        REG_JNI(register_android_util_MemoryIntArray),
+54 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.
 */

#include "core_jni_helpers.h"
#include "nativehelper/scoped_primitive_array.h"

namespace android {

static jint android_util_CharsetUtils_toUtf8Bytes(JNIEnv *env, jobject clazz,
        jstring src, jint srcLen, jlong dest, jint destOff, jint destLen) {
    char *destPtr = reinterpret_cast<char*>(dest);

    // Quickly check if destination has plenty of room for worst-case
    // 4-bytes-per-char encoded size
    if (destOff >= 0 && destOff + (srcLen * 4) < destLen) {
        env->GetStringUTFRegion(src, 0, srcLen, destPtr + destOff);
        return strlen(destPtr + destOff + srcLen) + srcLen;
    }

    // String still might fit in destination, but we need to measure
    // its actual encoded size to be sure
    const size_t encodedLen = env->GetStringUTFLength(src);
    if (destOff >= 0 && destOff + encodedLen < destLen) {
        env->GetStringUTFRegion(src, 0, srcLen, destPtr + destOff);
        return encodedLen;
    }

    return -1;
}

static const JNINativeMethod methods[] = {
    // @FastNative
    {"toUtf8Bytes",      "(Ljava/lang/String;IJII)I",
            (void*)android_util_CharsetUtils_toUtf8Bytes},
};

int register_android_util_CharsetUtils(JNIEnv *env) {
    return RegisterMethodsOrDie(env, "android/util/CharsetUtils", methods, NELEM(methods));
}

}
Loading