凌晨4点,詹姆斯敦-苏格兰渡轮及其他优化策略

当性能很重要时,了解可用选项也很重要。

新年快乐!

我以为我将从2018年的性能优化故事开始到2018年。

该博客的重点

捷径

2017年1月2日,我和家人在凌晨3:30在本田领航员的码头上坐了一个半小时,等待凌晨4点到达詹姆斯敦-苏格兰的渡轮到来。 我走上了一条捷径,这并不完全符合我的预期。 我决定在我的汽车的导航系统上采用较短距离的路线,以避免仅在95号州际公路上向北行驶,然后才不得不向南行驶至弗吉尼亚州的威廉斯堡。 在几次从佛罗里达回来的深夜游乐中,我一直陷于弗吉尼亚州95号公路上的交通拥挤。 当我们沿着较短的路线到达路的尽头时,导航系统指示下一个转弯就是上渡轮(请参见上图)。

我愿意走较慢的当地道路,尤其是因为这是清晨,那里没有人流。 我们发现为时已晚,因为我们选择的路线包括乘坐渡轮。 在这一点上,我们只有两个选择。 我们可以等待轮渡并希望它正在运行,或者转身为我们的旅程增加3至4个小时。 经典的霍布森之选。 我们在等轮渡。 事实证明,一旦我们将汽车停在渡轮上,这将是一次有趣的体验,但是我宁愿在14小时后的凌晨4点选择另一种选择。

“两条路在树林中分开……” —罗伯特·弗罗斯特

我当然少走了一个。 我确实学到了一条从奥兰多到威廉斯堡之前不知道的新路线,以及根据轮渡时间表优化该路线所需的计划。

您可能会问,这次旅行与Eclipse Collections有什么关系? 好吧,我走的路是串行路 (殖民时代的一条车道), 懒惰 (轮渡工作直到到达码头),以及盒装 (您的汽车实际上是由其他汽车装在渡轮上的) Eclipse Collections和Java Streams可以选择的许多选项中。

“过早的优化是万恶之源” —唐纳德·克努斯(Donald Knuth)

编写代码时,应优先考虑可读性,而不是性能。 但是,在发现最后一刻之前,您唯一的选择就是停止并等待下一个渡轮,这有助于您了解可用的性能优化选项。 实际上,您可能能够在不牺牲可读性的情况下获得更好的性能。 实际上,您可能以前没有意识到可以提高可读性和性能的选项。

我相信有一套迭代模式优化策略,我相信所有开发人员都应该意识到这一点,以便他们可以适当地调整其代码以获得最佳性能。

优化代码时不要猜测。 首先证明您有需要解决的问题。 然后对您认为可能有助于证明它们确实有效的所有解决方案进行基准测试。

旅行者要当心:衡量性能优化的好处可能会浪费很多时间。 我在下面进行的测试每次需要45-50分钟才能运行。 我必须将它们与单元测试一起运行几次,以验证所有相似测试的结果是否相同。 当您看到图表时,您可能首先会因为想要将代码更改为更“最佳”而受到图表的强迫。 就应用程序的整体性能而言,最佳可能并不意味着明显更快。 这些测试中的每一个最多花费数百毫秒来运行。 它们都是“快速的”,因为它们都在内存中。 最佳解决方案可能只会在大量执行过程中节省成本。 如果您碰巧看到一种您不了解的可读性更高的解决方案,请选择该解决方案。

迭代模式优化策略

您是否知道如何在不牺牲可读性的情况下分别和同时利用所有这些策略来提高性能?

  • 渴望 -立即执行针对每种算法和数据结构的潜在优化。 急切的算法与您将获得的手编码的for-loop极为接近,因此它们易于理解和调试。 我更希望将eage作为迭代集合的默认选项。 它是最简单且通常最简洁易读的解决方案。 我认为所有其他解决方案都是潜在的优化方案,可能还为时过早。
  • 原始文件 -如果可以避免装箱原始文件,则可以减少内存成本并可能提高性能。 我会尽可能使用原始集合和算法。
  • 惰性 -仅在调用终端操作时执行。 优化包括减少执行多个操作时所需的内存量和总计算量。 懒惰运行时,短路效应确实可以帮助提高性能。 我更喜欢懒惰,因为我执行多个操作会导致创建临时集合。
  • 并行并行运行成本更高。 您需要正确的数据大小,算法和多个内核。 如果拥有所有这些,则可以从并行运行中受益。 测量它以证明这一点。

渴望与懒惰—了解它们如何工作

让我们列出五个整数,并急切地和懒惰地执行过滤,映射和减少一组操作。

  @测试 
公共无效eagerVsLazy()
{
long eagerSum =列表。
可变的 .with(1、2、3、4、5)
.tap(i->系统。
out .println( “急切选择:” + i))
.select(i-> i%2 == 0)
.tap(i->系统。
out .println( “渴望收集:” + i))
.collectInt(i-> i * 2)
.tap(i->系统。
out .println( “渴望的总和:” + i))
。和();
系统。
out .println(eagerSum);
long lazySum =列表。
可变的 .with(1、2、3、4、5)
.asLazy()
.tap(i->系统。
out .println( “惰性选择:” + i))
.select(i-> i%2 == 0)
.tap(i->系统。
out .println( “惰性收集:” + i))
.collectInt(i-> i * 2)
.tap(i->系统。
out .println( “惰性总和:” + i))
。和();
系统。
out .println(lazySum);
断言。
assertEquals (eagerSum,lazySum);
}

除了在惰性示例中对asLazy的附加调用之外,代码应看起来相同。 打印结果如下:

 急切选择:1 
急切选择:2
急切选择:3
急切选择:4
急切选择:5
渴望收集:2
渴望收集:4
渴望总和:4
渴望总和:8
12
延迟选择:1
延迟选择:2
懒收集:2
懒惰的总和:4
延迟选择:3
延迟选择:4
懒收集:4
懒惰的总和:8
延迟选择:5
12

请注意,在惰性情况下,执行顺序如何在lambda上更改。 在紧急情况下,将在执行期间创建两个附加列表作为中间结果。 在最终求和之前,先创建一个带有两个整数(2,4)的整数列表,然后创建一个带有两个整数(4,8)的IntList。 在惰性情况下,不会创建任何中间集合。 这样可以减少产生的垃圾。 这就是为什么当涉及多个操作时,我更喜欢惰性执行。 如果只涉及一个操作,那么我将默认使用急切的解决方案。

如果我们看一下串行Stream解决方案,它的执行顺序将与懒惰的Eclipse Collections解决方案相同。

  @测试 
公共无效流()
{
int streamSum =列表。
可变的 .with(1、2、3、4、5)
。流()
.peek(i->系统。
out .println( “流过滤器:” + i))
.filter(i-> i%2 == 0)
.peek(i->系统。
out .println( “流图:” + i))
.mapToInt(i-> i * 2)
.peek(i->系统。
out .println( “ stream sum:” + i))
。和();
系统。
out .println(streamSum);
}

这是输出:

 流过滤器:1 
流过滤器:2
流图:2
流总和:4
流过滤器:3
流过滤器:4
流图:4
流总和:8
流过滤器:5
12

懒惰+平行=难以跟随

使用批处理大小为1的Eclipse Collections延迟并行进行,因此我们可以看到非常小的列表的结果。

 @测试 
公共无效parallel()
{
long parallelSum =列表。
可变的 .with(1、2、3、4、5)

asParallel (执行程序。newWorkStealingPool (), 1
.select(i-> {
系统。
out .println( “ parallel select:” + i);
返回i%2 == 0;
})
.collect(i-> {
系统。
out .println( “平行收集:” + i);
返回我* 2;
})
.sumOfInt(i-> {
系统。
out .println( “平行和:” + i);
返回我
});
系统。
out .println(parallelSum);
}
运行1:
并行选择:2
并行选择:1
并行选择:4
平行收集:4
并行选择:3
总计:8
并行选择:5
平行收集:2
总计:4
12
运行2:
并行选择:1
并行选择:3
并行选择:2
并行选择:5
并行选择:4
平行收集:2
平行收集:4
平行和:4
平行总和:8
12
运行3:
并行选择:4
并行选择:2
平行收集:2
并行选择:5
并行选择:3
并行选择:1
平行总和:4
平行收集:4
平行总和:8
12

运行之间的结果是一致的,但是不能保证lambda的执行顺序也不是一致的。

使用并行流:

  @测试 
公共无效parallelStream()
{
int streamSum =时间间隔。
oneTo (5).toList()
.parallelStream()
.peek(i->系统。
out .println( “流过滤器:” + i))
.filter(i-> i%2 == 0)
.peek(i->系统。
out .println( “流图:” + i))
.mapToInt(i-> i * 2)
.peek(i->系统。
out .println( “ stream sum:” + i))
。和();
系统。
out .println(streamSum);
}
运行1:
流过滤器:4
流过滤器:1
流图:4
流过滤器:2
流总和:8
流过滤器:3
流过滤器:5
流图:2
流总和:4
12
运行2:
流过滤器:5
流过滤器:1
流过滤器:3
流过滤器:2
流过滤器:4
流图:2
流图:4
流总和:4
流总和:8
12
运行3:
流过滤器:2
流过滤器:4
流图:2
流图:4
流总和:8
流过滤器:1
流过滤器:3
流过滤器:5
流总和:4
12

测量,执行和重复。

我将使用存储在列表中的一百万个随机生成的整数,展示一组用例的不同选项及其性能特征。 这些不太可能是您在生产代码中会遇到的用例,但是希望它们可以说明您下次在基本Java数据结构和算法中找不到您所不期望的瓶颈时可能没有意识到的一些选项。 我将演示在四个不同的用例下,使用对象列表和原始列表,渴望的和惰性的API,串行和并行执行之间的性能差异。

在每个用例中,我都分享我所观察到的-预期的和意外的。 我只是观察。 我没有弄清楚结果为何如此。 “为什么”也许是另一个博客的主题。

用例-过滤,映射,缩小和过滤/映射/缩小

  1.甚至将整数过滤到列表中 
2.将整数乘以2并将结果存储在列表中
3.将所有整数求和成一个长整数
4.过滤/映射/缩小(过滤偶数,乘以2,总和成长)

数据-1,000,000整数

 私人List  jdkList; 
私有MutableList ecList;
私有IntList ecPrimitiveList;
私人ExecutorService executorService;
@设定
公共无效setup()
{
PrimitiveIterator.OfInt intGenerator =
新的Random(1L).ints(-1000,1000).iterator();
this.ecList =
快速列表。
newWithNValues (1_000_000,intGenerator :: nextInt);
this.jdkList =新的ArrayList (1_000_000);
this.jdkList.addAll(this.ecList);
this.ecPrimitiveList =
this.ecList.collectInt(i-> i,new IntArrayList(1_000_000));
this.executorService =执行者。
newWorkStealingPool ();
}

硬件

我将使用具有以下硬件规格的MacPro来衡量基准:

 处理器名称:12核Intel Xeon E5 
处理器速度:2.7 GHz
处理器数量:1
核心总数:12
L2高速缓存(每核):256 KB
L3快取:30 MB
记忆体:64 GB

软件

为了说明可用于这些特定用例的不同选项,我将在Eclipse Collections和Streams中使用JDK 1.8.0_152。

标杆管理

我将JMH 1.19版用作测试的基准测试工具。 我正在使用2个fork运行30个热身迭代和20个测量迭代。 我在测试中使用Mode.Throughput,因此它们易于阅读。 数字以每秒操作数为单位。 数字越大,结果越好。

 公共静态void main(String [] args)引发RunnerException 
{
选项options =新的OptionsBuilder()
。包括(
“。*” + IntListJMHTest.class.getSimpleName()+ “。*”
.forks(2)
.mode(模式
吞吐量
.timeUnit(TimeUnit。

.warmupIterations(30)
。建立();
新的Runner(options).run();
}

我将以深绿色突出显示运行中的最佳总体结果。 我将以浅绿色突出显示最佳的串行执行结果。 在图表的标签中使用EC的地方,它表示使用Eclipse Collections的解决方案。 在使用JDK的地方,该解决方案使用标准的JDK方法。

过滤偶数整数

预期:

  • 我期望ECParallelEager表现更好。
  • 我期望原始集合的性能优于盒装集合。
  • 我期望串行渴望胜过串行懒惰。

意外:

  • 我没想到并行流会表现得很差。
  @基准 
公共MutableList
filterECBoxedEager ()
{
返回this.ecList.select(i-> i%2 == 0);
}
@基准
公共MutableList
filterECBoxedLazy ()
{
返回this.ecList
.asLazy()
.select(i-> i%2 == 0)
.toList();
}
@基准
公共MutableList
filterECParallelEager ()
{
返回ParallelIterate。
选择
this.ecList,
i-> i%2 == 0,
新的CompositeFastList (),
假);
}
@基准
公共MutableList
filterECParallelLazy ()
{
返回this.ecList
.asParallel(this.executorService,50_000)
.select(i-> i%2 == 0)
.toList();
}
@基准
公共IntList
filterECPrimitiveEager ()
{
返回this.ecPrimitiveList.select(i-> i%2 == 0);
}
@基准
公共IntList
filterECPrimitiveLazy ()
{
返回this.ecPrimitiveList
.asLazy()
.select(i-> i%2 == 0)
.toList();
}
@基准
公共列表
filterJDKBoxedParallelStream ()
{
返回this.jdkList
.parallelStream()
.filter(i-> i%2 == 0)
.collect(收藏家。
toList ());
}
@基准
公共列表
filterJDKBoxedStream ()
{
返回this.jdkList
。流()
.filter(i-> i%2 == 0)
.collect(收藏家。
toList ());
}

映射每个整数x 2

预期:

  • 我期望原始集合的性能优于盒装集合。
  • 我期望串行渴望胜过串行懒惰。

意外:

  • 我没想到ECParallelLazy会表现得这么差。
  • 我没想到任何一个Stream解决方案都会如此糟糕。
  @基准 
公共MutableList
mapECBoxedEager ()
{
返回this.ecList.collect(i-> i * 2);
}
@基准
公共MutableList
mapECBoxedLazy ()
{
返回this.ecList
.asLazy()
.collect(i-> i * 2)
.toList();
}
@基准
公共MutableList
mapECParallelEager ()
{
返回ParallelIterate。
收集
this.ecList,i-> i * 2,
新的CompositeFastList (),
假);
}
@基准
公共MutableList
mapECParallelLazy ()
{
返回this.ecList
.asParallel(this.executorService,50_000)
.collect(i-> i * 2)
.toList();
}
@基准
公共IntList
mapECPrimitiveEager ()
{
返回this.ecPrimitiveList
.collectInt(i-> i * 2,IntLists。
可变的 .empty());
}
@基准
公共IntList
mapECPrimitiveLazy ()
{
返回this.ecPrimitiveList
.asLazy()
.collectInt(i-> i * 2)
.toList();
}
@基准
公共列表
mapJDKBoxedParallelStream ()
{
返回this.jdkList
.parallelStream()
.mapToInt(i-> i * 2)
.boxed()
.collect(收藏家。
toList ());
}
@基准
公共List mapJDKBoxedStream()
{
返回this.jdkList
。流()
.mapToInt(i-> i * 2)
.boxed()
.collect(收藏家。
toList ());
}

将所有整数相加

预期:

  • 我期望原始集合的性能优于盒装集合。
  • 我期望这里的并行化几乎不会带来任何好处。 对int求和是非常快速的操作。 我期望热切的原语比大多数并行选项要快。

意外:

  • 我没想到串行流会崩溃。 Java 9似乎有所改进。我再次使用Java 9运行了基准测试,该特定基准测试提高了约7-8倍。
  @基准 
公共长
sumECBoxedEager ()
{
返回this.ecList.sumOfInt(Integer :: intValue);
}
@基准
公共长
sumECBoxedLazy ()
{
返回this.ecList
.asLazy()
.sumOfInt(Integer :: intValue);
}
@基准
公共长
sumECParallelEager ()
{
返回ParallelIterate。
sumByInt
this.ecList,
我->整数。
valueOf (0),
整数:: intValue).get(0);
}
@基准
公共长
sumECParallelLazy ()
{
返回this.ecList
.asParallel(this.executorService,50_000)
.sumOfInt(Integer :: intValue);
}
@基准
公共长
sumECPrimitiveEager ()
{
返回this.ecPrimitiveList.sum();
}
@基准
公共长
sumECPrimitiveLazy ()
{
返回this.ecPrimitiveList
.asLazy()
。和();
}
@基准
公共长
sumJDKBoxedParallelStream ()
{
返回this.jdkList
.parallelStream()
.mapToLong(Integer :: longValue)
。和();
}
@基准
公共长
sumJDKBoxedStream ()
{
返回this.jdkList
。流()
.mapToLong(Integer :: longValue)
。和();
}

筛选,映射,求和

预期:

  • 我希望懒惰的操作能胜过渴望的操作。
  • 我期望原始的懒惰将胜过所有其他串行操作。
  • 我期望JDKBoxedParallelStream在此用例中能表现良好。

意外:

  • 我没想到ECParallelEager会比ECParallelLazy更好或更出色。
  • 我没想到JDKBoxedParallelStream会比ECParallelLazy做得更好。
  @基准 
公共长
filterMapSumECBoxedEager ()
{
返回this.ecList
.select(i-> i%2 == 0)
.sumOfInt(i-> i * 2);
}
@基准
公共长
filterMapSumECBoxedLazy ()
{
返回this.ecList
.asLazy()
.select(i-> i%2 == 0)
.sumOfInt(i-> i * 2);
}
@基准
公共长
filterMapSumECOptimizedParallelEager ()
{
返回ParallelIterate。
sumByInt
this.ecList,
我->我%2,
i-> i * 2).get(0);
}
@基准
公共长
filterMapSumECOptimizedParallelLazy ()
{
返回this.ecList
.asParallel(this.executorService,50_000)
.sumOfInt(i-> i%2 == 0?i * 2:0);
}
@基准
公共长
filterMapSumECParallelLazy ()
{
返回this.ecList
.asParallel(this.executorService,50_000)
.select(i-> i%2 == 0)
.sumOfInt(i-> i * 2);
}
@基准
公共长
filterMapSumECPrimitiveEager ()
{
返回this.ecPrimitiveList
.select(i-> i%2 == 0)
.collectInt(i-> i * 2,IntLists。
可变 .empty())
。和();
}
@基准
公共长
filterMapSumECPrimitiveLazy ()
{
返回this.ecPrimitiveList
.asLazy()
.select(i-> i%2 == 0)
.collectInt(i-> i * 2)
。和();
}
@基准
公共长
filterMapSumJDKBoxedParallelStream ()
{
返回this.jdkList
.parallelStream()
.filter(i-> i%2 == 0)
.mapToLong(i->(long)(i * 2))
。和();
}
@基准
公共长
filterMapSumJDKBoxedStream ()
{
返回this.jdkList
。流()
.filter(i-> i%2 == 0)
.mapToLong(i->(long)(i * 2))
。和();
}

恭喜你!

我希望您喜欢该博客,并学习了有关使用Eclipse集合和Java流的“迭代模式选项”和“优化策略”的新知识。 如果您唯一的工具是锤子,那么其他所有东西都是钉子。 在开始旅程之前了解可用选项并根据需要进行适应是编写更好,响应速度更快的应用程序的关键之一。 如果发生这种情况,这也可以帮助您减少从奥兰多到威廉斯堡的压力。

推荐建议

  • 优先于拳击的基元。
  • 对于单个或融合的操作,建议使用Eager迭代。
  • 对于多步操作,请首选惰性迭代。
  • 在并行之前进行验证。
  • 如果您想要获得霍布森选择之外的更多内容,请尝试Eclipse Collections。

Eclipse Collections 开放 供稿 如果您喜欢该库,可以通过在GitHub上加注星标来告知我们。