Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

59: Improve Bytes Performance #111

Merged
merged 4 commits into from
Nov 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pbj-core/pbj-runtime/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
* limitations under the License.
*/

plugins { id("com.hedera.pbj.runtime") }
plugins {
id("com.hedera.pbj.runtime")
id("me.champeau.jmh").version("0.7.1")
}

testModuleInfo {
requires("org.junit.jupiter.api")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.hedera.pbj.runtime.io;

import com.hedera.pbj.runtime.io.buffer.Bytes;
import java.util.Random;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;

@Fork(value = 1)
@State(Scope.Benchmark)
public class BytesGetLong {

@Param({"10000"})
public int size = 10000;

private byte[] array;

private boolean printSum;

@Setup(Level.Trial)
public void init() {
System.out.println("Initializing array, size = " + size);
array = new byte[size];
final Random r = new Random(size);
for (int i = 0; i < size; i++) {
array[i] = (byte) r.nextInt(127);
}
}

@Setup(Level.Iteration)
public void initEach() {
printSum = true;
}

@Benchmark
public void testBytesGetLong(final Blackhole blackhole) {
long sum = 0;
final Bytes bytes = Bytes.wrap(array);
for (int i = 0; i < size + 1 - Long.BYTES; i++) {
sum += bytes.getLong(i);
}
if (printSum) {
System.out.println("sum = " + sum);
printSum = false;
}
blackhole.consume(sum);
}

@Benchmark
public void testUnsafeGetLong(final Blackhole blackhole) {
long sum = 0;
for (int i = 0; i < size + 1 - Long.BYTES; i++) {
sum += UnsafeUtils.getLong(array, i);
}
if (printSum) {
System.out.println("sum = " + sum);
printSum = false;
}
blackhole.consume(sum);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.hedera.pbj.runtime.io;

import java.lang.reflect.Field;
import java.nio.BufferUnderflowException;
import java.nio.ByteOrder;
import sun.misc.Unsafe;

/**
* A set of utility methods on top of sun.misc.Unsafe
*/
public class UnsafeUtils {

private static final Unsafe UNSAFE;

private static final boolean NEED_CHANGE_BYTE_ORDER;

private static final int BYTE_ARRAY_BASE_OFFSET;

static {
try {
final Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
UNSAFE = (Unsafe) f.get(null);
NEED_CHANGE_BYTE_ORDER = ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN;

Check warning on line 24 in pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/UnsafeUtils.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/UnsafeUtils.java#L24

Use equals() to compare object references.
nathanklick marked this conversation as resolved.
Show resolved Hide resolved
BYTE_ARRAY_BASE_OFFSET = UNSAFE.arrayBaseOffset(byte[].class);
} catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
throw new InternalError(e);
}
}

private UnsafeUtils() {
}

/**
* Reads an integer from the given array starting at the given offset. Array bytes are
* interpreted in BIG_ENDIAN order.
*
* @param arr The byte array
* @param offset The offset to read an integer at
* @return The integer number
* @throws java.nio.BufferOverflowException If array length is less than offset + integer bytes
*/
public static int getInt(final byte[] arr, final int offset) {
if (arr.length < offset + Integer.BYTES) {
throw new BufferUnderflowException();
}
final int value = UNSAFE.getInt(arr, BYTE_ARRAY_BASE_OFFSET + offset);
return NEED_CHANGE_BYTE_ORDER ? Integer.reverseBytes(value) : value;
}

/**
* Reads a long from the given array starting at the given offset. Array bytes are
* interpreted in BIG_ENDIAN order.
*
* @param arr The byte array
* @param offset The offset to read a long at
* @return The long number
* @throws java.nio.BufferOverflowException If array length is less than offset + long bytes
*/
public static long getLong(final byte[] arr, final int offset) {
if (arr.length < offset + Long.BYTES) {
throw new BufferUnderflowException();
}
final long value = UNSAFE.getLong(arr, BYTE_ARRAY_BASE_OFFSET + offset);
return NEED_CHANGE_BYTE_ORDER ? Long.reverseBytes(value) : value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.hedera.pbj.runtime.io.DataAccessException;
import com.hedera.pbj.runtime.io.ReadableSequentialData;
import com.hedera.pbj.runtime.io.UnsafeUtils;
import com.hedera.pbj.runtime.io.WritableSequentialData;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
Expand All @@ -11,7 +12,9 @@
import java.io.UncheckedIOException;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Base64;
import java.util.HexFormat;

Expand Down Expand Up @@ -168,6 +171,26 @@
// ================================================================================================================
// Object Methods

@Override
public int getInt(final long offset) {
return UnsafeUtils.getInt(buffer, Math.toIntExact(offset));
}

@Override
public int getInt(final long offset, @NonNull final ByteOrder byteOrder) {
return byteOrder == ByteOrder.BIG_ENDIAN ? getInt(offset) : Integer.reverseBytes(getInt(offset));

Check warning on line 181 in pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/Bytes.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/Bytes.java#L181

Use equals() to compare object references.
nathanklick marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public long getLong(final long offset) {
return UnsafeUtils.getLong(buffer, Math.toIntExact(offset));
}

@Override
public long getLong(final long offset, @NonNull final ByteOrder byteOrder) {
return byteOrder == ByteOrder.BIG_ENDIAN ? getLong(offset) : Long.reverseBytes(getLong(offset));

Check warning on line 191 in pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/Bytes.java

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

pbj-core/pbj-runtime/src/main/java/com/hedera/pbj/runtime/io/buffer/Bytes.java#L191

Use equals() to compare object references.
nathanklick marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Duplicate this {@link Bytes} by making a copy of the underlying byte array and returning a new {@link Bytes}
* over the copied data. Use this method when you need to wrap a copy of a byte array:
Expand Down Expand Up @@ -474,7 +497,7 @@
/** {@inheritDoc} */
@NonNull
@Override
public Bytes getBytes(long offset, long length) {
public Bytes getBytes(final long offset, final long length) {
validateOffset(offset);

if (length > this.length - offset) {
Expand All @@ -490,11 +513,34 @@
return new Bytes(buffer, Math.toIntExact(start + offset), Math.toIntExact(length));
}

/** {@inheritDoc} */
@NonNull
@Override
public String asUtf8String(final long offset, final long len) {
if (offset < 0 || offset + len > length()) {
throw new IndexOutOfBoundsException();
}
if (len == 0) {
return "";
}
return new String(buffer, Math.toIntExact(offset), length, StandardCharsets.UTF_8);
}

/** {@inheritDoc} */
@Override
public boolean contains(final long offset, @NonNull final byte[] bytes) {
validateOffset(offset);
final int len = bytes.length;
if (offset + len > length()) {
return false;
}
return Arrays.equals(buffer, Math.toIntExact(offset), Math.toIntExact(offset + len), bytes, 0, len);
}

/** {@inheritDoc} */
@NonNull
@Override
public Bytes slice(long offset, long length) {
public Bytes slice(final long offset, final long length) {
return getBytes(offset, length);
}

Expand All @@ -520,7 +566,7 @@
return ret;
}

private void validateOffset(long offset) {
private void validateOffset(final long offset) {
if (offset < 0 || offset > this.length) {
throw new IndexOutOfBoundsException("offset=" + offset + ", length=" + this.length);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -455,16 +455,15 @@ default String asUtf8String() {
* @param offset the offset into the buffer to start reading bytes from
* @param len the number of bytes to read
* @return data converted to string
* @throws BufferUnderflowException if {@code len} is greater than {@link #length()} - {@code offset}.
* @throws BufferUnderflowException if {@code len} is greater than {@link #length()} - {@code offset}
* @throws IndexOutOfBoundsException If the given {@code offset} is negative or not less than {@link #length()}
*/
@NonNull
default String asUtf8String(final long offset, final long len) {
if (len > length() - offset) {
throw new BufferUnderflowException();
}

if (offset < 0 || offset + len > length()) {
if (offset < 0) {
throw new IndexOutOfBoundsException();
}

Expand Down
3 changes: 3 additions & 0 deletions pbj-core/pbj-runtime/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/** Runtime module of code needed by PBJ generated code at runtime. */
module com.hedera.pbj.runtime {

requires jdk.unsupported;
requires transitive org.antlr.antlr4.runtime;

requires static com.github.spotbugs.annotations;

exports com.hedera.pbj.runtime;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.stream.Stream;
import org.junit.jupiter.params.provider.ValueSource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

final class BytesTest {
Expand Down Expand Up @@ -681,10 +683,19 @@ void changedToString() {
Bytes b1 = Bytes.wrap(new byte[]{0, 0, (byte)0xFF});
assertEquals("0000ff", b1.toString());
}

@Test
@DisplayName("Changed toString2")
void changedToString2() {
Bytes b1 = Bytes.wrap(new byte[]{(byte)0x0f, 0, (byte)0x0a});
assertEquals("0f000a", b1.toString());
}

@ParameterizedTest
@ValueSource(strings = { "", "a", "ab", "abc", "abc123", "✅" })
@DisplayName("Overridden asUtf8String")
void asUtf8StringTest(final String value) {
final Bytes bytes = Bytes.wrap(value.getBytes(StandardCharsets.UTF_8));
assertThat(bytes.asUtf8String()).isEqualTo(value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.hedera.pbj.runtime.io;

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

public class UnsafeUtilsTest {

// RandomAccessData.getInt()
private static int getInt(final byte[] arr, final int offset) {
final byte b1 = arr[offset];
final byte b2 = arr[offset + 1];
final byte b3 = arr[offset + 2];
final byte b4 = arr[offset + 3];
return ((b1 & 0xFF) << 24) | ((b2 & 0xFF) << 16) | ((b3 & 0xFF) << 8) | (b4 & 0xFF);
}

// RandomAccessData.getLong()
private static long getLong(final byte[] arr, final int offset) {
final byte b1 = arr[offset];
final byte b2 = arr[offset + 1];
final byte b3 = arr[offset + 2];
final byte b4 = arr[offset + 3];
final byte b5 = arr[offset + 4];
final byte b6 = arr[offset + 5];
final byte b7 = arr[offset + 6];
final byte b8 = arr[offset + 7];
return (((long)b1 << 56) +
((long)(b2 & 255) << 48) +
((long)(b3 & 255) << 40) +
((long)(b4 & 255) << 32) +
((long)(b5 & 255) << 24) +
((b6 & 255) << 16) +
((b7 & 255) << 8) +
(b8 & 255));
}

// Tests that UnsafeUtils.getInt() and RandomAccessData.getInt() produce the same results
@Test
void getIntTest() {
final int SIZE = 1000;
final byte[] src = new byte[SIZE];
for (int i = 0; i < SIZE; i++) {
src[i] = (byte) (i % 111);
}
for (int i = 0; i < SIZE + 1 - Integer.BYTES; i++) {
assertEquals(getInt(src, i), UnsafeUtils.getInt(src, i));
}
}

// Tests that UnsafeUtils.getLong() and RandomAccessData.getLong() produce the same results
@Test
void getLongTest() {
final int SIZE = 1000;
final byte[] src = new byte[SIZE];
for (int i = 0; i < SIZE; i++) {
src[i] = (byte) (i % 111);
}
for (int i = 0; i < SIZE + 1 - Long.BYTES; i++) {
assertEquals(getLong(src, i), UnsafeUtils.getLong(src, i));
}
}

}