【问题标题】:Optimize Java 8 functional algorithm compared to its procedural counter part与其程序对应部分相比,优化 Java 8 函数算法
【发布时间】:2015-07-14 16:12:54
【问题描述】:

我正在尝试优化 Java 8 中的函数操作,与它的等效程序相比,但我遇到了一些严重的性能问题。

情况

我必须从给定枚举的值中解析 HTTP 标头 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"));

还有我的自定义枚举:

LogHeaders

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 的相应变体的值集。对于给定的标头,只有一种变化是可能的:

// {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

您知道如何优化我的功能部分吗?

谢谢

更新基准

我使用@Tagir Valeev 代码运行了 100k 预热 + 100k 迭代:

FunctionalParsing : 0.040s

ProceduralParsing : 0.010s

更新基准 #2

我使用@Misha 代码运行了 100k 热身 + 100k 迭代:

FunctionalParsing : 0.025s

ProceduralParsing : 0.017s

【问题讨论】:

  • 第一个问题显然是,你是如何衡量性能的,第二个问题是这些集合大致有多大?
  • 当涉及到任何新的闪亮功能 f 时,您似乎是一个典型错误的受害者:使用 f 是为了使用 f 而你就是 f;你的旧代码工作得很好,既然 f 显然没有好处,为什么还要尝试使用它呢?
  • 没有人告诉过函数式代码更快。
  • 忘记性能——“功能”代码完全不可读
  • 我也愿意接受任何“可读性”建议。我使用了 Junit 测试输出。准备好后,我将使用 JMH 基准测试我的问题。

标签: java functional-programming java-8 procedural-programming


【解决方案1】:

我绝对确定您做的基准测试不正确。您可能只执行了一次。你不在乎你的程序是 0.001 秒还是 0.086 秒,对吧?它仍然比你眨眼还快。因此,您可能希望多次运行此代码。但是您似乎只测量了一次时间,并错误地假设每次连续运行都将花费大致相同的时间。在第一次启动期间,代码主要由解释器执行,而稍后将进行 JIT 编译,运行速度会更快。这对于流相关的代码非常重要。

至于您的代码,似乎没有必要使用自定义收集器。你可以这样实现它:

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 标头(就像在程序代码中通过 break 完成的那样)。

【讨论】:

  • 结果来自 Junit 运行。我将运行它们 10k 次,并为另外 10k 次迭代获得一些结果,让 JIT 完成它的工作。谢谢你的提议。
  • 我做了 100k 热身 + 100k 基准测试。功能解析在 0.046 秒内运行,过程解析在 0.014 秒内运行。基本上慢了 4 倍~ 没什么大不了的。
【解决方案2】:

您的功能代码与原始代码的功能不同。如果LogHeaders 之一未能匹配标头,旧代码将跳过它,而功能代码将抛出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&lt;String,String&gt;。您可以像这样修改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));

【讨论】:

  • 这很聪明,它消除了算法中的计算复杂性。谢谢您的意见。我正在更新我的代码并将发布一个新的基准。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-01-20
  • 1970-01-01
  • 1970-01-01
  • 2020-02-09
  • 1970-01-01
  • 2021-04-07
相关资源
最近更新 更多