Skip to content

Commit

Permalink
Improve predictability of writeUtf8/writeAscii performance (netty#10368)
Browse files Browse the repository at this point in the history
Motivation:

writeUtf8 can suffer from inlining issues and/or megamorphic call-sites on the hot path due to ByteBuf hierarchy

Modifications:

Duplicate and specialize the code paths to reduce the need of polymorphic calls

Result:

Performance are more stable in user code
  • Loading branch information
franz1981 authored Sep 9, 2020
1 parent 5631f1b commit 162e598
Show file tree
Hide file tree
Showing 8 changed files with 2,044 additions and 132 deletions.
2 changes: 1 addition & 1 deletion buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ private int setCharSequence0(int index, CharSequence sequence, Charset charset,
} else {
checkIndex(index, length);
}
return ByteBufUtil.writeUtf8(this, index, sequence, sequence.length());
return ByteBufUtil.writeUtf8(this, index, length, sequence, sequence.length());
}
if (charset.equals(CharsetUtil.US_ASCII) || charset.equals(CharsetUtil.ISO_8859_1)) {
int length = sequence.length();
Expand Down
265 changes: 225 additions & 40 deletions buffer/src/main/java/io/netty/buffer/ByteBufUtil.java

Large diffs are not rendered by default.

197 changes: 112 additions & 85 deletions buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions common/src/main/java/io/netty/util/internal/PlatformDependent.java
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ public Boolean run() {
LINUX_OS_CLASSIFIERS = Collections.unmodifiableSet(availableClassifiers);
}

public static long byteArrayBaseOffset() {
return BYTE_ARRAY_BASE_OFFSET;
}

public static boolean hasDirectBufferNoCleanerConstructor() {
return PlatformDependent0.hasDirectBufferNoCleanerConstructor();
}
Expand Down Expand Up @@ -659,6 +663,10 @@ public static void putByte(byte[] data, int index, byte value) {
PlatformDependent0.putByte(data, index, value);
}

public static void putByte(Object data, long offset, byte value) {
PlatformDependent0.putByte(data, offset, value);
}

public static void putShort(byte[] data, int index, short value) {
PlatformDependent0.putShort(data, index, value);
}
Expand Down Expand Up @@ -687,6 +695,11 @@ public static void copyMemory(byte[] src, int srcIndex, long dstAddr, long lengt
PlatformDependent0.copyMemory(src, BYTE_ARRAY_BASE_OFFSET + srcIndex, null, dstAddr, length);
}

public static void copyMemory(byte[] src, int srcIndex, byte[] dst, int dstIndex, long length) {
PlatformDependent0.copyMemory(src, BYTE_ARRAY_BASE_OFFSET + srcIndex,
dst, BYTE_ARRAY_BASE_OFFSET + dstIndex, length);
}

public static void copyMemory(long srcAddr, byte[] dst, int dstIndex, long length) {
PlatformDependent0.copyMemory(null, srcAddr, dst, BYTE_ARRAY_BASE_OFFSET + dstIndex, length);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,10 @@ static void putByte(byte[] data, int index, byte value) {
UNSAFE.putByte(data, BYTE_ARRAY_BASE_OFFSET + index, value);
}

static void putByte(Object data, long offset, byte value) {
UNSAFE.putByte(data, offset, value);
}

static void putShort(byte[] data, int index, short value) {
UNSAFE.putShort(data, BYTE_ARRAY_BASE_OFFSET + index, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import io.netty.util.CharsetUtil;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
Expand All @@ -34,6 +35,11 @@
@Measurement(iterations = 10)
public class
ByteBufUtilBenchmark extends AbstractMicrobenchmark {

@Param({ "true", "false" })
private boolean direct;
@Param({ "8", "16", "64", "128" })
private int length;
private ByteBuf buffer;
private ByteBuf wrapped;
private ByteBuf asciiBuffer;
Expand All @@ -48,18 +54,19 @@
@Setup
public void setup() {
// Use buffer sizes that will also allow to write UTF-8 without grow the buffer
buffer = Unpooled.directBuffer(512);
wrapped = Unpooled.unreleasableBuffer(Unpooled.directBuffer(512));
asciiSequence = new StringBuilder(128);
for (int i = 0; i < 128; i++) {
final int maxBytes = ByteBufUtil.utf8MaxBytes(length);
buffer = direct? Unpooled.directBuffer(maxBytes) : Unpooled.buffer(maxBytes);
wrapped = Unpooled.unreleasableBuffer(direct? Unpooled.directBuffer(maxBytes) : Unpooled.buffer(maxBytes));
asciiSequence = new StringBuilder(length);
for (int i = 0; i < length; i++) {
asciiSequence.append('a');
}
ascii = asciiSequence.toString();

// Generate some mixed UTF-8 String for benchmark
utf8Sequence = new StringBuilder(128);
utf8Sequence = new StringBuilder(length);
char[] chars = "Some UTF-8 like äÄ∏ŒŒ".toCharArray();
for (int i = 0; i < 128; i++) {
for (int i = 0; i < length; i++) {
utf8Sequence.append(chars[i % chars.length]);
}
utf8 = utf8Sequence.toString();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
/*
* Copyright 2020 The Netty Project
*
* The Netty Project licenses this file to you 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 io.netty.microbench.buffer;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.microbench.util.AbstractMicrobenchmark;
import io.netty.util.AsciiString;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.CompilerControl;
import org.openjdk.jmh.annotations.CompilerControl.Mode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Setup;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

@Fork(value = 2, jvmArgsAppend = "-XX:MaxInlineLevel=9")
public class Utf8EncodingBenchmark extends AbstractMicrobenchmark {
private static class AnotherCharSequence implements CharSequence {
private final char[] chars;

AnotherCharSequence(String chars) {
this.chars = new char[chars.length()];
chars.getChars(0, chars.length(), this.chars, 0);
}

@Override
public int length() {
return chars.length;
}

@Override
public char charAt(int i) {
return chars[i];
}

@Override
public CharSequence subSequence(int start, int end) {
throw new UnsupportedOperationException();
}

@Override
public String toString() {
throw new UnsupportedOperationException();
}
}

// experiment test input
private String[] strings;
private StringBuilder[] stringBuilders;
private AnotherCharSequence[] anotherCharSequences;
private AsciiString[] asciiStrings;
@Param({ "false", "true" })
private boolean direct;
private ByteBuf buffer;
@Param({ "false", "true" })
private boolean noUnsafe;
private int dataSetLength;

@Setup
public void init() {
System.setProperty("io.netty.noUnsafe", Boolean.valueOf(noUnsafe).toString());
InputStream testTextStream = null;
InputStreamReader inStreamReader = null;
BufferedReader buffReader = null;
int maxExpectedSize = 0;
List<String> strings = new ArrayList<String>();
List<StringBuilder> stringBuilders = new ArrayList<StringBuilder>();
List<AnotherCharSequence> anotherCharSequenceList = new ArrayList<AnotherCharSequence>();
List<AsciiString> asciiStrings = new ArrayList<AsciiString>();
try {
testTextStream = getClass().getResourceAsStream("/Utf8Samples.txt");
inStreamReader = new InputStreamReader(testTextStream, "UTF-8");
buffReader = new BufferedReader(inStreamReader);
String line;
while ((line = buffReader.readLine()) != null) {
strings.add(line);
stringBuilders.add(new StringBuilder(line));
anotherCharSequenceList.add(new AnotherCharSequence(line));
asciiStrings.add(new AsciiString(line));
maxExpectedSize = Math.max(maxExpectedSize, ByteBufUtil.utf8MaxBytes(line.length()));
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
closeStream(testTextStream);
closeReader(inStreamReader);
closeReader(buffReader);
}
buffer = direct? Unpooled.directBuffer(maxExpectedSize, maxExpectedSize) :
Unpooled.buffer(maxExpectedSize, maxExpectedSize);
buffer.setByte(maxExpectedSize - 1, 0);
this.strings = strings.toArray(new String[strings.size()]);
this.stringBuilders = stringBuilders.toArray(new StringBuilder[stringBuilders.size()]);
this.anotherCharSequences =
anotherCharSequenceList.toArray(new AnotherCharSequence[anotherCharSequenceList.size()]);
this.asciiStrings = asciiStrings.toArray(new AsciiString[asciiStrings.size()]);
this.dataSetLength = this.strings.length;
}

private static void closeStream(InputStream inStream) {
if (inStream != null) {
try {
inStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

private static void closeReader(Reader buffReader) {
if (buffReader != null) {
try {
buffReader.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int nestedByteBufUtilWriteUtf8String() {
int countBytes = 0;
for (String string : strings) {
countBytes += nestedByteBufUtilWriteUtf8String1(string);
}
return countBytes;
}

private int nestedByteBufUtilWriteUtf8String1(String string) {
return nestedByteBufUtilWriteUtf8String2(string);
}

private int nestedByteBufUtilWriteUtf8String2(String string) {
return nestedByteBufUtilWriteUtf8String3(string);
}

private int nestedByteBufUtilWriteUtf8String3(String string) {
return nestedByteBufUtilWriteUtf8String4(string);
}

private int nestedByteBufUtilWriteUtf8String4(String string) {
return nestedByteBufUtilWriteUtf8String5(string);
}

private int nestedByteBufUtilWriteUtf8String5(String string) {
return nestedByteBufUtilWriteUtf8String6(string);
}

private int nestedByteBufUtilWriteUtf8String6(String string) {
// this calls should be inlined but...what happen to the subsequent calls > MaxInlineLevel?
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, string, 0, string.length());
return buffer.writerIndex();
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int byteBufUtilWriteUtf8String() {
int countBytes = 0;
for (String string : strings) {
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, string, 0, string.length());
countBytes += buffer.writerIndex();
}
return countBytes;
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int byteBufUtilWriteUtf8Bimorphic() {
int countBytes = 0;
for (int i = 0, size = dataSetLength; i < size; i++) {
final StringBuilder stringBuilder = stringBuilders[i];
final String string = strings[i];
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, stringBuilder, 0, stringBuilder.length());
countBytes += buffer.writerIndex();
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, string, 0, string.length());
countBytes += buffer.writerIndex();
}
return countBytes;
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int byteBufUtilWriteUtf8Megamorphic() {
int countBytes = 0;
for (int i = 0, size = dataSetLength; i < size; i++) {
final StringBuilder stringBuilder = stringBuilders[i];
final String string = strings[i];
final AnotherCharSequence anotherCharSequence = anotherCharSequences[i];
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, stringBuilder, 0, stringBuilder.length());
countBytes += buffer.writerIndex();
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, string, 0, string.length());
countBytes += buffer.writerIndex();
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, anotherCharSequence, 0, anotherCharSequence.length());
countBytes += buffer.writerIndex();
}
return countBytes;
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int byteBufUtilWriteUtf8CommonCharSequences() {
int countBytes = 0;
for (int i = 0, size = dataSetLength; i < size; i++) {
final StringBuilder stringBuilder = stringBuilders[i];
final String string = strings[i];
final AsciiString asciiString = asciiStrings[i];
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, stringBuilder, 0, stringBuilder.length());
countBytes += buffer.writerIndex();
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, string, 0, string.length());
countBytes += buffer.writerIndex();
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, asciiString, 0, asciiString.length());
countBytes += buffer.writerIndex();
}
return countBytes;
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int byteBufUtilWriteUtf8AsciiString() {
int countBytes = 0;
for (int i = 0, size = dataSetLength; i < size; i++) {
final AsciiString asciiString = asciiStrings[i];
buffer.resetWriterIndex();
ByteBufUtil.writeUtf8(buffer, asciiString, 0, asciiString.length());
countBytes += buffer.writerIndex();
}
return countBytes;
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public int writeGetBytes() throws UnsupportedEncodingException {
int countBytes = 0;
for (String string : strings) {
buffer.resetWriterIndex();
final byte[] bytes = string.getBytes("UTF-8");
buffer.writeBytes(bytes);
countBytes += buffer.writerIndex();
}
return countBytes;
}

}
Loading

0 comments on commit 162e598

Please sign in to comment.