与其程序计数器部分相比,优化 Java 8 功能算法
Optimize Java 8 functional algorithm compared to its procedural counter part
我正在尝试优化 Java 8 中的功能操作,与其程序等效操作相比,但我遇到了一些严重的性能问题。
情况
我必须根据给定枚举的值解析 HTTP Headers String, List<String>
,该枚举将 HeaderName 映射到许多可能的变体 String, Set<String>
。
例子
给定以下 HttpHeaders:
public static final Map<String, List<String>> httpHeaders = new HashMap<>();
httpHeaders.put("Content-Type", Arrays.asList("application/json", "text/x-json"));
httpHeaders.put("SID", Arrays.asList("ABC123"));
httpHeaders.put("CORRELATION-ID", Arrays.asList("ZYX666"));
我的自定义枚举:
日志Headers
protected final String key;
protected final Set<String> variation;
SESSION_ID("_sid", Arrays.asList("SESSION-ID", "SID"));
CORRELATION_ID("cid", Arrays.asList("CORRELATION-ID", "CID")),
private LogHeaders(final String logKey, final List<String> logKeyVariations) {
this.logKey = logKey;
this.logKeyVariations = new HashSet<>(logKeyVariations);
}
@Override
public String toString() {
return this.logKey;
}
结果应该是 "LogHeaders.key" 的映射以及来自 HttpHeaders 的相应变体的值集。对于给定的 header 只能有一种变化:
// {LogHeaders.key : HttpHeaderValue>
{
_sid=[ABC123],
_cid=[ZYX666]
}
程序代码
final Map<String, List<String>> logHeadersToValue = new HashMap<>();
for (final LogHeaders header : LogHeaders.values()) {
for (final String variation : header.getLogKeyVariations()) {
final List<String> headerValue = httpHeaders.get(variation);
if (headerValue != null) {
logHeadersToValue.put(header.logKey, headerValue);
break;
}
}
}
功能代码
final Map<String, List<String>> logHeadersToValue =
EnumSet.allOf(LogHeaders.class)
.stream()
.collect(Collectors.toMap(
LogHeaders::toString,
logHeader -> logHeader.getLogKeyVariations().stream()
.map(variation -> httpHeaders.get(variation)).filter(Objects::nonNull)
.collect(singletonCollector())));
public static <T> Collector<T, ?, T> singletonCollector() {
return Collectors.collectingAndThen(Collectors.toList(), list -> {
if (list.size() < 1) {
return null;
}
return list.get(0);
});
}
当前基准
FunctionalParsing : 0.086s
ProceduralParsing : 0.001s
你知道我该如何优化我的功能部分吗?
谢谢
更新基准
我 运行 100k 预热 + 100k 迭代 @Tagir Valeev 代码:
FunctionalParsing : 0.040s
ProceduralParsing : 0.010s
更新基准#2
我 运行 100k 预热 + 100k 迭代 @Misha 代码:
FunctionalParsing : 0.025s
ProceduralParsing : 0.017s
我绝对确定您做的基准测试不正确。您可能只执行了一次。您不关心您的程序运行 0.001 秒还是 0.086 秒,对吗?它仍然比你眨眼还快。所以你可能想要多次 运行 这段代码。但您似乎只测量了一次时间,并错误地假设每个连续的 运行 将花费大致相同的时间。在第一次启动期间,代码主要由解释器执行,而 JIT-compiled 之后会运行得更快。这对于 stream-related 代码非常重要。
至于您的代码,似乎不需要自定义收集器。你可以这样实现它:
final Map<String, List<String>> logHeadersToValue =
EnumSet.allOf(LogHeaders.class)
.stream()
.collect(Collectors.toMap(
LogHeaders::toString,
logHeader -> logHeader.getLogKeyVariations().stream()
.map(httpHeaders::get).filter(Objects::nonNull)
.findFirst().orElse(null)));
此解决方案也可能更快,因为它不会读取多个 http header(就像它是通过程序代码中的 break
完成的)。
您的功能代码与原始代码不一样。如果 LogHeader
之一无法匹配 header,旧代码将跳过它,而功能代码将抛出 NullPointerException
.
将您的原始代码直接转换为流如下所示:
Map<String, List<String>> logHeadersToValue = Arrays.stream(LogHeaders.values())
.collect(
HashMap::new,
(map, logHeader) -> logHeader.getLogKeyVariations().stream()
.filter(httpHeaders::containsKey)
.findAny()
.ifPresent(x -> map.put(logHeader.key, httpHeaders.get(x))),
Map::putAll
);
如果您希望它更高效且更易于阅读,请考虑预先计算其键的每个变体的 Map<String,String>
。您可以像这样修改 enum
来做到这一点:
enum LogHeaders {
SESSION_ID("_sid", "SESSION-ID", "SID"),
CORRELATION_ID("cid", "CORRELATION-ID", "CID");
final String key;
final Map<String, String> variations;
private LogHeaders(final String key, String... variation) {
this.key = key;
variations = Arrays.stream(variation).collect(collectingAndThen(
toMap(x -> x, x -> key),
Collections::unmodifiableMap
));
}
// unmodifiable map of every variation to its key
public final static Map<String, String> variationToKey =
Arrays.stream(LogHeaders.values())
.flatMap(lh -> lh.variations.entrySet().stream())
.collect(collectingAndThen(
toMap(Map.Entry<String, String>::getKey, Map.Entry<String, String>::getValue),
Collections::unmodifiableMap
)); // will throw if 2 keys have the same variation
}
如果存在重复的变体,这种方法的优点是可以快速失败。现在代码变得非常简单:
Map<String, List<String>> logHeadersToValue = LogHeaders.variationToKey.keySet().stream()
.filter(httpHeaders::containsKey)
.collect(toMap(LogHeaders.variationToKey::get, httpHeaders::get));
我正在尝试优化 Java 8 中的功能操作,与其程序等效操作相比,但我遇到了一些严重的性能问题。
情况
我必须根据给定枚举的值解析 HTTP Headers String, List<String>
,该枚举将 HeaderName 映射到许多可能的变体 String, Set<String>
。
例子
给定以下 HttpHeaders:
public static final Map<String, List<String>> httpHeaders = new HashMap<>();
httpHeaders.put("Content-Type", Arrays.asList("application/json", "text/x-json"));
httpHeaders.put("SID", Arrays.asList("ABC123"));
httpHeaders.put("CORRELATION-ID", Arrays.asList("ZYX666"));
我的自定义枚举:
日志Headers
protected final String key;
protected final Set<String> variation;
SESSION_ID("_sid", Arrays.asList("SESSION-ID", "SID"));
CORRELATION_ID("cid", Arrays.asList("CORRELATION-ID", "CID")),
private LogHeaders(final String logKey, final List<String> logKeyVariations) {
this.logKey = logKey;
this.logKeyVariations = new HashSet<>(logKeyVariations);
}
@Override
public String toString() {
return this.logKey;
}
结果应该是 "LogHeaders.key" 的映射以及来自 HttpHeaders 的相应变体的值集。对于给定的 header 只能有一种变化:
// {LogHeaders.key : HttpHeaderValue>
{
_sid=[ABC123],
_cid=[ZYX666]
}
程序代码
final Map<String, List<String>> logHeadersToValue = new HashMap<>();
for (final LogHeaders header : LogHeaders.values()) {
for (final String variation : header.getLogKeyVariations()) {
final List<String> headerValue = httpHeaders.get(variation);
if (headerValue != null) {
logHeadersToValue.put(header.logKey, headerValue);
break;
}
}
}
功能代码
final Map<String, List<String>> logHeadersToValue =
EnumSet.allOf(LogHeaders.class)
.stream()
.collect(Collectors.toMap(
LogHeaders::toString,
logHeader -> logHeader.getLogKeyVariations().stream()
.map(variation -> httpHeaders.get(variation)).filter(Objects::nonNull)
.collect(singletonCollector())));
public static <T> Collector<T, ?, T> singletonCollector() {
return Collectors.collectingAndThen(Collectors.toList(), list -> {
if (list.size() < 1) {
return null;
}
return list.get(0);
});
}
当前基准
FunctionalParsing : 0.086s
ProceduralParsing : 0.001s
你知道我该如何优化我的功能部分吗?
谢谢
更新基准
我 运行 100k 预热 + 100k 迭代 @Tagir Valeev 代码:
FunctionalParsing : 0.040s
ProceduralParsing : 0.010s
更新基准#2
我 运行 100k 预热 + 100k 迭代 @Misha 代码:
FunctionalParsing : 0.025s
ProceduralParsing : 0.017s
我绝对确定您做的基准测试不正确。您可能只执行了一次。您不关心您的程序运行 0.001 秒还是 0.086 秒,对吗?它仍然比你眨眼还快。所以你可能想要多次 运行 这段代码。但您似乎只测量了一次时间,并错误地假设每个连续的 运行 将花费大致相同的时间。在第一次启动期间,代码主要由解释器执行,而 JIT-compiled 之后会运行得更快。这对于 stream-related 代码非常重要。
至于您的代码,似乎不需要自定义收集器。你可以这样实现它:
final Map<String, List<String>> logHeadersToValue =
EnumSet.allOf(LogHeaders.class)
.stream()
.collect(Collectors.toMap(
LogHeaders::toString,
logHeader -> logHeader.getLogKeyVariations().stream()
.map(httpHeaders::get).filter(Objects::nonNull)
.findFirst().orElse(null)));
此解决方案也可能更快,因为它不会读取多个 http header(就像它是通过程序代码中的 break
完成的)。
您的功能代码与原始代码不一样。如果 LogHeader
之一无法匹配 header,旧代码将跳过它,而功能代码将抛出 NullPointerException
.
将您的原始代码直接转换为流如下所示:
Map<String, List<String>> logHeadersToValue = Arrays.stream(LogHeaders.values())
.collect(
HashMap::new,
(map, logHeader) -> logHeader.getLogKeyVariations().stream()
.filter(httpHeaders::containsKey)
.findAny()
.ifPresent(x -> map.put(logHeader.key, httpHeaders.get(x))),
Map::putAll
);
如果您希望它更高效且更易于阅读,请考虑预先计算其键的每个变体的 Map<String,String>
。您可以像这样修改 enum
来做到这一点:
enum LogHeaders {
SESSION_ID("_sid", "SESSION-ID", "SID"),
CORRELATION_ID("cid", "CORRELATION-ID", "CID");
final String key;
final Map<String, String> variations;
private LogHeaders(final String key, String... variation) {
this.key = key;
variations = Arrays.stream(variation).collect(collectingAndThen(
toMap(x -> x, x -> key),
Collections::unmodifiableMap
));
}
// unmodifiable map of every variation to its key
public final static Map<String, String> variationToKey =
Arrays.stream(LogHeaders.values())
.flatMap(lh -> lh.variations.entrySet().stream())
.collect(collectingAndThen(
toMap(Map.Entry<String, String>::getKey, Map.Entry<String, String>::getValue),
Collections::unmodifiableMap
)); // will throw if 2 keys have the same variation
}
如果存在重复的变体,这种方法的优点是可以快速失败。现在代码变得非常简单:
Map<String, List<String>> logHeadersToValue = LogHeaders.variationToKey.keySet().stream()
.filter(httpHeaders::containsKey)
.collect(toMap(LogHeaders.variationToKey::get, httpHeaders::get));