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

Commit 162f86dd authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

More efficient alternatives to ByteBuffer.

Some upcoming binary XML work needs to efficiently read and write
raw bytes, and we initially started using ByteBuffer.  However, that
design had additional overhead since we were performing bounds checks
twice (once to fill/drain buffers, then again to parse data).  In
addition, the upstream ByteBuffer makes per-byte method invocations
internally, instead of going directly the the buffer.

This change introduces FastDataInput/Output as local implementations
of DataInput/Output which are focused on performance.  They also
handle fill/drain from an underlying Input/OutputStream, and the
included benchmarks show reading 3x faster and writing 2x faster:

    timeRead_Upstream_mean: 5543730
    timeRead_Local_mean: 1698602

    timeWrite_Upstream_mean: 3731119
    timeWrite_Local_mean: 1885983

We also use the new CharsetUtils methods to write UTF-8 values
directly without additional allocations whenever possible.  This
requires using a non-movable buffer to avoid JNI overhead to gain
the 30% benchmarked performance wins.

Bug: 171832118
Test: atest CorePerfTests:com.android.internal.util.FastDataPerfTest
Test: atest FrameworksCoreTests:com.android.internal.util.FastDataTest
Change-Id: If28ee381adb528d03cc9851d78236d985b6ede16
parent 7a38dcc9
Loading
Loading
Loading
Loading
+132 −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 com.android.internal.util;

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

import androidx.test.filters.LargeTest;
import androidx.test.runner.AndroidJUnit4;

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

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;

@LargeTest
@RunWith(AndroidJUnit4.class)
public class FastDataPerfTest {
    @Rule
    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();

    private static final int OUTPUT_SIZE = 64000;
    private static final int BUFFER_SIZE = 4096;

    @Test
    public void timeWrite_Upstream() throws IOException {
        final ByteArrayOutputStream os = new ByteArrayOutputStream(OUTPUT_SIZE);
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            os.reset();
            final BufferedOutputStream bos = new BufferedOutputStream(os, BUFFER_SIZE);
            final DataOutput out = new DataOutputStream(bos);
            doWrite(out);
            bos.flush();
        }
    }

    @Test
    public void timeWrite_Local() throws IOException {
        final ByteArrayOutputStream os = new ByteArrayOutputStream(OUTPUT_SIZE);
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            os.reset();
            final FastDataOutput out = new FastDataOutput(os, BUFFER_SIZE);
            doWrite(out);
            out.flush();
        }
    }

    @Test
    public void timeRead_Upstream() throws Exception {
        final ByteArrayInputStream is = new ByteArrayInputStream(doWrite());
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            is.reset();
            final BufferedInputStream bis = new BufferedInputStream(is, BUFFER_SIZE);
            final DataInput in = new DataInputStream(bis);
            doRead(in);
        }
    }

    @Test
    public void timeRead_Local() throws Exception {
        final ByteArrayInputStream is = new ByteArrayInputStream(doWrite());
        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
        while (state.keepRunning()) {
            is.reset();
            final DataInput in = new FastDataInput(is, BUFFER_SIZE);
            doRead(in);
        }
    }

    /**
     * Since each iteration is around 64 bytes, we need to iterate many times to
     * exercise the buffer logic.
     */
    private static final int REPEATS = 1000;

    private static byte[] doWrite() throws IOException {
        final ByteArrayOutputStream os = new ByteArrayOutputStream(OUTPUT_SIZE);
        final DataOutput out = new DataOutputStream(os);
        doWrite(out);
        return os.toByteArray();
    }

    private static void doWrite(DataOutput out) throws IOException {
        for (int i = 0; i < REPEATS; i++) {
            out.writeByte(Byte.MAX_VALUE);
            out.writeShort(Short.MAX_VALUE);
            out.writeInt(Integer.MAX_VALUE);
            out.writeLong(Long.MAX_VALUE);
            out.writeFloat(Float.MAX_VALUE);
            out.writeDouble(Double.MAX_VALUE);
            out.writeUTF("com.example.typical_package_name");
        }
    }

    private static void doRead(DataInput in) throws IOException {
        for (int i = 0; i < REPEATS; i++) {
            in.readByte();
            in.readShort();
            in.readInt();
            in.readLong();
            in.readFloat();
            in.readDouble();
            in.readUTF();
        }
    }
}
+256 −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 com.android.internal.util;

import android.annotation.NonNull;

import java.io.BufferedInputStream;
import java.io.Closeable;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;

/**
 * Optimized implementation of {@link DataInput} which buffers data in memory
 * from the underlying {@link InputStream}.
 * <p>
 * Benchmarks have demonstrated this class is 3x more efficient than using a
 * {@link DataInputStream} with a {@link BufferedInputStream}.
 */
public class FastDataInput implements DataInput, Closeable {
    private static final int MAX_UNSIGNED_SHORT = 65_535;

    private final InputStream mIn;

    private final byte[] mBuffer;
    private final int mBufferCap;

    private int mBufferPos;
    private int mBufferLim;

    /**
     * Values that have been "interned" by {@link #readInternedUTF()}.
     */
    private int mStringRefCount = 0;
    private String[] mStringRefs = new String[32];

    public FastDataInput(@NonNull InputStream in, int bufferSize) {
        mIn = Objects.requireNonNull(in);
        if (bufferSize < 8) {
            throw new IllegalArgumentException();
        }

        mBuffer = new byte[bufferSize];
        mBufferCap = mBuffer.length;
    }

    private void fill(int need) throws IOException {
        final int remain = mBufferLim - mBufferPos;
        System.arraycopy(mBuffer, mBufferPos, mBuffer, 0, remain);
        mBufferPos = 0;
        mBufferLim = remain;
        need -= remain;

        while (need > 0) {
            int c = mIn.read(mBuffer, mBufferLim, mBufferCap - mBufferLim);
            if (c == -1) {
                throw new EOFException();
            } else {
                mBufferLim += c;
                need -= c;
            }
        }
    }

    @Override
    public void close() throws IOException {
        mIn.close();
    }

    @Override
    public void readFully(byte[] b) throws IOException {
        readFully(b, 0, b.length);
    }

    @Override
    public void readFully(byte[] b, int off, int len) throws IOException {
        // Attempt to read directly from buffer space if there's enough room,
        // otherwise fall back to chunking into place
        if (mBufferCap >= len) {
            if (mBufferLim - mBufferPos < len) fill(len);
            System.arraycopy(mBuffer, mBufferPos, b, off, len);
            mBufferPos += len;
        } else {
            final int remain = mBufferLim - mBufferPos;
            System.arraycopy(mBuffer, mBufferPos, b, off, remain);
            mBufferPos += remain;
            off += remain;
            len -= remain;

            while (len > 0) {
                int c = mIn.read(b, off, len);
                if (c == -1) {
                    throw new EOFException();
                } else {
                    off += c;
                    len -= c;
                }
            }
        }
    }

    @Override
    public String readUTF() throws IOException {
        // Attempt to read directly from buffer space if there's enough room,
        // otherwise fall back to chunking into place
        final int len = readUnsignedShort();
        if (mBufferCap >= len) {
            if (mBufferLim - mBufferPos < len) fill(len);
            final String res = new String(mBuffer, mBufferPos, len, StandardCharsets.UTF_8);
            mBufferPos += len;
            return res;
        } else {
            final byte[] tmp = new byte[len];
            readFully(tmp, 0, tmp.length);
            return new String(tmp, StandardCharsets.UTF_8);
        }
    }

    /**
     * Read a {@link String} value with the additional signal that the given
     * value is a candidate for being canonicalized, similar to
     * {@link String#intern()}.
     * <p>
     * Canonicalization is implemented by writing each unique string value once
     * the first time it appears, and then writing a lightweight {@code short}
     * reference when that string is written again in the future.
     *
     * @see FastDataOutput#writeInternedUTF(String)
     */
    public @NonNull String readInternedUTF() throws IOException {
        final int ref = readUnsignedShort();
        if (ref == MAX_UNSIGNED_SHORT) {
            final String s = readUTF();

            // We can only safely intern when we have remaining values; if we're
            // full we at least sent the string value above
            if (mStringRefCount < MAX_UNSIGNED_SHORT) {
                if (mStringRefCount == mStringRefs.length) {
                    mStringRefs = Arrays.copyOf(mStringRefs,
                            mStringRefCount + (mStringRefCount >> 1));
                }
                mStringRefs[mStringRefCount++] = s;
            }

            return s;
        } else {
            return mStringRefs[ref];
        }
    }

    @Override
    public boolean readBoolean() throws IOException {
        return readByte() != 0;
    }

    /**
     * Returns the same decoded value as {@link #readByte()} but without
     * actually consuming the underlying data.
     */
    public byte peekByte() throws IOException {
        if (mBufferLim - mBufferPos < 1) fill(1);
        return mBuffer[mBufferPos];
    }

    @Override
    public byte readByte() throws IOException {
        if (mBufferLim - mBufferPos < 1) fill(1);
        return mBuffer[mBufferPos++];
    }

    @Override
    public int readUnsignedByte() throws IOException {
        return Byte.toUnsignedInt(readByte());
    }

    @Override
    public short readShort() throws IOException {
        if (mBufferLim - mBufferPos < 2) fill(2);
        return (short) (((mBuffer[mBufferPos++] & 0xff) <<  8) |
                        ((mBuffer[mBufferPos++] & 0xff) <<  0));
    }

    @Override
    public int readUnsignedShort() throws IOException {
        return Short.toUnsignedInt((short) readShort());
    }

    @Override
    public char readChar() throws IOException {
        return (char) readShort();
    }

    @Override
    public int readInt() throws IOException {
        if (mBufferLim - mBufferPos < 4) fill(4);
        return (((mBuffer[mBufferPos++] & 0xff) << 24) |
                ((mBuffer[mBufferPos++] & 0xff) << 16) |
                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
                ((mBuffer[mBufferPos++] & 0xff) <<  0));
    }

    @Override
    public long readLong() throws IOException {
        if (mBufferLim - mBufferPos < 8) fill(8);
        int h = ((mBuffer[mBufferPos++] & 0xff) << 24) |
                ((mBuffer[mBufferPos++] & 0xff) << 16) |
                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
                ((mBuffer[mBufferPos++] & 0xff) <<  0);
        int l = ((mBuffer[mBufferPos++] & 0xff) << 24) |
                ((mBuffer[mBufferPos++] & 0xff) << 16) |
                ((mBuffer[mBufferPos++] & 0xff) <<  8) |
                ((mBuffer[mBufferPos++] & 0xff) <<  0);
        return (((long) h) << 32L) | ((long) l) & 0xffffffffL;
    }

    @Override
    public float readFloat() throws IOException {
        return Float.intBitsToFloat(readInt());
    }

    @Override
    public double readDouble() throws IOException {
        return Double.longBitsToDouble(readLong());
    }

    @Override
    public int skipBytes(int n) throws IOException {
        // Callers should read data piecemeal
        throw new UnsupportedOperationException();
    }

    @Override
    public String readLine() throws IOException {
        // Callers should read data piecemeal
        throw new UnsupportedOperationException();
    }
}
+228 −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 com.android.internal.util;

import android.annotation.NonNull;
import android.util.CharsetUtils;

import dalvik.system.VMRuntime;

import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.Flushable;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Objects;

/**
 * Optimized implementation of {@link DataOutput} which buffers data in memory
 * before flushing to the underlying {@link OutputStream}.
 * <p>
 * Benchmarks have demonstrated this class is 2x more efficient than using a
 * {@link DataOutputStream} with a {@link BufferedOutputStream}.
 */
public class FastDataOutput implements DataOutput, Flushable, Closeable {
    private static final int MAX_UNSIGNED_SHORT = 65_535;

    private final OutputStream mOut;

    private final byte[] mBuffer;
    private final long mBufferPtr;
    private final int mBufferCap;

    private int mBufferPos;

    /**
     * Values that have been "interned" by {@link #writeInternedUTF(String)}.
     */
    private HashMap<String, Short> mStringRefs = new HashMap<>();

    public FastDataOutput(@NonNull OutputStream out, int bufferSize) {
        mOut = Objects.requireNonNull(out);
        if (bufferSize < 8) {
            throw new IllegalArgumentException();
        }

        mBuffer = (byte[]) VMRuntime.getRuntime().newNonMovableArray(byte.class, bufferSize);
        mBufferPtr = VMRuntime.getRuntime().addressOf(mBuffer);
        mBufferCap = mBuffer.length;
    }

    private void drain() throws IOException {
        if (mBufferPos > 0) {
            mOut.write(mBuffer, 0, mBufferPos);
            mBufferPos = 0;
        }
    }

    @Override
    public void flush() throws IOException {
        drain();
        mOut.flush();
    }

    @Override
    public void close() throws IOException {
        mOut.close();
    }

    @Override
    public void write(int b) throws IOException {
        writeByte(b);
    }

    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        if (mBufferCap < len) {
            drain();
            mOut.write(b, off, len);
        } else {
            if (mBufferCap - mBufferPos < len) drain();
            System.arraycopy(b, off, mBuffer, mBufferPos, len);
            mBufferPos += len;
        }
    }

    @Override
    public void writeUTF(String s) throws IOException {
        // Attempt to write directly to buffer space if there's enough room,
        // otherwise fall back to chunking into place
        if (mBufferCap - mBufferPos < 2 + s.length()) drain();
        final int res = CharsetUtils.toUtf8Bytes(s, mBufferPtr, mBufferPos + 2,
                mBufferCap - mBufferPos - 2);
        if (res >= 0) {
            if (res > MAX_UNSIGNED_SHORT) {
                throw new IOException("UTF-8 length too large: " + res);
            }
            writeShort(res);
            mBufferPos += res;
        } else {
            final byte[] tmp = s.getBytes(StandardCharsets.UTF_8);
            if (tmp.length > MAX_UNSIGNED_SHORT) {
                throw new IOException("UTF-8 length too large: " + res);
            }
            writeShort(tmp.length);
            write(tmp, 0, tmp.length);
        }
    }

    /**
     * Write a {@link String} value with the additional signal that the given
     * value is a candidate for being canonicalized, similar to
     * {@link String#intern()}.
     * <p>
     * Canonicalization is implemented by writing each unique string value once
     * the first time it appears, and then writing a lightweight {@code short}
     * reference when that string is written again in the future.
     *
     * @see FastDataInput#readInternedUTF()
     */
    public void writeInternedUTF(@NonNull String s) throws IOException {
        Short ref = mStringRefs.get(s);
        if (ref != null) {
            writeShort(ref);
        } else {
            writeShort(MAX_UNSIGNED_SHORT);
            writeUTF(s);

            // We can only safely intern when we have remaining values; if we're
            // full we at least sent the string value above
            ref = (short) mStringRefs.size();
            if (ref < MAX_UNSIGNED_SHORT) {
                mStringRefs.put(s, ref);
            }
        }
    }

    @Override
    public void writeBoolean(boolean v) throws IOException {
        writeByte(v ? 1 : 0);
    }

    @Override
    public void writeByte(int v) throws IOException {
        if (mBufferCap - mBufferPos < 1) drain();
        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
    }

    @Override
    public void writeShort(int v) throws IOException {
        if (mBufferCap - mBufferPos < 2) drain();
        mBuffer[mBufferPos++] = (byte) ((v >>  8) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
    }

    @Override
    public void writeChar(int v) throws IOException {
        writeShort((short) v);
    }

    @Override
    public void writeInt(int v) throws IOException {
        if (mBufferCap - mBufferPos < 4) drain();
        mBuffer[mBufferPos++] = (byte) ((v >> 24) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((v >> 16) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((v >>  8) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((v >>  0) & 0xff);
    }

    @Override
    public void writeLong(long v) throws IOException {
        if (mBufferCap - mBufferPos < 8) drain();
        int i = (int) (v >> 32);
        mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((i >>  8) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((i >>  0) & 0xff);
        i = (int) v;
        mBuffer[mBufferPos++] = (byte) ((i >> 24) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((i >> 16) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((i >>  8) & 0xff);
        mBuffer[mBufferPos++] = (byte) ((i >>  0) & 0xff);
    }

    @Override
    public void writeFloat(float v) throws IOException {
        writeInt(Float.floatToIntBits(v));
    }

    @Override
    public void writeDouble(double v) throws IOException {
        writeLong(Double.doubleToLongBits(v));
    }

    @Override
    public void writeBytes(String s) throws IOException {
        // Callers should use writeUTF()
        throw new UnsupportedOperationException();
    }

    @Override
    public void writeChars(String s) throws IOException {
        // Callers should use writeUTF()
        throw new UnsupportedOperationException();
    }
}
+348 −0

File added.

Preview size limit exceeded, changes collapsed.