【问题标题】:TreeMap filtered view performanceTreeMap 过滤视图性能
【发布时间】:2014-08-29 17:50:23
【问题描述】:

我有一门课(除其他外):

public class TimeSeries {
  private final NavigableMap<LocalDate, Double> prices;

  public TimeSeries() { prices = new TreeMap<>(); }

  private TimeSeries(NavigableMap<LocalDate, Double> prices) {
    this.prices = prices;
  }

  public void add(LocalDate date, double price) { prices.put(date, price); }

  public Set<LocalDate> dates() { return prices.keySet(); }

  //the 2 methods below are examples of why I need a TreeMap

  public double lastPriceAt(LocalDate date) {
    Map.Entry<LocalDate, Double> price = prices.floorEntry(date);
    return price.getValue(); //after some null checks
  }

  public TimeSeries between(LocalDate from, LocalDate to) {
    return new TimeSeries(this.prices.subMap(from, true, to, true));
  }
}

现在我需要在地图上有一个“过滤”视图,其中只有一些日期可用。为此,我添加了以下方法:

public TimeSeries onDates(Set<LocalDate> retainDates) {
  TimeSeries filtered = new TimeSeries(new TreeMap<> (this.prices));
  filtered.dates().retainAll(retainDates);
  return filtered;
}

onDates 方法是一个巨大的性能瓶颈,占程序处理时间的 85%。而且由于该程序正在运行数百万次模拟,这意味着在该方法中花费了数小时。

如何提高该方法的性能?

【问题讨论】:

  • 过滤后的TimeSeries副本会被迭代多少次? retainDates 通常比 prices 小很多吗?
  • @biziclop 该地图通常包含 1500 个条目,并且集合的大小几乎相同(可能大小相同,包含相同的日期)。过滤的 TimeSeries 通常只使用(迭代)一次。
  • 我对LocalDate不太熟悉,但是简单地做prices.get(localeDate)来获得你想要的值会安全吗?
  • 在这种情况下,我唯一能说的是,与其创建地图副本,然后调用retainAll(),不如先从一张空地图开始,然后添加需要保留的项目。因此,您只需对集合进行一次迭代,而且还可以节省一些内存。
  • @assylias 没关系。我更具体地说是在谈论您的 onDates 方法。创建一个新的空 NavigableMap,然后遍历您的集合,并添加您通过调用 prices.get(localeDate) 获得的任何非空值。

标签: java performance treemap


【解决方案1】:

我会尝试ImmutableSortedMap,假设您可以使用它。它基于排序数组而不是平衡树,所以我猜它的开销要小得多(*)。要构建它,您需要采用 biziclop 的想法,因为构建器不支持删除。

(*) 那里调用了Collection.sort,但它应该是无害的,因为集合已经排序并且TimSort 针对这种情况进行了优化。


如果您的原始地图在创建 onDates 后没有改变,也许视图会有所帮助。如果确实如此,您将需要一些“持久”地图,这听起来相当复杂。

也许一些基于排序数组和二进制搜索的 hacky 解决方案可能是最快的,也许您甚至可以先将 LocalDate 转换为 int 然后再转换为 double 并将所有内容放入一个交错的 double[] 中,以便节省内存(希望还有时间)。你需要自己的二分搜索,但这很简单。


视图的想法相当简单,假设

  • 您不需要所有 NavigableMap 方法,只需要几个方法
  • 原图不变
  • retainDates 中仅缺少几个元素

一个示例方法:

public double lastPriceAt(LocalDate date) {
    Map.Entry<LocalDate, Double> price = prices.floorEntry(date);
    while (!retainDates.contains(price.getKey()) {
        price = prices.lowerEntry(price.getKey()); // after some null checks
    }
    return price.getValue(); // after some null checks
}

【讨论】:

  • 这是一个无法导航的旧版本 - latest one 是,我会试一试。谢谢你的想法。
  • 我已经运行了一个微基准测试,其中包含 1500 个数据点和 3 个在 retainDates 中缺失的日期:我的代码需要 50 微秒,而您的提案需要 80 微秒。所以你的建议确实显着改进了 biziclop 的代码,但仍然比原始版本慢。还是谢谢!
  • 经过更多测试后,看起来唯一的方法是使用LocalDate[] dates;double[] prices; 并使用二进制搜索lastPriceAt。使用数组可以将性能提高约 3 倍。
  • @assylias 日期是否代表天数?您可以轻松地将它们转换为int 吗?因为在这种情况下,您可以拥有单个价格数组,这可能会更快,具体取决于指定价格的日期之间的“差距”有多大。
【解决方案2】:

最简单的优化:

public TimeSeries onDates(Set<LocalDate> retainDates) {
  TreeMap<LocalDate, Double> filteredPrices = new TreeMap<>();
  for (Entry<LocalDate, Double> entry : prices.entrySet() ) {
      if (retainDates.contains( entry.getKey() ) ) {
          filteredPrices.put( entry.getKey(), entry.getValue() );
      }
  }
  TimeSeries filtered = new TimeSeries( filteredPrices );
  return filtered;
}

为您节省先创建完整地图副本然后再次遍历副本以进行过滤的成本。

【讨论】:

  • 谢谢,我明天测试一下。 TreeMap 的复制构造函数已经过优化,所以我不知道这是否会更快。那就待定!
  • 构造函数已经过优化,但retainAll() 没有:它只是盲目地遍历整个地图。尽管在保留时可能需要较少的树重新平衡。值得一试,我并不是说它一定会奏效。
  • 我在retainDates 中运行了一个包含1500 个数据点和3 个缺失日期的微型基准测试:我的代码需要50 微秒,而您的提案需要200 微秒。还是谢谢!
  • 我认为这次成功很大程度上是由于调整大小/重新散列。
猜你喜欢
  • 1970-01-01
  • 2012-05-13
  • 2015-10-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-07
  • 1970-01-01
相关资源
最近更新 更多