HttpMethod#valueOf improvement

Motivation:
HttpMethod#valueOf shows up on profiler results in the top set of
results. Since it is a relatively simple operation it can be improved in
isolation.

Modifications:
- Introduce a special case map which assigns each HttpMethod to a unique
index in an array and provides constant time lookup from a hash code
algorithm. When the bucket is matched we can then directly do equality
comparison instead of potentially following a linked structure when
HashMap has hash collisions.

Result:
~10% improvement in benchmark results for HttpMethod#valueOf

Benchmark                                     Mode  Cnt   Score   Error   Units
HttpMethodMapBenchmark.newMapKnownMethods    thrpt   16  31.831 ± 0.928  ops/us
HttpMethodMapBenchmark.newMapMixMethods      thrpt   16  25.568 ± 0.400  ops/us
HttpMethodMapBenchmark.newMapUnknownMethods  thrpt   16  51.413 ± 1.824  ops/us
HttpMethodMapBenchmark.oldMapKnownMethods    thrpt   16  29.226 ± 0.330  ops/us
HttpMethodMapBenchmark.oldMapMixMethods      thrpt   16  21.073 ± 0.247  ops/us
HttpMethodMapBenchmark.oldMapUnknownMethods  thrpt   16  49.081 ± 0.577  ops/us
This commit is contained in:
Scott Mitchell 2017-11-11 22:37:26 -08:00
parent 0a47c590fe
commit 93b144b7b4
2 changed files with 273 additions and 13 deletions

View File

@ -15,11 +15,10 @@
*/
package io.netty.handler.codec.http;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.util.AsciiString;
import java.util.HashMap;
import java.util.Map;
import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
* The request method of HTTP or its derived protocols, such as
@ -86,18 +85,19 @@ public class HttpMethod implements Comparable<HttpMethod> {
*/
public static final HttpMethod CONNECT = new HttpMethod("CONNECT");
private static final Map<String, HttpMethod> methodMap = new HashMap<String, HttpMethod>();
private static final EnumNameMap<HttpMethod> methodMap;
static {
methodMap.put(OPTIONS.toString(), OPTIONS);
methodMap.put(GET.toString(), GET);
methodMap.put(HEAD.toString(), HEAD);
methodMap.put(POST.toString(), POST);
methodMap.put(PUT.toString(), PUT);
methodMap.put(PATCH.toString(), PATCH);
methodMap.put(DELETE.toString(), DELETE);
methodMap.put(TRACE.toString(), TRACE);
methodMap.put(CONNECT.toString(), CONNECT);
methodMap = new EnumNameMap<HttpMethod>(
new EnumNameMap.Node<HttpMethod>(OPTIONS.toString(), OPTIONS),
new EnumNameMap.Node<HttpMethod>(GET.toString(), GET),
new EnumNameMap.Node<HttpMethod>(HEAD.toString(), HEAD),
new EnumNameMap.Node<HttpMethod>(POST.toString(), POST),
new EnumNameMap.Node<HttpMethod>(PUT.toString(), PUT),
new EnumNameMap.Node<HttpMethod>(PATCH.toString(), PATCH),
new EnumNameMap.Node<HttpMethod>(DELETE.toString(), DELETE),
new EnumNameMap.Node<HttpMethod>(TRACE.toString(), TRACE),
new EnumNameMap.Node<HttpMethod>(CONNECT.toString(), CONNECT));
}
/**
@ -173,4 +173,46 @@ public class HttpMethod implements Comparable<HttpMethod> {
public int compareTo(HttpMethod o) {
return name().compareTo(o.name());
}
private static final class EnumNameMap<T> {
private final EnumNameMap.Node<T>[] values;
private final int valuesMask;
EnumNameMap(EnumNameMap.Node<T>... nodes) {
values = (EnumNameMap.Node<T>[]) new EnumNameMap.Node[findNextPositivePowerOfTwo(nodes.length)];
valuesMask = values.length - 1;
for (EnumNameMap.Node<T> node : nodes) {
int i = hashCode(node.key) & valuesMask;
if (values[i] != null) {
throw new IllegalArgumentException("index " + i + " collision between values: [" +
values[i].key + ", " + node.key + ']');
}
values[i] = node;
}
}
T get(String name) {
EnumNameMap.Node<T> node = values[hashCode(name) & valuesMask];
return node == null || !node.key.equals(name) ? null : node.value;
}
private static int hashCode(String name) {
// This hash code needs to produce a unique index in the "values" array for each HttpMethod. If new
// HttpMethods are added this algorithm will need to be adjusted. The constructor will "fail fast" if there
// are duplicates detected.
// For example with the current set of HttpMethods it just so happens that the String hash code value
// shifted right by 6 bits modulo 16 is unique relative to all other HttpMethod values.
return name.hashCode() >>> 6;
}
private static final class Node<T> {
final String key;
final T value;
Node(String key, T value) {
this.key = key;
this.value = value;
}
}
}
}

View File

@ -0,0 +1,218 @@
/*
* Copyright 2017 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:
*
* http://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.http;
import io.netty.microbench.util.AbstractMicrobenchmark;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static io.netty.handler.codec.http.HttpMethod.CONNECT;
import static io.netty.handler.codec.http.HttpMethod.DELETE;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.HEAD;
import static io.netty.handler.codec.http.HttpMethod.OPTIONS;
import static io.netty.handler.codec.http.HttpMethod.PATCH;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpMethod.PUT;
import static io.netty.handler.codec.http.HttpMethod.TRACE;
import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo;
@State(Scope.Benchmark)
@Warmup(iterations = 5)
@Measurement(iterations = 8)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class HttpMethodMapBenchmark extends AbstractMicrobenchmark {
private static final Map<String, HttpMethod> OLD_MAP = new HashMap<String, HttpMethod>();
private static final SimpleStringMap<HttpMethod> NEW_MAP;
private static final String[] KNOWN_METHODS;
private static final String[] MIXED_METHODS;
private static final String[] UNKNOWN_METHODS;
static {
// We intentionally don't use HttpMethod.toString() here to avoid the equals(..) comparison method from being
// able to short circuit due to reference equality checks and being biased toward the new approach. This
// simulates the behavior of HttpObjectDecoder which will build new String objects during the decode operation.
KNOWN_METHODS = new String[] {
"OPTIONS",
"GET",
"HEAD",
"POST",
"PUT",
"PATCH",
"DELETE",
"TRACE",
"CONNECT"
};
MIXED_METHODS = new String[] {
"OPTIONS",
"FAKEMETHOD",
"GET",
"HEAD",
"POST",
"UBERGET",
"PUT",
"PATCH",
"MYMETHOD",
"DELETE",
"TRACE",
"CONNECT",
"WHATMETHOD"
};
UNKNOWN_METHODS = new String[] {
"FAKEMETHOD",
"UBERGET",
"MYMETHOD",
"TESTING",
"WHATMETHOD",
"UNKNOWN",
"FOOBAR"
};
OLD_MAP.put(OPTIONS.toString(), OPTIONS);
OLD_MAP.put(GET.toString(), GET);
OLD_MAP.put(HEAD.toString(), HEAD);
OLD_MAP.put(POST.toString(), POST);
OLD_MAP.put(PUT.toString(), PUT);
OLD_MAP.put(PATCH.toString(), PATCH);
OLD_MAP.put(DELETE.toString(), DELETE);
OLD_MAP.put(TRACE.toString(), TRACE);
OLD_MAP.put(CONNECT.toString(), CONNECT);
NEW_MAP = new SimpleStringMap<HttpMethod>(
new SimpleStringMap.Node<HttpMethod>(OPTIONS.toString(), OPTIONS),
new SimpleStringMap.Node<HttpMethod>(GET.toString(), GET),
new SimpleStringMap.Node<HttpMethod>(HEAD.toString(), HEAD),
new SimpleStringMap.Node<HttpMethod>(POST.toString(), POST),
new SimpleStringMap.Node<HttpMethod>(PUT.toString(), PUT),
new SimpleStringMap.Node<HttpMethod>(PATCH.toString(), PATCH),
new SimpleStringMap.Node<HttpMethod>(DELETE.toString(), DELETE),
new SimpleStringMap.Node<HttpMethod>(TRACE.toString(), TRACE),
new SimpleStringMap.Node<HttpMethod>(CONNECT.toString(), CONNECT));
}
private static final class SimpleStringMap<T> {
private final SimpleStringMap.Node<T>[] values;
private final int valuesMask;
SimpleStringMap(SimpleStringMap.Node<T>... nodes) {
values = (SimpleStringMap.Node<T>[]) new SimpleStringMap.Node[findNextPositivePowerOfTwo(nodes.length)];
valuesMask = values.length - 1;
for (SimpleStringMap.Node<T> node : nodes) {
int i = hashCode(node.key) & valuesMask;
if (values[i] != null) {
throw new IllegalArgumentException("index " + i + " collision between values: [" +
values[i].key + ", " + node.key + "]");
}
values[i] = node;
}
}
T get(String name) {
SimpleStringMap.Node<T> node = values[hashCode(name) & valuesMask];
return node == null || !node.key.equals(name) ? null : node.value;
}
private static int hashCode(String name) {
// This hash code needs to produce a unique index for each HttpMethod. If new methods are added this
// algorithm will need to be adjusted. The goal is to have each enum name's hash value correlate to a unique
// index in the values array.
return name.hashCode() >>> 6;
}
private static final class Node<T> {
final String key;
final T value;
Node(String key, T value) {
this.key = key;
this.value = value;
}
}
}
@Benchmark
public int oldMapKnownMethods() throws Exception {
int x = 0;
for (int i = 0; i < KNOWN_METHODS.length; ++i) {
x += OLD_MAP.get(KNOWN_METHODS[i]).toString().length();
}
return x;
}
@Benchmark
public int newMapKnownMethods() throws Exception {
int x = 0;
for (int i = 0; i < KNOWN_METHODS.length; ++i) {
x += NEW_MAP.get(KNOWN_METHODS[i]).toString().length();
}
return x;
}
@Benchmark
public int oldMapMixMethods() throws Exception {
int x = 0;
for (int i = 0; i < MIXED_METHODS.length; ++i) {
HttpMethod method = OLD_MAP.get(MIXED_METHODS[i]);
if (method != null) {
x += method.toString().length();
}
}
return x;
}
@Benchmark
public int newMapMixMethods() throws Exception {
int x = 0;
for (int i = 0; i < MIXED_METHODS.length; ++i) {
HttpMethod method = NEW_MAP.get(MIXED_METHODS[i]);
if (method != null) {
x += method.toString().length();
}
}
return x;
}
@Benchmark
public int oldMapUnknownMethods() throws Exception {
int x = 0;
for (int i = 0; i < UNKNOWN_METHODS.length; ++i) {
HttpMethod method = OLD_MAP.get(UNKNOWN_METHODS[i]);
if (method != null) {
x += method.toString().length();
}
}
return x;
}
@Benchmark
public int newMapUnknownMethods() throws Exception {
int x = 0;
for (int i = 0; i < UNKNOWN_METHODS.length; ++i) {
HttpMethod method = NEW_MAP.get(UNKNOWN_METHODS[i]);
if (method != null) {
x += method.toString().length();
}
}
return x;
}
}