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.
|
* are fast, but don't provide any security guarantees.
|
||||||
*/
|
*/
|
||||||
final class HpackEncoder {
|
final class HpackEncoder {
|
||||||
|
static final int NOT_FOUND = -1;
|
||||||
static final int HUFF_CODE_THRESHOLD = 512;
|
static final int HUFF_CODE_THRESHOLD = 512;
|
||||||
// a linked hash map of header fields
|
// a linked hash map of header fields
|
||||||
private final HeaderEntry[] headerFields;
|
private final HeaderEntry[] headerFields;
|
||||||
@ -162,7 +163,7 @@ final class HpackEncoder {
|
|||||||
// If the peer will only use the static table
|
// If the peer will only use the static table
|
||||||
if (maxHeaderTableSize == 0) {
|
if (maxHeaderTableSize == 0) {
|
||||||
int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
|
int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
|
||||||
if (staticTableIndex == -1) {
|
if (staticTableIndex == HpackStaticTable.NOT_FOUND) {
|
||||||
int nameIndex = HpackStaticTable.getIndex(name);
|
int nameIndex = HpackStaticTable.getIndex(name);
|
||||||
encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
|
encodeLiteral(out, name, value, IndexType.NONE, nameIndex);
|
||||||
} else {
|
} else {
|
||||||
@ -185,7 +186,7 @@ final class HpackEncoder {
|
|||||||
encodeInteger(out, 0x80, 7, index);
|
encodeInteger(out, 0x80, 7, index);
|
||||||
} else {
|
} else {
|
||||||
int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
|
int staticTableIndex = HpackStaticTable.getIndexInsensitive(name, value);
|
||||||
if (staticTableIndex != -1) {
|
if (staticTableIndex != HpackStaticTable.NOT_FOUND) {
|
||||||
// Section 6.1. Indexed Header Field Representation
|
// Section 6.1. Indexed Header Field Representation
|
||||||
encodeInteger(out, 0x80, 7, staticTableIndex);
|
encodeInteger(out, 0x80, 7, staticTableIndex);
|
||||||
} else {
|
} else {
|
||||||
@ -285,7 +286,7 @@ final class HpackEncoder {
|
|||||||
*/
|
*/
|
||||||
private void encodeLiteral(ByteBuf out, CharSequence name, CharSequence value, IndexType indexType,
|
private void encodeLiteral(ByteBuf out, CharSequence name, CharSequence value, IndexType indexType,
|
||||||
int nameIndex) {
|
int nameIndex) {
|
||||||
boolean nameIndexValid = nameIndex != -1;
|
boolean nameIndexValid = nameIndex != NOT_FOUND;
|
||||||
switch (indexType) {
|
switch (indexType) {
|
||||||
case INCREMENTAL:
|
case INCREMENTAL:
|
||||||
encodeInteger(out, 0x40, 6, nameIndexValid ? nameIndex : 0);
|
encodeInteger(out, 0x40, 6, nameIndexValid ? nameIndex : 0);
|
||||||
@ -307,7 +308,7 @@ final class HpackEncoder {
|
|||||||
|
|
||||||
private int getNameIndex(CharSequence name) {
|
private int getNameIndex(CharSequence name) {
|
||||||
int index = HpackStaticTable.getIndex(name);
|
int index = HpackStaticTable.getIndex(name);
|
||||||
if (index == -1) {
|
if (index == HpackStaticTable.NOT_FOUND) {
|
||||||
index = getIndex(name);
|
index = getIndex(name);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
index += HpackStaticTable.length;
|
index += HpackStaticTable.length;
|
||||||
@ -381,7 +382,7 @@ final class HpackEncoder {
|
|||||||
*/
|
*/
|
||||||
private int getIndex(CharSequence name) {
|
private int getIndex(CharSequence name) {
|
||||||
if (length() == 0 || name == null) {
|
if (length() == 0 || name == null) {
|
||||||
return -1;
|
return NOT_FOUND;
|
||||||
}
|
}
|
||||||
int h = AsciiString.hashCode(name);
|
int h = AsciiString.hashCode(name);
|
||||||
int i = index(h);
|
int i = index(h);
|
||||||
@ -390,14 +391,14 @@ final class HpackEncoder {
|
|||||||
return getIndex(e.index);
|
return getIndex(e.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the index into the dynamic table given the index in the header entry.
|
* Compute the index into the dynamic table given the index in the header entry.
|
||||||
*/
|
*/
|
||||||
private int getIndex(int index) {
|
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 {
|
final class HpackStaticTable {
|
||||||
|
|
||||||
|
static final int NOT_FOUND = -1;
|
||||||
|
|
||||||
// Appendix A: Static Table
|
// Appendix A: Static Table
|
||||||
// https://tools.ietf.org/html/rfc7541#appendix-A
|
// https://tools.ietf.org/html/rfc7541#appendix-A
|
||||||
private static final List<HpackHeaderField> STATIC_TABLE = Arrays.asList(
|
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 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.
|
* The number of header fields in the static table.
|
||||||
*/
|
*/
|
||||||
@ -136,7 +140,7 @@ final class HpackStaticTable {
|
|||||||
static int getIndex(CharSequence name) {
|
static int getIndex(CharSequence name) {
|
||||||
Integer index = STATIC_INDEX_BY_NAME.get(name);
|
Integer index = STATIC_INDEX_BY_NAME.get(name);
|
||||||
if (index == null) {
|
if (index == null) {
|
||||||
return -1;
|
return NOT_FOUND;
|
||||||
}
|
}
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
@ -147,20 +151,33 @@ final class HpackStaticTable {
|
|||||||
*/
|
*/
|
||||||
static int getIndexInsensitive(CharSequence name, CharSequence value) {
|
static int getIndexInsensitive(CharSequence name, CharSequence value) {
|
||||||
int index = getIndex(name);
|
int index = getIndex(name);
|
||||||
if (index == -1) {
|
if (index == NOT_FOUND) {
|
||||||
return -1;
|
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.
|
// Note this assumes all entries for a given header field are sequential.
|
||||||
while (index <= length) {
|
index++;
|
||||||
HpackHeaderField entry = getEntry(index);
|
while (index <= MAX_SAME_NAME_FIELD_INDEX) {
|
||||||
if (equalsVariableTime(name, entry.name) && equalsVariableTime(value, entry.value)) {
|
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;
|
return index;
|
||||||
}
|
}
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1;
|
return NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a map CharSequenceMap header name to index value to allow quick lookup
|
// create a map CharSequenceMap header name to index value to allow quick lookup
|
||||||
@ -179,6 +196,26 @@ final class HpackStaticTable {
|
|||||||
return ret;
|
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
|
// singleton
|
||||||
private HpackStaticTable() {
|
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