From 1e4f0e6a09f965a64b0b13356244a3f86c1ed3db Mon Sep 17 00:00:00 2001 From: Francesco Nigro Date: Mon, 23 Dec 2019 21:15:56 +0100 Subject: [PATCH] Faster decodeHexNibble (#9896) Motivation: decodeHexNibble can be a lot faster using a lookup table Modifications: decodeHexNibble is made faster by using a lookup table Result: decodeHexNibble is faster --- .../io/netty/util/internal/StringUtil.java | 42 ++++-- .../codec/http/DecodeHexBenchmark.java | 136 ++++++++++++++++++ 2 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java index 9f7ffd8580..60309da89e 100644 --- a/common/src/main/java/io/netty/util/internal/StringUtil.java +++ b/common/src/main/java/io/netty/util/internal/StringUtil.java @@ -17,6 +17,7 @@ package io.netty.util.internal; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -40,6 +41,7 @@ public final class StringUtil { private static final String[] BYTE2HEX_PAD = new String[256]; private static final String[] BYTE2HEX_NOPAD = new String[256]; + private static final byte[] HEX2B; /** * 2 - Quote character at beginning and end. @@ -55,6 +57,33 @@ public final class StringUtil { BYTE2HEX_PAD[i] = i > 0xf ? str : ('0' + str); BYTE2HEX_NOPAD[i] = str; } + // Generate the lookup table that converts an hex char into its decimal value: + // the size of the table is such that the JVM is capable of save any bounds-check + // if a char type is used as an index. + HEX2B = new byte[Character.MAX_VALUE + 1]; + Arrays.fill(HEX2B, (byte) -1); + HEX2B['0'] = (byte) 0; + HEX2B['1'] = (byte) 1; + HEX2B['2'] = (byte) 2; + HEX2B['3'] = (byte) 3; + HEX2B['4'] = (byte) 4; + HEX2B['5'] = (byte) 5; + HEX2B['6'] = (byte) 6; + HEX2B['7'] = (byte) 7; + HEX2B['8'] = (byte) 8; + HEX2B['9'] = (byte) 9; + HEX2B['A'] = (byte) 10; + HEX2B['B'] = (byte) 11; + HEX2B['C'] = (byte) 12; + HEX2B['D'] = (byte) 13; + HEX2B['E'] = (byte) 14; + HEX2B['F'] = (byte) 15; + HEX2B['a'] = (byte) 10; + HEX2B['b'] = (byte) 11; + HEX2B['c'] = (byte) 12; + HEX2B['d'] = (byte) 13; + HEX2B['e'] = (byte) 14; + HEX2B['f'] = (byte) 15; } private StringUtil() { @@ -212,18 +241,11 @@ public final class StringUtil { * given, or {@code -1} if the character is invalid. */ public static int decodeHexNibble(final char c) { + assert HEX2B.length == (Character.MAX_VALUE + 1); // Character.digit() is not used here, as it addresses a larger // set of characters (both ASCII and full-width latin letters). - if (c >= '0' && c <= '9') { - return c - '0'; - } - if (c >= 'A' && c <= 'F') { - return c - ('A' - 0xA); - } - if (c >= 'a' && c <= 'f') { - return c - ('a' - 0xA); - } - return -1; + final int index = c; + return HEX2B[index]; } /** diff --git a/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java new file mode 100644 index 0000000000..61afbacbe0 --- /dev/null +++ b/microbench/src/main/java/io/netty/handler/codec/http/DecodeHexBenchmark.java @@ -0,0 +1,136 @@ +/* + * Copyright 2019 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.handler.codec.http; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.OutputTimeUnit; +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.annotations.Warmup; + +import java.util.Arrays; +import java.util.concurrent.TimeUnit; + +@State(Scope.Benchmark) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class DecodeHexBenchmark extends AbstractMicrobenchmark { + + @Param({ + //with HEX chars + "135aBa9BBCEA030b947d79fCcaf48Bde", + //with HEX chars + 'g' + "4DDeA5gDD1C6fE567E1b6gf0C40FEcDg", + }) + private String hex; + private char[] hexDigits; + + @Setup + public void init() { + hexDigits = hex.toCharArray(); + } + + @Benchmark + public long hexDigits() { + long v = 0; + final char[] hexDigits = this.hexDigits; + for (int i = 0, size = hexDigits.length; i < size; i++) { + v += StringUtil.decodeHexNibble(hexDigits[i]); + } + return v; + } + + @Benchmark + public long hexDigitsWithChecks() { + long v = 0; + final char[] hexDigits = this.hexDigits; + for (int i = 0, size = hexDigits.length; i < size; i++) { + v += decodeHexNibbleWithCheck(hexDigits[i]); + } + return v; + } + + @Benchmark + public long hexDigitsOriginal() { + long v = 0; + final char[] hexDigits = this.hexDigits; + for (int i = 0, size = hexDigits.length; i < size; i++) { + v += decodeHexNibble(hexDigits[i]); + } + return v; + } + + private static int decodeHexNibble(final char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 0xA); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 0xA); + } + return -1; + } + + private static final byte[] HEX2B; + + static { + HEX2B = new byte['f' + 1]; + Arrays.fill(HEX2B, (byte) -1); + HEX2B['0'] = (byte) 0; + HEX2B['1'] = (byte) 1; + HEX2B['2'] = (byte) 2; + HEX2B['3'] = (byte) 3; + HEX2B['4'] = (byte) 4; + HEX2B['5'] = (byte) 5; + HEX2B['6'] = (byte) 6; + HEX2B['7'] = (byte) 7; + HEX2B['8'] = (byte) 8; + HEX2B['9'] = (byte) 9; + HEX2B['A'] = (byte) 10; + HEX2B['B'] = (byte) 11; + HEX2B['C'] = (byte) 12; + HEX2B['D'] = (byte) 13; + HEX2B['E'] = (byte) 14; + HEX2B['F'] = (byte) 15; + HEX2B['a'] = (byte) 10; + HEX2B['b'] = (byte) 11; + HEX2B['c'] = (byte) 12; + HEX2B['d'] = (byte) 13; + HEX2B['e'] = (byte) 14; + HEX2B['f'] = (byte) 15; + } + + private static int decodeHexNibbleWithCheck(final char c) { + final int index = c; + if (index >= HEX2B.length) { + return -1; + } + if (PlatformDependent.hasUnsafe()) { + return PlatformDependent.getByte(HEX2B, index); + } + return HEX2B[index]; + } + +}