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 <norman_maurer@apple.com>
This commit is contained in:
parent
3e8e52725b
commit
ab8c4f22c6
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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<HpackHeaderField> STATIC_TABLE = Arrays.asList(
|
||||
@ -117,6 +119,8 @@ final class HpackStaticTable {
|
||||
|
||||
private static final CharSequenceMap<Integer> 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() {
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user