From ab8c4f22c6bc11b7082fddfc6890fd02f30d5524 Mon Sep 17 00:00:00 2001 From: Oleksii Kachaiev Date: Mon, 21 Dec 2020 06:34:04 -0800 Subject: [PATCH] Improve performance of HPACK static table lookup (#10840) Motivation: HPACK static table is organized in a way that fields with the same name are sequential. Which means when doing sequential scan we can short-circuit scan on name mismatch. Modifications: * `HpackStaticTable.getIndexIndensitive` returns -1 on name mismatch rather than keep scanning. * `HpackStaticTable` statically defined max position in the array where name duplication is possible (after the given index there's no need to check for other fields with the same name) * Benchmark for different lookup patterns Result: Better HPACK static table lookup performance. Co-authored-by: Norman Maurer --- .../handler/codec/http2/HpackEncoder.java | 15 ++-- .../handler/codec/http2/HpackStaticTable.java | 51 +++++++++-- .../http2/HpackStaticTableBenchmark.java | 87 +++++++++++++++++++ 3 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java index 8db29fd7bd..808991f7d3 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackEncoder.java @@ -62,6 +62,7 @@ import static java.lang.Math.min; * are fast, but don't provide any security guarantees. */ final class HpackEncoder { + static final int NOT_FOUND = -1; static final int HUFF_CODE_THRESHOLD = 512; // a linked hash map of header fields private final HeaderEntry[] headerFields; @@ -162,7 +163,7 @@ final class HpackEncoder { // If the peer will only use the static table if (maxHeaderTableSize == 0) { int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value); - if (staticTableIndex == -1) { + if (staticTableIndex == HpackStaticTable.NOT_FOUND) { int nameIndex = HpackStaticTable.getIndex(name); encodeLiteral(out, name, value, IndexType.NONE, nameIndex); } else { @@ -185,7 +186,7 @@ final class HpackEncoder { encodeInteger(out, 0x80, 7, index); } else { int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value); - if (staticTableIndex != -1) { + if (staticTableIndex != HpackStaticTable.NOT_FOUND) { // Section 6.1. Indexed Header Field Representation encodeInteger(out, 0x80, 7, staticTableIndex); } else { @@ -285,7 +286,7 @@ final class HpackEncoder { */ private void encodeLiteral(ByteBuf out, CharSequence name, CharSequence value, IndexType indexType, int nameIndex) { - boolean nameIndexValid = nameIndex != -1; + boolean nameIndexValid = nameIndex != NOT_FOUND; switch (indexType) { case INCREMENTAL: encodeInteger(out, 0x40, 6, nameIndexValid ? nameIndex : 0); @@ -307,7 +308,7 @@ final class HpackEncoder { private int getNameIndex(CharSequence name) { int index = HpackStaticTable.getIndex(name); - if (index == -1) { + if (index == HpackStaticTable.NOT_FOUND) { index = getIndex(name); if (index >= 0) { index += HpackStaticTable.length; @@ -381,7 +382,7 @@ final class HpackEncoder { */ private int getIndex(CharSequence name) { if (length() == 0 || name == null) { - return -1; + return NOT_FOUND; } int h = AsciiString.hashCode(name); int i = index(h); @@ -390,14 +391,14 @@ final class HpackEncoder { return getIndex(e.index); } } - return -1; + return NOT_FOUND; } /** * Compute the index into the dynamic table given the index in the header entry. */ private int getIndex(int index) { - return index == -1 ? -1 : index - head.before.index + 1; + return index == NOT_FOUND ? NOT_FOUND : index - head.before.index + 1; } /** diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java index c077dbceff..f04c97ad26 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java @@ -41,6 +41,8 @@ import static io.netty.handler.codec.http2.HpackUtil.equalsVariableTime; final class HpackStaticTable { + static final int NOT_FOUND = -1; + // Appendix A: Static Table // https://tools.ietf.org/html/rfc7541#appendix-A private static final List STATIC_TABLE = Arrays.asList( @@ -117,6 +119,8 @@ final class HpackStaticTable { private static final CharSequenceMap STATIC_INDEX_BY_NAME = createMap(); + private static final int MAX_SAME_NAME_FIELD_INDEX = maxSameNameFieldIndex(); + /** * The number of header fields in the static table. */ @@ -136,7 +140,7 @@ final class HpackStaticTable { static int getIndex(CharSequence name) { Integer index = STATIC_INDEX_BY_NAME.get(name); if (index == null) { - return -1; + return NOT_FOUND; } return index; } @@ -147,20 +151,33 @@ final class HpackStaticTable { */ static int getIndexInsensitive(CharSequence name, CharSequence value) { int index = getIndex(name); - if (index == -1) { - return -1; + if (index == NOT_FOUND) { + return NOT_FOUND; + } + + // Compare values for the first name match + HpackHeaderField entry = getEntry(index); + if (equalsVariableTime(value, entry.value)) { + return index; } // Note this assumes all entries for a given header field are sequential. - while (index <= length) { - HpackHeaderField entry = getEntry(index); - if (equalsVariableTime(name, entry.name) && equalsVariableTime(value, entry.value)) { + index++; + while (index <= MAX_SAME_NAME_FIELD_INDEX) { + entry = getEntry(index); + if (!equalsVariableTime(name, entry.name)) { + // As far as fields with the same name are placed in the table sequentialy + // and INDEX_BY_NAME returns index of the fist position, - it's safe to + // exit immediatly. + return NOT_FOUND; + } + if (equalsVariableTime(value, entry.value)) { return index; } index++; } - return -1; + return NOT_FOUND; } // create a map CharSequenceMap header name to index value to allow quick lookup @@ -179,6 +196,26 @@ final class HpackStaticTable { return ret; } + /** + * Returns the last position in the array that contains multiple + * fields with the same name. Starting from this position, all + * names are unique. Similary to {@link getIndexInsensitive} method + * assumes all entries for a given header field are sequential + */ + private static int maxSameNameFieldIndex() { + final int length = STATIC_TABLE.size(); + HpackHeaderField cursor = getEntry(length); + for (int index = length - 1; index > 0; index--) { + HpackHeaderField entry = getEntry(index); + if (equalsVariableTime(entry.name, cursor.name)) { + return index + 1; + } else { + cursor = entry; + } + } + return length; + } + // singleton private HpackStaticTable() { } diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java new file mode 100644 index 0000000000..e771cfcea8 --- /dev/null +++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java @@ -0,0 +1,87 @@ +/* + * 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: + * + * https://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.http2; + +import io.netty.microbench.util.AbstractMicrobenchmark; +import io.netty.util.CharsetUtil; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; + +import java.util.concurrent.TimeUnit; + +import io.netty.util.AsciiString; + +@Fork(1) +@Threads(1) +@State(Scope.Benchmark) +@Warmup(iterations = 5) +@Measurement(iterations = 5) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class HpackStaticTableBenchmark extends AbstractMicrobenchmark { + + private static final CharSequence X_CONTENT_ENCODING = + new AsciiString("x-content-encoding".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence X_GZIP = new AsciiString("x-gzip".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence STATUS = new AsciiString(":status".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence STATUS_200 = new AsciiString("200".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence STATUS_500 = new AsciiString("500".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence AUTHORITY = + new AsciiString(":authority".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence AUTHORITY_NETTY = + new AsciiString("netty.io".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence USER_AGENT = + new AsciiString("user-agent".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence USER_AGENT_CURL = + new AsciiString("curl/7.64.1".getBytes(CharsetUtil.US_ASCII), false); + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupNoNameMatch() { + return HpackStaticTable.getIndexInsensitive(X_CONTENT_ENCODING, X_GZIP); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupNameAndValueMatchFirst() { + return HpackStaticTable.getIndexInsensitive(STATUS, STATUS_200); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupNameAndValueMatchLast() { + return HpackStaticTable.getIndexInsensitive(STATUS, STATUS_500); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupNameOnlyMatchBeginTable() { + return HpackStaticTable.getIndexInsensitive(AUTHORITY, AUTHORITY_NETTY); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupNameOnlyMatchEndTable() { + return HpackStaticTable.getIndexInsensitive(USER_AGENT, USER_AGENT_CURL); + } +}