KAFKA-14766: Improve performance of VarInt encoding and decoding (#13312)

Motivation

Reading/writing the protocol buffer varInt32 and varInt64 (also called varLong in our code base) is in the hot path of data plane code in Apache Kafka. We read multiple varInt in a record and in long. Hence, even a minor change in performance could extrapolate to larger performance benefit.

In this PR, we only update varInt32 encoding/decoding.
Changes

This change uses loop unrolling and reduces the amount of repetition of calculations. Based on the empirical results from the benchmark, the code has been modified to pick up the best implementation.
Results

Performance has been evaluated using JMH benchmarks on JDK 17.0.6. Various implementations have been added in the benchmark and benchmarking has been done for different sizes of varints and varlongs. The benchmark for various implementations have been added at ByteUtilsBenchmark.java

Reviewers: Ismael Juma <mlists@juma.me.uk>, Luke Chen <showuon@gmail.com>, Alexandre Dupriez <alexandre.dupriez@gmail.com>
This commit is contained in:
Divij Vaidya 2023-05-05 14:05:20 +02:00 committed by GitHub
parent e34f884031
commit 6bcc497c36
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 782 additions and 75 deletions

View File

@ -18,6 +18,7 @@ package org.apache.kafka.common.utils;
import java.io.DataInput; import java.io.DataInput;
import java.io.DataOutput; import java.io.DataOutput;
import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -25,6 +26,9 @@ import java.nio.ByteBuffer;
/** /**
* This classes exposes low-level methods for reading/writing from byte streams or buffers. * This classes exposes low-level methods for reading/writing from byte streams or buffers.
*
* The implementation of these methods has been tuned for JVM and the empirical calculations could be found
* using ByteUtilsBenchmark.java
*/ */
public final class ByteUtils { public final class ByteUtils {
@ -144,47 +148,84 @@ public final class ByteUtils {
* Read an integer stored in variable-length format using unsigned decoding from * Read an integer stored in variable-length format using unsigned decoding from
* <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a>. * <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a>.
* *
* The implementation is based on Netty's decoding of varint.
* @see <a href="https://github.com/netty/netty/blob/59aa6e635b9996cf21cd946e64353270679adc73/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java#L73">Netty's varint decoding</a>
*
* @param buffer The buffer to read from * @param buffer The buffer to read from
* @return The integer read * @return The integer read
* *
* @throws IllegalArgumentException if variable-length value does not terminate after 5 bytes have been read * @throws IllegalArgumentException if variable-length value does not terminate after 5 bytes have been read
*/ */
public static int readUnsignedVarint(ByteBuffer buffer) { public static int readUnsignedVarint(ByteBuffer buffer) {
int value = 0; byte tmp = buffer.get();
int i = 0; if (tmp >= 0) {
int b; return tmp;
while (((b = buffer.get()) & 0x80) != 0) { } else {
value |= (b & 0x7f) << i; int result = tmp & 127;
i += 7; if ((tmp = buffer.get()) >= 0) {
if (i > 28) result |= tmp << 7;
throw illegalVarintException(value); } else {
result |= (tmp & 127) << 7;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 127) << 14;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 127) << 21;
result |= (tmp = buffer.get()) << 28;
if (tmp < 0) {
throw illegalVarintException(result);
}
}
}
}
return result;
} }
value |= b << i;
return value;
} }
/** /**
* Read an integer stored in variable-length format using unsigned decoding from * Read an integer stored in variable-length format using unsigned decoding from
* <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a>. * <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a>.
* *
* The implementation is based on Netty's decoding of varint.
* @see <a href="https://github.com/netty/netty/blob/59aa6e635b9996cf21cd946e64353270679adc73/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java#L73">Netty's varint decoding</a>
*
* @param in The input to read from * @param in The input to read from
* @return The integer read * @return The integer read
* *
* @throws IllegalArgumentException if variable-length value does not terminate after 5 bytes have been read * @throws IllegalArgumentException if variable-length value does not terminate after 5 bytes have been read
* @throws IOException if {@link DataInput} throws {@link IOException} * @throws IOException if {@link DataInput} throws {@link IOException}
* @throws EOFException if {@link DataInput} throws {@link EOFException}
*/ */
public static int readUnsignedVarint(DataInput in) throws IOException { static int readUnsignedVarint(DataInput in) throws IOException {
int value = 0; byte tmp = in.readByte();
int i = 0; if (tmp >= 0) {
int b; return tmp;
while (((b = in.readByte()) & 0x80) != 0) { } else {
value |= (b & 0x7f) << i; int result = tmp & 127;
i += 7; if ((tmp = in.readByte()) >= 0) {
if (i > 28) result |= tmp << 7;
throw illegalVarintException(value); } else {
result |= (tmp & 127) << 7;
if ((tmp = in.readByte()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 127) << 14;
if ((tmp = in.readByte()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 127) << 21;
result |= (tmp = in.readByte()) << 28;
if (tmp < 0) {
throw illegalVarintException(result);
}
}
}
}
return result;
} }
value |= b << i;
return value;
} }
/** /**
@ -250,6 +291,12 @@ public final class ByteUtils {
* @throws IllegalArgumentException if variable-length value does not terminate after 10 bytes have been read * @throws IllegalArgumentException if variable-length value does not terminate after 10 bytes have been read
*/ */
public static long readVarlong(ByteBuffer buffer) { public static long readVarlong(ByteBuffer buffer) {
long raw = readUnsignedVarlong(buffer);
return (raw >>> 1) ^ -(raw & 1);
}
// visible for testing
static long readUnsignedVarlong(ByteBuffer buffer) {
long value = 0L; long value = 0L;
int i = 0; int i = 0;
long b; long b;
@ -260,7 +307,7 @@ public final class ByteUtils {
throw illegalVarlongException(value); throw illegalVarlongException(value);
} }
value |= b << i; value |= b << i;
return (value >>> 1) ^ -(value & 1); return value;
} }
/** /**
@ -288,33 +335,68 @@ public final class ByteUtils {
* <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a> * <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a>
* into the buffer. * into the buffer.
* *
* Implementation copied from https://github.com/astei/varint-writing-showdown/tree/dev (MIT License)
* @see <a href="https://github.com/astei/varint-writing-showdown/blob/6b1a4baec4b1f0ce65fa40cf0b282ec775fdf43e/src/jmh/java/me/steinborn/varintshowdown/res/SmartNoDataDependencyUnrolledVarIntWriter.java#L8"> Sample implementation </a>
*
* @param value The value to write * @param value The value to write
* @param buffer The output to write to * @param buffer The output to write to
*/ */
public static void writeUnsignedVarint(int value, ByteBuffer buffer) { public static void writeUnsignedVarint(int value, ByteBuffer buffer) {
while ((value & 0xffffff80) != 0L) { if ((value & (0xFFFFFFFF << 7)) == 0) {
byte b = (byte) ((value & 0x7f) | 0x80); buffer.put((byte) value);
buffer.put(b); } else {
value >>>= 7; buffer.put((byte) (value & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 14)) == 0) {
buffer.put((byte) ((value >>> 7) & 0xFF));
} else {
buffer.put((byte) ((value >>> 7) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 21)) == 0) {
buffer.put((byte) ((value >>> 14) & 0xFF));
} else {
buffer.put((byte) ((value >>> 14) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 28)) == 0) {
buffer.put((byte) ((value >>> 21) & 0xFF));
} else {
buffer.put((byte) ((value >>> 21) & 0x7F | 0x80));
buffer.put((byte) ((value >>> 28) & 0xFF));
}
}
}
} }
buffer.put((byte) value);
} }
/** /**
* Write the given integer following the variable-length unsigned encoding from * Write the given integer following the variable-length unsigned encoding from
* <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a> * <a href="http://code.google.com/apis/protocolbuffers/docs/encoding.html"> Google Protocol Buffers</a>
* into the buffer. * into the buffer.
*
* For implementation notes, see {@link #writeUnsignedVarint(int, ByteBuffer)}
* *
* @param value The value to write * @param value The value to write
* @param out The output to write to * @param out The output to write to
*/ */
public static void writeUnsignedVarint(int value, DataOutput out) throws IOException { public static void writeUnsignedVarint(int value, DataOutput out) throws IOException {
while ((value & 0xffffff80) != 0L) { if ((value & (0xFFFFFFFF << 7)) == 0) {
byte b = (byte) ((value & 0x7f) | 0x80); out.writeByte(value);
out.writeByte(b); } else {
value >>>= 7; out.writeByte(value & 0x7F | 0x80);
if ((value & (0xFFFFFFFF << 14)) == 0) {
out.writeByte(value >>> 7);
} else {
out.writeByte((value >>> 7) & 0x7F | 0x80);
if ((value & (0xFFFFFFFF << 21)) == 0) {
out.writeByte(value >>> 14);
} else {
out.writeByte((byte) ((value >>> 14) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 28)) == 0) {
out.writeByte(value >>> 21);
} else {
out.writeByte((value >>> 21) & 0x7F | 0x80);
out.writeByte(value >>> 28);
}
}
}
} }
out.writeByte((byte) value);
} }
/** /**
@ -368,6 +450,11 @@ public final class ByteUtils {
*/ */
public static void writeVarlong(long value, ByteBuffer buffer) { public static void writeVarlong(long value, ByteBuffer buffer) {
long v = (value << 1) ^ (value >> 63); long v = (value << 1) ^ (value >> 63);
writeUnsignedVarlong(v, buffer);
}
// visible for testing and benchmarking
public static void writeUnsignedVarlong(long v, ByteBuffer buffer) {
while ((v & 0xffffffffffffff80L) != 0L) { while ((v & 0xffffffffffffff80L) != 0L) {
byte b = (byte) ((v & 0x7f) | 0x80); byte b = (byte) ((v & 0x7f) | 0x80);
buffer.put(b); buffer.put(b);
@ -437,8 +524,11 @@ public final class ByteUtils {
* @see #sizeOfUnsignedVarint(int) * @see #sizeOfUnsignedVarint(int)
*/ */
public static int sizeOfVarlong(long value) { public static int sizeOfVarlong(long value) {
long v = (value << 1) ^ (value >> 63); return sizeOfUnsignedVarlong((value << 1) ^ (value >> 63));
}
// visible for benchmarking
public static int sizeOfUnsignedVarlong(long v) {
// For implementation notes @see #sizeOfUnsignedVarint(int) // For implementation notes @see #sizeOfUnsignedVarint(int)
// Similar logic is applied to allow for 64bit input -> 1-9byte output. // Similar logic is applied to allow for 64bit input -> 1-9byte output.
// return (70 - leadingZeros) / 7 + leadingZeros / 64; // return (70 - leadingZeros) / 7 + leadingZeros / 64;

View File

@ -16,6 +16,7 @@
*/ */
package org.apache.kafka.common.utils; package org.apache.kafka.common.utils;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@ -24,6 +25,7 @@ import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.function.Function;
import java.util.function.IntFunction; import java.util.function.IntFunction;
import java.util.function.LongFunction; import java.util.function.LongFunction;
@ -32,6 +34,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
public class ByteUtilsTest { public class ByteUtilsTest {
private static final int MAX_LENGTH_VARINT = 5;
private static final int MAX_LENGTH_VARLONG = 10;
private final byte x00 = 0x00; private final byte x00 = 0x00;
private final byte x01 = 0x01; private final byte x01 = 0x01;
private final byte x02 = 0x02; private final byte x02 = 0x02;
@ -241,6 +245,121 @@ public class ByteUtilsTest {
assertDoubleSerde(Double.NEGATIVE_INFINITY, 0xFFF0000000000000L); assertDoubleSerde(Double.NEGATIVE_INFINITY, 0xFFF0000000000000L);
} }
@Test
@Disabled // Enable this when we change the implementation of UnsignedVarlong
public void testCorrectnessWriteUnsignedVarlong() {
// The old well-known implementation for writeVarlong.
LongFunction<ByteBuffer> simpleImplementation = (long value) -> {
ByteBuffer buffer = ByteBuffer.allocate(MAX_LENGTH_VARLONG);
while ((value & 0xffffffffffffff80L) != 0L) {
byte b = (byte) ((value & 0x7f) | 0x80);
buffer.put(b);
value >>>= 7;
}
buffer.put((byte) value);
return buffer;
};
// compare the full range of values
final ByteBuffer actual = ByteBuffer.allocate(MAX_LENGTH_VARLONG);
for (long i = 1; i < Long.MAX_VALUE && i >= 0; i = i << 1) {
ByteUtils.writeUnsignedVarlong(i, actual);
final ByteBuffer expected = simpleImplementation.apply(i);
assertArrayEquals(expected.array(), actual.array(), "Implementations do not match for number=" + i);
actual.clear();
}
}
@Test
public void testCorrectnessWriteUnsignedVarint() {
// The old well-known implementation for writeUnsignedVarint.
IntFunction<ByteBuffer> simpleImplementation = (int value) -> {
ByteBuffer buffer = ByteBuffer.allocate(MAX_LENGTH_VARINT);
while (true) {
if ((value & ~0x7F) == 0) {
buffer.put((byte) value);
break;
} else {
buffer.put((byte) ((value & 0x7F) | 0x80));
value >>>= 7;
}
}
return buffer;
};
// compare the full range of values
final ByteBuffer actual = ByteBuffer.allocate(MAX_LENGTH_VARINT);
for (int i = 0; i < Integer.MAX_VALUE && i >= 0; i += 13) {
ByteUtils.writeUnsignedVarint(i, actual);
final ByteBuffer expected = simpleImplementation.apply(i);
assertArrayEquals(expected.array(), actual.array(), "Implementations do not match for integer=" + i);
actual.clear();
}
}
@Test
public void testCorrectnessReadUnsignedVarint() {
// The old well-known implementation for readUnsignedVarint
Function<ByteBuffer, Integer> simpleImplementation = (ByteBuffer buffer) -> {
int value = 0;
int i = 0;
int b;
while (((b = buffer.get()) & 0x80) != 0) {
value |= (b & 0x7f) << i;
i += 7;
if (i > 28)
throw new IllegalArgumentException("Invalid varint");
}
value |= b << i;
return value;
};
// compare the full range of values
final ByteBuffer testData = ByteBuffer.allocate(MAX_LENGTH_VARINT);
for (int i = 0; i < Integer.MAX_VALUE && i >= 0; i += 13) {
ByteUtils.writeUnsignedVarint(i, testData);
// prepare buffer for reading
testData.flip();
final int actual = ByteUtils.readUnsignedVarint(testData.duplicate());
final int expected = simpleImplementation.apply(testData);
assertEquals(expected, actual);
testData.clear();
}
}
@Test
@Disabled // Enable this when we change the implementation of UnsignedVarlong
public void testCorrectnessReadUnsignedVarlong() {
// The old well-known implementation for readUnsignedVarlong
Function<ByteBuffer, Long> simpleImplementation = (ByteBuffer buffer) -> {
long value = 0L;
int i = 0;
long b;
while (((b = buffer.get()) & 0x80) != 0) {
value |= (b & 0x7f) << i;
i += 7;
if (i > 63)
throw new IllegalArgumentException();
}
value |= b << i;
return value;
};
// compare the full range of values
final ByteBuffer testData = ByteBuffer.allocate(MAX_LENGTH_VARLONG);
for (long i = 1; i < Long.MAX_VALUE && i >= 0; i = i << 1) {
ByteUtils.writeUnsignedVarlong(i, testData);
// prepare buffer for reading
testData.flip();
final long actual = ByteUtils.readUnsignedVarlong(testData.duplicate());
final long expected = simpleImplementation.apply(testData);
assertEquals(expected, actual);
testData.clear();
}
}
@Test @Test
public void testSizeOfUnsignedVarint() { public void testSizeOfUnsignedVarint() {
// The old well-known implementation for sizeOfUnsignedVarint // The old well-known implementation for sizeOfUnsignedVarint
@ -298,7 +417,7 @@ public class ByteUtilsTest {
} }
private void assertUnsignedVarintSerde(int value, byte[] expectedEncoding) throws IOException { private void assertUnsignedVarintSerde(int value, byte[] expectedEncoding) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(32); ByteBuffer buf = ByteBuffer.allocate(MAX_LENGTH_VARINT);
ByteUtils.writeUnsignedVarint(value, buf); ByteUtils.writeUnsignedVarint(value, buf);
buf.flip(); buf.flip();
assertArrayEquals(expectedEncoding, Utils.toArray(buf)); assertArrayEquals(expectedEncoding, Utils.toArray(buf));
@ -314,7 +433,7 @@ public class ByteUtilsTest {
} }
private void assertVarintSerde(int value, byte[] expectedEncoding) throws IOException { private void assertVarintSerde(int value, byte[] expectedEncoding) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(32); ByteBuffer buf = ByteBuffer.allocate(MAX_LENGTH_VARINT);
ByteUtils.writeVarint(value, buf); ByteUtils.writeVarint(value, buf);
buf.flip(); buf.flip();
assertArrayEquals(expectedEncoding, Utils.toArray(buf)); assertArrayEquals(expectedEncoding, Utils.toArray(buf));
@ -330,7 +449,7 @@ public class ByteUtilsTest {
} }
private void assertVarlongSerde(long value, byte[] expectedEncoding) throws IOException { private void assertVarlongSerde(long value, byte[] expectedEncoding) throws IOException {
ByteBuffer buf = ByteBuffer.allocate(32); ByteBuffer buf = ByteBuffer.allocate(MAX_LENGTH_VARLONG);
ByteUtils.writeVarlong(value, buf); ByteUtils.writeVarlong(value, buf);
buf.flip(); buf.flip();
assertEquals(value, ByteUtils.readVarlong(buf.duplicate())); assertEquals(value, ByteUtils.readVarlong(buf.duplicate()));

View File

@ -523,4 +523,11 @@ For a detailed description of spotbugs bug categories, see https://spotbugs.read
<Bug pattern="NM_SAME_SIMPLE_NAME_AS_SUPERCLASS"/> <Bug pattern="NM_SAME_SIMPLE_NAME_AS_SUPERCLASS"/>
</Match> </Match>
<Match>
<Class name="org.apache.kafka.jmh.util.ByteUtilsBenchmark$BaseBenchmarkState" />
<!-- Suppress warning because JMH code is used to instantiate Random() once per Trial. This is done to ensure
that all benchmarks run with same seed for Random. -->
<Bug pattern="DMI_RANDOM_USED_ONLY_ONCE"/>
</Match>
</FindBugsFilter> </FindBugsFilter>

View File

@ -17,68 +17,270 @@
package org.apache.kafka.jmh.util; package org.apache.kafka.jmh.util;
import java.util.concurrent.ThreadLocalRandom; import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.utils.ByteUtils; import org.apache.kafka.common.utils.ByteUtils;
import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.CompilerControl;
import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder; import org.openjdk.jmh.runner.options.OptionsBuilder;
@State(Scope.Benchmark) @OutputTimeUnit(TimeUnit.SECONDS)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Fork(3) @Fork(3)
@Warmup(iterations = 5, time = 1) @Warmup(iterations = 3, time = 1)
@Measurement(iterations = 10, time = 1) @Measurement(iterations = 5, time = 1)
/**
* This benchmark calculates the empirical evidence of different implementation for encoding/decoding a protobuf
* <a href="https://protobuf.dev/programming-guides/encoding/#varints">VarInt</a> and VarLong.
*
* The benchmark uses JMH and calculates results for different sizes of variable length integer. We expect most of the
* usage in Kafka code base to be 1 or 2 byte integers.
*/
public class ByteUtilsBenchmark { public class ByteUtilsBenchmark {
private int inputInt; private static final int DATA_SET_SAMPLE_SIZE = 16384;
private long inputLong;
@Setup(Level.Iteration)
public void setUp() {
inputInt = ThreadLocalRandom.current().nextInt();
inputLong = ThreadLocalRandom.current().nextLong();
}
@Benchmark @State(Scope.Benchmark)
public int testSizeOfUnsignedVarint() { public static class BaseBenchmarkState {
return ByteUtils.sizeOfUnsignedVarint(inputInt); private ByteBuffer testBuffer;
} private SecureRandom random;
@Setup(Level.Trial)
@Benchmark public void setUpBenchmarkLevel() {
public int testSizeOfUnsignedVarintSimple() { // Initialize the random number generator with a seed so that for each benchmark it produces the same sequence
int value = inputInt; // of random numbers. Note that it is important to initialize it again with the seed before every benchmark.
int bytes = 1; random = new SecureRandom();
while ((value & 0xffffff80) != 0L) { random.setSeed(133713371337L);
bytes += 1;
value >>>= 7;
} }
return bytes;
}
@Benchmark @Setup(Level.Invocation)
public int testSizeOfVarlong() { public void setUpInvocationBuffer() {
return ByteUtils.sizeOfVarlong(inputLong); testBuffer = ByteBuffer.allocate(10);
} }
@Benchmark /**
public int testSizeOfVarlongSimple() { * Generates a random int64 number which occupies exactly bytesSet in the variable length encoding for int64
long v = (inputLong << 1) ^ (inputLong >> 63); *
int bytes = 1; * Upper bound is set by finding the largest number that can be represented by N bits. This number is found
while ((v & 0xffffffffffffff80L) != 0L) { * by bit shifting by N and subtracting decimal 1. For example, for 2 bytes = 16 bits, we calculate the
bytes += 1; * upper bound by:
v >>>= 7; * 1. 0001 0000 0000 0000 0000 // bit shift by 16 = 65536 decimal
* 2. Subtract 1 from 65536 = 65535
* 3. 65535 is the upper bound which is represented in binary as 1111 1111 1111 1111 i.e. largest number
* that could be represented by 2 bytes.
*
* Hence, range of random number of different byte length is:
* 1 byte - [1, 255)
* 2 byte - [256, 65535)
* ... and so on.
*/
long generateRandomLongWithExactBytesSet(int bytesSet) {
long lowerBound = 1L << ((bytesSet - 1) * 8);
long upperBound = (1L << (bytesSet * 8)) - 1;
if (lowerBound >= upperBound) {
throw new IllegalArgumentException();
}
return lowerBound +
random.longs(lowerBound, upperBound).findFirst()
.orElseThrow(() -> new IllegalStateException("Unable to create a random long in the range=[" + lowerBound + ", " + upperBound + "]"));
}
/**
* Generates a random int32 number which occupies exactly bytesSet in the variable length encoding for int32
*
* @see {@link #generateRandomLongWithExactBytesSet(int)} for implementation details.
*
*/
int generateRandomIntWithExactBytesSet(int bytesSet) {
int lowerBound = 1 << ((bytesSet - 1) * 8);
int upperBound = (1 << (bytesSet * 8)) - 1;
if (lowerBound >= upperBound) {
throw new IllegalArgumentException();
}
return lowerBound + random.nextInt(upperBound - lowerBound);
}
public ByteBuffer getTestBuffer() {
return testBuffer;
}
}
@State(Scope.Benchmark)
public static class IterationStateForLong extends BaseBenchmarkState {
@Param({"1", "3", "5", "7"})
int numNonZeroBytes;
private long[] randomLongs;
@Setup(Level.Iteration)
public void setup() {
randomLongs = new long[DATA_SET_SAMPLE_SIZE];
for (int i = 0; i < DATA_SET_SAMPLE_SIZE; i++) {
this.randomLongs[i] = generateRandomLongWithExactBytesSet(numNonZeroBytes);
}
}
public long[] getRandomValues() {
return randomLongs;
}
}
@State(Scope.Benchmark)
public static class IterationStateForInt extends BaseBenchmarkState {
@Param({"1", "2", "3"})
int numNonZeroBytes;
private int[] randomInts;
@Setup(Level.Iteration)
public void setup() {
randomInts = new int[DATA_SET_SAMPLE_SIZE];
for (int i = 0; i < DATA_SET_SAMPLE_SIZE; i++) {
this.randomInts[i] = generateRandomIntWithExactBytesSet(numNonZeroBytes);
}
}
public int[] getRandomValues() {
return randomInts;
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedReadVarintNetty(IterationStateForInt state, Blackhole bk) {
for (int randomValue : state.getRandomValues()) {
ByteUtils.writeUnsignedVarint(randomValue, state.getTestBuffer());
// prepare for reading
state.getTestBuffer().flip();
bk.consume(ByteUtilsBenchmark.readUnsignedVarintNetty(state.getTestBuffer()));
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedReadVarintLegacy(IterationStateForInt state, Blackhole bk) {
for (int randomValue : state.getRandomValues()) {
ByteUtils.writeUnsignedVarint(randomValue, state.getTestBuffer());
// prepare for reading
state.getTestBuffer().flip();
bk.consume(ByteUtilsBenchmark.readUnsignedVarintLegacy(state.getTestBuffer()));
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedReadVarintProtobuf(IterationStateForInt state, Blackhole bk) {
for (int randomValue : state.getRandomValues()) {
ByteUtils.writeUnsignedVarint(randomValue, state.getTestBuffer());
// prepare for reading
state.getTestBuffer().flip();
bk.consume(ByteUtilsBenchmark.readUnsignedVarintProtoBuf(state.getTestBuffer()));
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedReadVarlongUnrolled(IterationStateForLong state, Blackhole bk) throws IOException {
for (long randomValue : state.getRandomValues()) {
ByteUtils.writeUnsignedVarlong(randomValue, state.getTestBuffer());
// prepare for reading
state.getTestBuffer().flip();
bk.consume(ByteUtilsBenchmark.readUnsignedVarlongNetty(state.getTestBuffer()));
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedReadVarlongLegacy(IterationStateForLong state, Blackhole bk) {
for (long randomValue : state.getRandomValues()) {
ByteUtils.writeUnsignedVarlong(randomValue, state.getTestBuffer());
// prepare for reading
state.getTestBuffer().flip();
bk.consume(ByteUtilsBenchmark.readUnsignedVarlongLegacy(state.getTestBuffer()));
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedWriteVarintUnrolled(IterationStateForInt state) {
for (int randomValue : state.getRandomValues()) {
ByteUtilsBenchmark.writeUnsignedVarintUnrolled(randomValue, state.getTestBuffer());
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedWriteVarintLegacy(IterationStateForInt state) {
for (int randomValue : state.getRandomValues()) {
ByteUtilsBenchmark.writeUnsignedVarintLegacy(randomValue, state.getTestBuffer());
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedWriteVarlongUnrolled(IterationStateForLong state) {
for (long randomValue : state.getRandomValues()) {
ByteUtilsBenchmark.writeUnsignedVarlongUnrolled(randomValue, state.getTestBuffer());
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testUnsignedWriteVarlongLegacy(IterationStateForLong state) {
for (long randomValue : state.getRandomValues()) {
ByteUtilsBenchmark.writeUnsignedVarlongLegacy(randomValue, state.getTestBuffer());
state.getTestBuffer().clear();
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testSizeOfUnsignedVarint(IterationStateForInt state, Blackhole bk) {
for (int randomValue : state.getRandomValues()) {
bk.consume(ByteUtils.sizeOfUnsignedVarint(randomValue));
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testSizeOfUnsignedVarintSimple(IterationStateForInt state, Blackhole bk) {
for (int randomValue : state.getRandomValues()) {
int value = randomValue;
int bytes = 1;
while ((value & 0xffffff80) != 0L) {
bytes += 1;
value >>>= 7;
}
bk.consume(bytes);
}
}
@Benchmark
@CompilerControl(CompilerControl.Mode.DONT_INLINE)
public void testSizeOfUnsignedVarlong(IterationStateForLong state, Blackhole bk) {
for (long randomValue : state.getRandomValues()) {
bk.consume(ByteUtils.sizeOfUnsignedVarlong(randomValue));
} }
return bytes;
} }
public static void main(String[] args) throws RunnerException { public static void main(String[] args) throws RunnerException {
@ -89,4 +291,293 @@ public class ByteUtilsBenchmark {
new Runner(opt).run(); new Runner(opt).run();
} }
/******************* Implementations **********************/
/*
* Implementation in Trunk as of Apr 2023 / v3.4
*/
private static int readUnsignedVarintLegacy(ByteBuffer buffer) {
int value = 0;
int i = 0;
int b;
while (((b = buffer.get()) & 0x80) != 0) {
value |= (b & 0x7f) << i;
i += 7;
if (i > 28)
throw new IllegalArgumentException();
}
value |= b << i;
return value;
}
/*
* Implementation in Trunk as of Apr 2023 / v3.4
*/
private static long readUnsignedVarlongLegacy(ByteBuffer buffer) {
long value = 0L;
int i = 0;
long b;
while (((b = buffer.get()) & 0x80) != 0) {
value |= (b & 0x7f) << i;
i += 7;
if (i > 63)
throw new IllegalArgumentException();
}
value |= b << i;
return value;
}
/**
* Implementation copied from Protobuf's implementation.
* see: https://github.com/protocolbuffers/protobuf/blob/f1c7820c9bd0e31f8b7d091092851441ad2716b6/java/core/src/main/java/com/google/protobuf/CodedInputStream.java#L1048
*/
private static int readUnsignedVarintProtoBuf(ByteBuffer buf) {
fastpath:
{
int tempPos = buf.position();
int limit = buf.limit();
if (limit == tempPos) {
break fastpath;
}
final byte[] buffer = buf.array();
int x;
if ((x = buffer[tempPos++]) >= 0) {
buf.position(tempPos);
return x;
} else if (limit - tempPos < 9) {
break fastpath;
} else if ((x ^= buffer[tempPos++] << 7) < 0) {
x ^= ~0 << 7;
} else if ((x ^= buffer[tempPos++] << 14) >= 0) {
x ^= (~0 << 7) ^ (~0 << 14);
} else if ((x ^= buffer[tempPos++] << 21) < 0) {
x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21);
} else {
int y = buffer[tempPos++];
x ^= y << 28;
x ^= (~0 << 7) ^ (~0 << 14) ^ (~0 << 21) ^ (~0 << 28);
if (y < 0
&& buffer[tempPos++] < 0
&& buffer[tempPos++] < 0
&& buffer[tempPos++] < 0
&& buffer[tempPos++] < 0
&& buffer[tempPos++] < 0) {
break fastpath; // Will throw malformedVarint()
}
}
buf.position(tempPos);
return x;
}
return readUnsignedVarintLegacy(buf);
}
/**
* Implementation copied from Netty
* see: https://github.com/netty/netty/blob/59aa6e635b9996cf21cd946e64353270679adc73/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java#L73
*/
private static int readUnsignedVarintNetty(ByteBuffer buffer) {
byte tmp = buffer.get();
if (tmp >= 0) {
return tmp;
} else {
int result = tmp & 127;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 127) << 7;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 127) << 14;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 127) << 21;
result |= (tmp = buffer.get()) << 28;
if (tmp < 0) {
throw new IllegalArgumentException();
}
}
}
}
return result;
}
}
/**
* Implementation extended from Int implementation from Netty
* see: https://github.com/netty/netty/blob/59aa6e635b9996cf21cd946e64353270679adc73/codec/src/main/java/io/netty/handler/codec/protobuf/ProtobufVarint32FrameDecoder.java#L73
*/
private static long readUnsignedVarlongNetty(ByteBuffer buffer) {
byte tmp = buffer.get();
if (tmp >= 0) {
return tmp;
} else {
long result = tmp & 0x7f;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 7;
} else {
result |= (tmp & 0x7f) << 7;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 14;
} else {
result |= (tmp & 0x7f) << 14;
if ((tmp = buffer.get()) >= 0) {
result |= tmp << 21;
} else {
result |= (tmp & 0x7f) << 21;
if ((tmp = buffer.get()) >= 0) {
result |= (long) tmp << 28;
} else {
result |= (long) (tmp & 0x7f) << 28;
result = innerUnsignedReadVarLong(buffer, result);
}
}
}
}
return result;
}
}
private static long innerUnsignedReadVarLong(ByteBuffer buffer, long result) {
byte tmp;
if ((tmp = buffer.get()) >= 0) {
result |= (long) tmp << 35;
} else {
result |= (long) (tmp & 0x7f) << 35;
if ((tmp = buffer.get()) >= 0) {
result |= (long) tmp << 42;
} else {
result |= (long) (tmp & 0x7f) << 42;
if ((tmp = buffer.get()) >= 0) {
result |= (long) tmp << 49;
} else {
result |= (long) (tmp & 0x7f) << 49;
if ((tmp = buffer.get()) >= 0) {
result |= (long) tmp << 56;
} else {
result |= (long) (tmp & 0x7f) << 56;
result |= (long) (tmp = buffer.get()) << 63;
if (tmp < 0) {
throw new IllegalArgumentException();
}
}
}
}
}
return result;
}
/*
* Implementation in Trunk as of Apr 2023 / v3.4
*/
private static void writeUnsignedVarintLegacy(int value, ByteBuffer buffer) {
while ((value & 0xffffff80) != 0L) {
byte b = (byte) ((value & 0x7f) | 0x80);
buffer.put(b);
value >>>= 7;
}
buffer.put((byte) value);
}
/*
* Implementation in Trunk as of Apr 2023 / v3.4
*/
private static void writeUnsignedVarlongLegacy(long v, ByteBuffer buffer) {
while ((v & 0xffffffffffffff80L) != 0L) {
byte b = (byte) ((v & 0x7f) | 0x80);
buffer.put(b);
v >>>= 7;
}
buffer.put((byte) v);
}
/*
* Implementation extended for Long from the Int implementation at https://github.com/astei/varint-writing-showdown/tree/dev (MIT License)
* see: https://github.com/astei/varint-writing-showdown/blob/6b1a4baec4b1f0ce65fa40cf0b282ec775fdf43e/src/jmh/java/me/steinborn/varintshowdown/res/SmartNoDataDependencyUnrolledVarIntWriter.java#L8
*/
private static void writeUnsignedVarlongUnrolled(long value, ByteBuffer buffer) {
if ((value & (0xFFFFFFFF << 7)) == 0) {
buffer.put((byte) value);
} else {
buffer.put((byte) (value & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 14)) == 0) {
buffer.put((byte) (value >>> 7));
} else {
buffer.put((byte) ((value >>> 7) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 21)) == 0) {
buffer.put((byte) (value >>> 14));
} else {
buffer.put((byte) ((value >>> 14) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 28)) == 0) {
buffer.put((byte) (value >>> 21));
} else {
buffer.put((byte) ((value >>> 21) & 0x7F | 0x80));
innerWriteUnsignedVarlongUnrolled(value, buffer);
}
}
}
}
}
private static void innerWriteUnsignedVarlongUnrolled(long value, ByteBuffer buffer) {
if ((value & (0xFFFFFFFFFFFFFFFFL << 35)) == 0) {
buffer.put((byte) (value >>> 28));
} else {
buffer.put((byte) ((value >>> 28) & 0x7FL | 0x80));
if ((value & (0xFFFFFFFFFFFFFFFFL << 42)) == 0) {
buffer.put((byte) (value >>> 35));
} else {
buffer.put((byte) ((value >>> 35) & 0x7FL | 0x80));
if ((value & (0xFFFFFFFFFFFFFFFFL << 49)) == 0) {
buffer.put((byte) (value >>> 42));
} else {
buffer.put((byte) ((value >>> 42) & 0x7FL | 0x80));
if ((value & (0xFFFFFFFFFFFFFFFFL << 56)) == 0) {
buffer.put((byte) (value >>> 49));
} else {
buffer.put((byte) ((value >>> 49) & 0x7FL | 0x80));
if ((value & (0xFFFFFFFFFFFFFFFFL << 63)) == 0) {
buffer.put((byte) (value >>> 56));
} else {
buffer.put((byte) ((value >>> 56) & 0x7FL | 0x80));
buffer.put((byte) (value >>> 63));
}
}
}
}
}
}
/*
* Implementation copied from https://github.com/astei/varint-writing-showdown/tree/dev (MIT License)
* see: https://github.com/astei/varint-writing-showdown/blob/6b1a4baec4b1f0ce65fa40cf0b282ec775fdf43e/src/jmh/java/me/steinborn/varintshowdown/res/SmartNoDataDependencyUnrolledVarIntWriter.java#L8
*/
private static void writeUnsignedVarintUnrolled(int value, ByteBuffer buffer) {
if ((value & (0xFFFFFFFF << 7)) == 0) {
buffer.put((byte) value);
} else {
buffer.put((byte) (value & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 14)) == 0) {
buffer.put((byte) ((value >>> 7) & 0xFF));
} else {
buffer.put((byte) ((value >>> 7) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 21)) == 0) {
buffer.put((byte) ((value >>> 14) & 0xFF));
} else {
buffer.put((byte) ((value >>> 14) & 0x7F | 0x80));
if ((value & (0xFFFFFFFF << 28)) == 0) {
buffer.put((byte) ((value >>> 21) & 0xFF));
} else {
buffer.put((byte) ((value >>> 21) & 0x7F | 0x80));
buffer.put((byte) ((value >>> 28) & 0xFF));
}
}
}
}
}
} }