Scala的不可变对象创建成本

4
我看到像[1]中的for-comprehension这样的帖子,这让我想知道使用不可变Map与可变Map的整体含义是什么。似乎Scala开发人员非常习惯于允许对不可变数据结构进行突变以产生新对象的代价-或者我可能错过了什么。虽然我理解它对线程安全很有好处,但如果我已经知道如何微调我的可变对象以做出同样的保证,每个不可变数据结构上的每个变异操作都返回一个新实例又会怎么样呢?
[1] 在Scala中,如何执行类似于SQL SUM和GROUP BY的操作?

2
结构共享意味着通常不需要完全新的副本。 - Travis Brown
2
如果不可变数据结构上的每个突变操作都返回一个新实例,它不会复制整个实例,不可变结构允许数据共享。 - vptheron
你有没有一个具体的例子,证明for循环的性能不足? - Rüdiger Klaehn
5
这里的实际问题是什么? - Jasper-M
@Rudiger,我不需要一个具体的例子。我正在问一个问题,试图揭示Scala在幕后做了什么。这是你评论的第二个问题,你似乎并没有真正尝试回答问题。 - Corey J. Nolet
显示剩余2条评论
3个回答

7
总的来说,回答这种性能问题的唯一方法是在你的实际代码中进行分析。微基准测试常常会引导人们做出错误的结论(例如,请参阅这篇性能测试的故事),特别是当你谈论并发性时,最佳策略可能因实际使用情况的不同而有所不同。
理论上讲,一个足够聪明的编译器应该能够-也许是借助于线性类型系统(推断或其他方式)-重现可变数据结构的所有效率优势。实际上,由于它拥有更多关于程序员意图的信息,并且受到程序员必须指定的非本质细节的限制较小,这样的编译器应该能够生成更高效的代码-例如,GCC为了优化目的将代码重写成不可变形式(SSA)。就更接近家庭的例子而言,许多实际的Java程序具有完全足够的吞吐量,但存在于Java的垃圾回收器停止整个堆栈以压缩堆的延迟问题。如果一个JVM知道某些对象是不可变的,那么它就能够在不停止整个系统的情况下移动它们(你可以简单地复制对象,更新所有对它的引用,然后删除旧副本,因为如果一些线程看到旧版本而另一些线程看到新版本是无关紧要的)。
实际上,这取决于具体情况,再次进行基准测试是唯一的方法。根据我的经验,在大多数实际业务问题中可用的程序员时间投资水平下,花费x小时在(不可变的)Scala版本上往往比花同样的时间在可变的Scala或Java版本上获得更高性能的程序-实际上,在生产足够高效的Scala版本所需的程序员时间量中,可能根本无法完成Java版本(特别是如果我们要求相同的缺陷率)。另一方面,如果你有无限的专家程序员时间可用,并且需要获得绝对最佳的性能,你可能想要使用一个非常低级别的可变语言(这就是为什么LAPACK仍然是用Fortran编写的)-甚至像JP Morgan最近那样直接在FPGA上实现你的算法。
但即使在这种情况下,你也可能希望在高级语言中拥有原型,以便你可以编写测试并比较这两者来确认高性能实现是否正确。特别是如果我们只是在Scala中讨论可变与不可变,那么过早的优化是万恶之源。写好你的程序,如果性能不足,请进行分析并查看热点。如果你真正花费了太多时间在复制不可变数据结构上,那么就应该在适当的时候将其替换为可变版本,并手动检查线程安全性保证。如果你编写的代码是适当解耦的,那么在需要的时候更换性能关键部分应该很容易,而在此之前,你可以获得代码更简单、更易于理解的开发时间收益(尤其是在并发情况下)。根据我的经验,良好编写的代码中的性能问题比人们预期的要少得多;大多数

lmm,非常感谢您抽出时间来解释这个问题。每个人都在寻求具体的例子和性能基准-我正在工作或寻求的两者都不是。我是一名对正在快速学习的语言有许多问题的工程师,但并不像我想象中那样熟悉它。我已经完成了Martin Ordersky的《Scala编程》一书的一半以上,但仍有很多问题没有得到解答。再次感谢您抽出时间解释这些概念。 - Corey J. Nolet
说实话,Stack Overflow可能不是这种一般性问题的最佳场所 - 它非常偏向于具体、明确的问题,这些问题有一个单一的客观答案。但我真的不想把你送到reddit或IRC :/。Scala吸引了很多重复的批评,当新手说出似乎是对该语言攻击的话时,一些Scala人可能会变得相当防御;这对所有人来说都是不幸的。 - lmm
我开始意识到我的问题可能不适合在Stack Overflow上提问。我长期以来一直是IRC的用户。我现在在freenode上,如果有人有时间更彻底地回答我的“假设”和“这是如何工作的”问题,我也不介意加入Scala聊天室。我自己是一个开源爱好者,我不确定为什么任何开发者社区会对建设性的挑战和批评感到防御-似乎这可能是一个迹象...特别是当我只是想学习的时候。 - Corey J. Nolet
嗯,在 Freenode 上的 #scala 可能是这些问题的更好场所,我希望人们会乐于助人。说实话,作为这门语言的铁杆粉丝,社区存在一些问题,部分原因是一些已经离开或被禁止的个性。Scala 似乎已经达到了一种“酷儿点”,它足够受欢迎以至于会受到很多批评,而它试图将面向对象与函数式结合起来的尝试可能会受到双方的抨击。希望这只是一个阶段,社区会改善;我已经看到了一些令人鼓舞的迹象。但是,这个问题确实存在。 - lmm
说实话,我的问题并不是批评。我是一名Java/C++开发人员,之前曾广泛使用过Groovy。我喜欢Scala没有像Groovy一样对编译器错误置之不理,有时候类型推断也很棒。我也喜欢Scala从许多不同的语言中汲取了一些最好的东西。我的问题更多的是“马丁·奥德斯基想到了什么”,而不是“为什么Scala要强制这样做?” - Corey J. Nolet

2
你的问题基于对使用不可变对象产生成本的误解和错误假设。
使用“保证”不可变对象以及由不可变对象构建的不可变对象,可以使用“结构共享”,因此您可以创建新对象而无需进行深复制,并且您可以重复使用新对象所依赖的旧对象的部分,大致上来说,这显著减轻了使用不可变对象的影响。
那么与经过精细调整和手工制作的可变对象有什么区别呢?
  • 不可变对象更适合FP范例
  • 编译时优化和检查
  • 降低了运行时异常的机会

1
这个问题太泛泛了,很难给出确定的答案。似乎你只是对使用for comprehensions等惯用的scala代码中发生的对象分配量感到不舒服。
Scala编译器没有进行任何特殊的魔法来融合操作或省略对象分配。由编写数据结构的人来确保函数式数据结构尽可能地重用以前版本的内容(结构共享)。Scala集合中使用的许多数据结构都做得相当好。例如,可以查看关于Functional Data Structures in Scala的演讲,以便获得一般性的想法。
如果您对细节感兴趣,那么需要获取的书籍是Purely Functional Data Structures,作者是Chris Okasaki。该书中的材料也适用于其他函数式语言,如Haskell、OCaml和Clojure。

JVM非常擅长分配和回收短暂的对象。许多看起来对于习惯于低级编程的人来说非常低效的事情实际上非常高效。但是,有些情况下可变状态具有性能或其他优势。这就是为什么Scala不禁止可变状态,而只是倾向于不可变性的原因。如果您发现确实需要可变状态以提高性能,则通常最好将可变状态包装在Akka Actor中,而不是尝试正确地进行低级线程同步。


我想,当我以适当的方式表达问题以传达我所寻求的结果时,我往往会得到更好的答案。谢谢!在我完成《Scala编程》之后,我一定会购买这本书并阅读。 - Corey J. Nolet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接