与其程序计数器部分相比,优化 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));