在article written by Daniel Korzekwa中,他表示以下代码的表现:
list.map(e => e*2).filter(e => e>10)
比使用Java编写的迭代解决方案更糟糕。
任何人都可以解释原因吗?什么是Scala中此类代码的最佳解决方案(我希望它不是Scala-fied的Java迭代版本)?
答案 0 :(得分:15)
特定代码很慢的原因是因为它正在处理基元但它使用泛型操作,因此原语必须加框。 (如果List
及其祖先是专门的,这可以得到改善。)这可能会使事情减慢5倍左右。
此外,从算法上讲,这些操作有点贵,因为你制作了一个完整的列表,然后创建一个全新的列表,抛弃中间列表的一些组件。如果你一举做到这一点,那么你会更好。你可以这样做:
list collect (case e if (e*2>10) => e*2)
但如果计算e*2
真的很贵怎么办?那么你可以
(List[Int]() /: list)((ls,e) => { val x = e*2; if (x>10) x :: ls else ls }
除了这会出现倒退。 (如果需要,你可以反转它,但这需要创建一个新的列表,这在算法上也不是理想的。)
当然,如果您使用的是单链表,那么您在Java中会遇到相同类型的算法问题 - 您的新列表将以向后的方式结束,或者您必须创建两次,首先是反向,然后是向前,或者你必须使用(非尾部)递归来构建它(这在Scala中很容易,但在任何一种语言中都不建议使用,因为你会耗尽堆栈),或者你必须创建一个可变列表然后假装事后说它不可变。 (顺便说一句,您也可以在Scala中执行此操作 - 请参阅mutable.LinkedList
。)
答案 1 :(得分:13)
基本上它是两次遍历列表。一次将每个元素乘以两个。然后另一次过滤结果。
将其视为一个while循环,生成带有中间结果的LinkedList。然后应用过滤器的另一个循环产生最终结果。
这应该更快:
list.view.map(e => e * 2).filter(e => e > 10).force
答案 2 :(得分:6)
解决方案主要在于JVM。虽然Scala在@specialization
的图形中有一个解决方法,但它大大增加了任何专业类的大小,并且只解决了一半的问题 - 另一半是临时对象的创建。
JVM实际上很好地优化了很多,或者性能会更糟糕,但Java不需要Scala所做的优化,因此JVM不提供它们。我希望通过在Java中引入SAM
非实际闭包来改变某种程度。
但是,最终,它归结为平衡需求。 Java和Scala比Scala的函数等效快得多的while
循环可以在C中更快地完成。然而,尽管微基准测试告诉我们,人们使用Java。
答案 3 :(得分:4)
Scala方法更抽象,更通用。因此很难优化每一个案例。
我可以想象,如果HotSpot JIT编译器发现未使用直接结果,可能会在未来对代码应用流和循环融合。
此外,Java代码还有更多功能。
如果您真的只想改变数据结构,请考虑transform
。
它看起来有点像map
但不会创建新的集合,例如。 G:
val array = Array(1,2,3,4,5,6,7,8,9,10).transform(_ * 2)
// array is now WrappedArray(2, 4, 6, 8, 10, 12, 14, 16, 18, 20)
我真的希望将来会增加一些额外的就地操作......
答案 4 :(得分:3)
为了避免遍历列表两次,我认为for
语法在这里是个不错的选择:
val list2 = for(v <- list1; e = v * 2; if e > 10) yield e
答案 5 :(得分:2)
Rex Kerr正确地指出了主要问题:在不可变列表上操作,所陈述的代码段在内存中创建了中间列表。请注意,这不一定比等效的Java代码慢;你永远不会在Java中使用不可变数据结构。
Wilfried Springer有一个很好的Scala idomatic解决方案。使用view
,不会创建整个列表的(操作)副本。
请注意,使用视图可能并不总是理想的。例如,如果您的第一个调用是filter
,预计会丢弃大部分列表,则可能值得明确创建较短版本并仅在此之后使用view
以提高内存位置以后的迭代。
答案 6 :(得分:1)
list.filter(e =&gt; e * 2&gt; 10).map(e =&gt; e * 2)
此尝试首先减少List。所以第二次遍历是在较少的元素上。