From 054af70fed01dd48afb14ff368984ddee47b3310 Mon Sep 17 00:00:00 2001 From: Norman Maurer Date: Tue, 13 Oct 2015 11:34:29 +0200 Subject: [PATCH] Minimize object allocation when calling AbstractByteBuf.toString(..., Charset) Motivation: Calling AbstractByteBuf.toString(..., Charset) is used quite frequently by users but produce a lot of GC. Modification: - Use a FastThreadLocal to store the CharBuffer that are needed for decoding. - Use internalNioBuffer(...) when possible Result: Less object creation / Less GC --- .../java/io/netty/buffer/AbstractByteBuf.java | 15 +----- .../java/io/netty/buffer/ByteBufUtil.java | 49 +++++++++++++++++-- .../java/io/netty/buffer/ByteBufUtilTest.java | 18 +++++++ .../buffer/ByteBufUtilBenchmark.java | 21 +++++++- 4 files changed, 83 insertions(+), 20 deletions(-) diff --git a/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java b/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java index f64b981813..b0ed0ad562 100644 --- a/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java +++ b/buffer/src/main/java/io/netty/buffer/AbstractByteBuf.java @@ -974,20 +974,7 @@ public abstract class AbstractByteBuf extends ByteBuf { @Override public String toString(int index, int length, Charset charset) { - if (length == 0) { - return ""; - } - - ByteBuffer nioBuffer; - if (nioBufferCount() == 1) { - nioBuffer = nioBuffer(index, length); - } else { - nioBuffer = ByteBuffer.allocate(length); - getBytes(index, nioBuffer); - nioBuffer.flip(); - } - - return ByteBufUtil.decodeString(nioBuffer, charset); + return ByteBufUtil.decodeString(this, index, length, charset); } @Override diff --git a/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java b/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java index 08c747dfcf..79b71cd33a 100644 --- a/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java +++ b/buffer/src/main/java/io/netty/buffer/ByteBufUtil.java @@ -18,6 +18,7 @@ package io.netty.buffer; import io.netty.util.CharsetUtil; import io.netty.util.Recycler; import io.netty.util.Recycler.Handle; +import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.internal.ObjectUtil; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; @@ -42,7 +43,14 @@ import java.util.Locale; public final class ByteBufUtil { private static final InternalLogger logger = InternalLoggerFactory.getInstance(ByteBufUtil.class); + private static final FastThreadLocal CHAR_BUFFERS = new FastThreadLocal() { + @Override + protected CharBuffer initialValue() throws Exception { + return CharBuffer.allocate(1024); + } + }; + private static final int MAX_CHAR_BUFFER_SIZE; private static final char[] HEXDUMP_TABLE = new char[256 * 4]; private static final String NEWLINE = StringUtil.NEWLINE; private static final String[] BYTE2HEX = new String[256]; @@ -125,6 +133,9 @@ public final class ByteBufUtil { THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 64 * 1024); logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE); + + MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024); + logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE); } /** @@ -519,10 +530,41 @@ public final class ByteBufUtil { } } - static String decodeString(ByteBuffer src, Charset charset) { + static String decodeString(ByteBuf src, int readerIndex, int len, Charset charset) { + if (len == 0) { + return StringUtil.EMPTY_STRING; + } final CharsetDecoder decoder = CharsetUtil.getDecoder(charset); - final CharBuffer dst = CharBuffer.allocate( - (int) ((double) src.remaining() * decoder.maxCharsPerByte())); + final int maxLength = (int) ((double) len * decoder.maxCharsPerByte()); + CharBuffer dst = CHAR_BUFFERS.get(); + if (dst.length() < maxLength) { + dst = CharBuffer.allocate(maxLength); + if (maxLength <= MAX_CHAR_BUFFER_SIZE) { + CHAR_BUFFERS.set(dst); + } + } else { + dst.clear(); + } + if (src.nioBufferCount() == 1) { + // Use internalNioBuffer(...) to reduce object creation. + decodeString(decoder, src.internalNioBuffer(readerIndex, len), dst); + } else { + // We use a heap buffer as CharsetDecoder is most likely able to use a fast-path if src and dst buffers + // are both backed by a byte array. + ByteBuf buffer = src.alloc().heapBuffer(len); + try { + buffer.writeBytes(src, readerIndex, len); + // Use internalNioBuffer(...) to reduce object creation. + decodeString(decoder, buffer.internalNioBuffer(readerIndex, len), dst); + } finally { + // Release the temporary buffer again. + buffer.release(); + } + } + return dst.flip().toString(); + } + + private static void decodeString(CharsetDecoder decoder, ByteBuffer src, CharBuffer dst) { try { CoderResult cr = decoder.decode(src, dst, true); if (!cr.isUnderflow()) { @@ -535,7 +577,6 @@ public final class ByteBufUtil { } catch (CharacterCodingException x) { throw new IllegalStateException(x); } - return dst.flip().toString(); } /** diff --git a/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java b/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java index 1725fb525a..328431e3b2 100644 --- a/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java +++ b/buffer/src/test/java/io/netty/buffer/ByteBufUtilTest.java @@ -20,6 +20,8 @@ import io.netty.util.ReferenceCountUtil; import org.junit.Assert; import org.junit.Test; +import java.nio.charset.Charset; + public class ByteBufUtilTest { @Test @@ -73,4 +75,20 @@ public class ByteBufUtilTest { private static void assertWrapped(ByteBuf buf) { Assert.assertTrue(buf instanceof WrappedByteBuf); } + + @Test + public void testDecodeUsAscii() { + testDecodeString("This is a test", CharsetUtil.US_ASCII); + } + + @Test + public void testDecodeUtf8() { + testDecodeString("Some UTF-8 like äÄ∏ŒŒ", CharsetUtil.UTF_8); + } + + private static void testDecodeString(String text, Charset charset) { + ByteBuf buffer = Unpooled.copiedBuffer(text, charset); + Assert.assertEquals(text, ByteBufUtil.decodeString(buffer, 0, buffer.readableBytes(), charset)); + buffer.release(); + } } diff --git a/microbench/src/test/java/io/netty/microbench/buffer/ByteBufUtilBenchmark.java b/microbench/src/test/java/io/netty/microbench/buffer/ByteBufUtilBenchmark.java index 060d2ef1fc..12e99cf520 100644 --- a/microbench/src/test/java/io/netty/microbench/buffer/ByteBufUtilBenchmark.java +++ b/microbench/src/test/java/io/netty/microbench/buffer/ByteBufUtilBenchmark.java @@ -30,12 +30,14 @@ import org.openjdk.jmh.annotations.Warmup; @State(Scope.Benchmark) -@Warmup(iterations = 10) -@Measurement(iterations = 25) +@Warmup(iterations = 5) +@Measurement(iterations = 10) public class ByteBufUtilBenchmark extends AbstractMicrobenchmark { private ByteBuf buffer; private ByteBuf wrapped; + private ByteBuf asciiBuffer; + private ByteBuf utf8Buffer; private StringBuilder asciiSequence; private String ascii; @@ -62,12 +64,17 @@ public class } utf8 = utf8Sequence.toString(); asciiSequence = utf8Sequence; + + asciiBuffer = Unpooled.copiedBuffer(ascii, CharsetUtil.US_ASCII); + utf8Buffer = Unpooled.copiedBuffer(utf8, CharsetUtil.UTF_8); } @TearDown public void tearDown() { buffer.release(); wrapped.release(); + asciiBuffer.release(); + utf8Buffer.release(); } @Benchmark @@ -165,4 +172,14 @@ public class wrapped.resetWriterIndex(); ByteBufUtil.writeUtf8(wrapped, utf8Sequence); } + + @Benchmark + public String decodeStringAscii() { + return asciiBuffer.toString(CharsetUtil.US_ASCII); + } + + @Benchmark + public String decodeStringUtf8() { + return utf8Buffer.toString(CharsetUtil.UTF_8); + } }