Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize HpackStaticTable by using a perfect hash function #12713

Merged
merged 8 commits into from Oct 3, 2022
Expand Up @@ -31,8 +31,8 @@
*/
package io.netty.handler.codec.http2;

import io.netty.handler.codec.UnsupportedValueConverter;
import io.netty.util.AsciiString;
import io.netty.util.internal.PlatformDependent;

import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -117,9 +117,53 @@ private static HpackHeaderField newHeaderField(String name, String value) {
return new HpackHeaderField(AsciiString.cached(name), AsciiString.cached(value));
}

private static final CharSequenceMap<Integer> STATIC_INDEX_BY_NAME = createMap();
// The table size and bit shift are chosen so that each hash bucket contains a single header name.
private static final int HEADER_NAMES_TABLE_SIZE = 1 << 9;

private static final int MAX_SAME_NAME_FIELD_INDEX = maxSameNameFieldIndex();
private static final int HEADER_NAMES_TABLE_SHIFT = PlatformDependent.BIG_ENDIAN_NATIVE_ORDER ? 22 : 18;

// A table mapping header names to their associated indexes.
private static final HeaderNameIndex[] HEADER_NAMES = new HeaderNameIndex[HEADER_NAMES_TABLE_SIZE];
static {
// Iterate through the static table in reverse order to
// save the smallest index for a given name in the table.
for (int index = STATIC_TABLE.size(); index > 0; index--) {
HpackHeaderField entry = getEntry(index);
int bucket = headerNameBucket(entry.name);
HeaderNameIndex tableEntry = HEADER_NAMES[bucket];
if (tableEntry != null && !equalsVariableTime(tableEntry.name, entry.name)) {
// Can happen if AsciiString.hashCode changes
throw new IllegalStateException("Hash bucket collision between " +
tableEntry.name + " and " + entry.name);
}
HEADER_NAMES[bucket] = new HeaderNameIndex(entry.name, index, entry.value.length() == 0);
}
}

// The table size and bit shift are chosen so that each hash bucket contains a single header.
private static final int HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SIZE = 1 << 6;

private static final int HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SHIFT =
PlatformDependent.BIG_ENDIAN_NATIVE_ORDER ? 0 : 6;

// A table mapping headers with non-empty values to their associated indexes.
private static final HeaderIndex[] HEADERS_WITH_NON_EMPTY_VALUES =
new HeaderIndex[HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SIZE];
static {
for (int index = STATIC_TABLE.size(); index > 0; index--) {
HpackHeaderField entry = getEntry(index);
if (entry.value.length() > 0) {
int bucket = headerBucket(entry.value);
HeaderIndex tableEntry = HEADERS_WITH_NON_EMPTY_VALUES[bucket];
if (tableEntry != null) {
// Can happen if AsciiString.hashCode changes
throw new IllegalStateException("Hash bucket collision between " +
tableEntry.value + " and " + entry.value);
}
HEADERS_WITH_NON_EMPTY_VALUES[bucket] = new HeaderIndex(entry.name, entry.value, index);
}
}
}

/**
* The number of header fields in the static table.
Expand All @@ -138,82 +182,74 @@ static HpackHeaderField getEntry(int index) {
* -1 if the header field name is not in the static table.
*/
static int getIndex(CharSequence name) {
Integer index = STATIC_INDEX_BY_NAME.get(name);
if (index == null) {
return NOT_FOUND;
}
return index;
HeaderNameIndex entry = getEntry(name);
return entry == null ? NOT_FOUND : entry.index;
}

/**
* Returns the index value for the given header field in the static table. Returns -1 if the
* header field is not in the static table.
*/
static int getIndexInsensitive(CharSequence name, CharSequence value) {
int index = getIndex(name);
if (index == NOT_FOUND) {
if (value.length() == 0) {
HeaderNameIndex entry = getEntry(name);
return entry == null || !entry.emptyValue ? NOT_FOUND : entry.index;
} else {
int bucket = headerBucket(value);
HeaderIndex header = HEADERS_WITH_NON_EMPTY_VALUES[bucket];
if (header == null) {
return NOT_FOUND;
}
if (equalsVariableTime(header.name, name) && equalsVariableTime(header.value, value)) {
return header.index;
}
return NOT_FOUND;
}
amirhadadi marked this conversation as resolved.
Show resolved Hide resolved
}

// Compare values for the first name match
HpackHeaderField entry = getEntry(index);
if (equalsVariableTime(value, entry.value)) {
return index;
private static HeaderNameIndex getEntry(CharSequence name) {
int bucket = headerNameBucket(name);
HeaderNameIndex entry = HEADER_NAMES[bucket];
if (entry == null) {
return null;
}
return equalsVariableTime(entry.name, name) ? entry : null;
}

// Note this assumes all entries for a given header field are sequential.
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 sequentially
// and INDEX_BY_NAME returns index of the fist position, - it's safe to
// exit immediately.
return NOT_FOUND;
}
if (equalsVariableTime(value, entry.value)) {
return index;
}
index++;
}
private static int headerNameBucket(CharSequence name) {
return bucket(name, HEADER_NAMES_TABLE_SHIFT, HEADER_NAMES_TABLE_SIZE - 1);
}

return NOT_FOUND;
private static int headerBucket(CharSequence value) {
return bucket(value, HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SHIFT, HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SIZE - 1);
}

// create a map CharSequenceMap header name to index value to allow quick lookup
private static CharSequenceMap<Integer> createMap() {
int length = STATIC_TABLE.size();
@SuppressWarnings("unchecked")
CharSequenceMap<Integer> ret = new CharSequenceMap<Integer>(true,
UnsupportedValueConverter.<Integer>instance(), length);
// Iterate through the static table in reverse order to
// save the smallest index for a given name in the map.
for (int index = length; index > 0; index--) {
HpackHeaderField entry = getEntry(index);
CharSequence name = entry.name;
ret.set(name, index);
private static int bucket(CharSequence s, int shift, int mask) {
return (AsciiString.hashCode(s) >> shift) & mask;
}

private static final class HeaderNameIndex {
final CharSequence name;
final int index;
final boolean emptyValue;

HeaderNameIndex(CharSequence name, int index, boolean emptyValue) {
this.name = name;
this.index = index;
this.emptyValue = emptyValue;
}
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. Similar to {@link #getIndexInsensitive(CharSequence, CharSequence)} 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;
}
private static final class HeaderIndex {
final CharSequence name;
final CharSequence value;
final int index;

HeaderIndex(CharSequence name, CharSequence value, int index) {
this.name = name;
this.value = value;
this.index = index;
}
return length;
}

// singleton
Expand Down
@@ -0,0 +1,76 @@
/*
* Copyright 2022 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.util.AsciiString;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class HpackStaticTableTest {

@Test
public void testEmptyHeaderName() {
assertEquals(-1, HpackStaticTable.getIndex(""));
}

@Test
public void testMissingHeaderName() {
assertEquals(-1, HpackStaticTable.getIndex("missing"));
}

@Test
public void testExistingHeaderName() {
assertEquals(6, HpackStaticTable.getIndex(":scheme"));
}

@Test
public void testMissingHeaderNameAndValue() {
assertEquals(-1, HpackStaticTable.getIndexInsensitive("missing", "value"));
}

@Test
public void testMissingHeaderNameButValueExists() {
assertEquals(-1, HpackStaticTable.getIndexInsensitive("missing", "https"));
}

@Test
public void testExistingHeaderNameAndValueFirstMatch() {
assertEquals(6, HpackStaticTable.getIndexInsensitive(":scheme", "http"));
}

@Test
public void testExistingHeaderNameAndValueSecondMatch() {
assertEquals(7, HpackStaticTable.getIndexInsensitive(
AsciiString.cached(":scheme"), AsciiString.cached("https")));
}

@Test
public void testExistingHeaderNameAndEmptyValueMismatch() {
assertEquals(-1, HpackStaticTable.getIndexInsensitive(":scheme", ""));
}

@Test
public void testExistingHeaderNameAndEmptyValueMatch() {
assertEquals(27, HpackStaticTable.getIndexInsensitive("content-language", ""));
}

@Test
public void testExistingHeaderNameButMissingValue() {
assertEquals(-1, HpackStaticTable.getIndexInsensitive(":scheme", "missing"));
}

}
Expand Up @@ -43,6 +43,9 @@ 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 SCHEME = new AsciiString(":scheme".getBytes(CharsetUtil.US_ASCII), false);
private static final CharSequence HTTP = new AsciiString("http".getBytes(CharsetUtil.US_ASCII), false);
private static final CharSequence HTTPS = new AsciiString("https".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);
Expand Down Expand Up @@ -79,6 +82,18 @@ public int lookupNameOnlyMatchBeginTable() {
return HpackStaticTable.getIndexInsensitive(AUTHORITY, AUTHORITY_NETTY);
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public int lookupHttp() {
return HpackStaticTable.getIndexInsensitive(SCHEME, HTTP);
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public int lookupHttps() {
return HpackStaticTable.getIndexInsensitive(SCHEME, HTTPS);
}

@Benchmark
@BenchmarkMode(Mode.AverageTime)
public int lookupNameOnlyMatchEndTable() {
Expand Down