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:
Oleksii Kachaiev 2020-12-21 06:34:04 -08:00 committed by GitHub
parent 3e8e52725b
commit ab8c4f22c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 14 deletions

View File

@ -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;
}
/**

View File

@ -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() {
}

View File

@ -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);
}
}