现代CPU经过优化,因此访问和修改内存中的相同位置(时间局部性)以及内存中的连续位置(空间局部性)是非常快速的操作。
现在,由于Haskell是一种纯粹的不可变语言,你自然无法覆盖现有的内存块,可能使foldl
之类的东西比带有连续访问结果变量的for
循环慢得多将在C.
Haskell是否在内部做任何事情来减轻这种性能损失?一般来说,它的地方性质是什么?
答案 0 :(得分:11)
一般规则是" vanilla" Haskell编程你很少(如果有的话)控制内存布局和内存局部性。
但是,确实存在许多允许这种控制的高级功能,以及在这些上面公开友好抽象的库。 vector
库可能是后者中最受欢迎的库。该库提供了几种固定大小的数组类型,其中两个(Data.Vector.Unboxed
和Data.Vector.Storable
)通过将向量及其内容表示为连续的内存数组来为您提供数据局部性。 Data.Vector.Unboxed
甚至包含一个简单的自动"数组结构"转换 - 未装箱的对矢量将表示为一对未装箱的矢量,每个矢量对应一个。组件。
另一个例子是用于图像处理的JuicyPixels
库,它将内存中的图像表示为连续的位图。这实际上是Data.Vector.Storable
的最低点,它利用标准工具(Foreign.Storable
)将用户定义的Haskell数据类型转换为原始字节。
但一般模式是这样的:在Haskell中,当您对内存局部性感兴趣时,您可以确定哪些数据需要从中受益并将其捆绑在一个自定义数据类型中,该数据类型的实现旨在提供局部性和绩效保证。编写这样的数据类型是一项高级任务,但大部分的工作已经以可重用的方式完成(例如,JuicyPixels
主要只是重用vector
)。
另请注意:
vector
提供流融合优化,以在应用嵌套矢量转换时消除中间数组。如果生成0到1,000,000之间的向量,过滤掉偶数,将(^2)
函数映射到该函数并对结果的元素求和,不分配任何数组 - 库有智能将其重写为累加器循环从0到1,000,000。因此,foldl
向量不一定比for
循环慢 - 可能根本没有数组!vector
也提供了可变数组。更一般地说,在Haskell中,如果你真的坚持,你可以覆盖现有的内存。它只是(a)不是语言中的默认范例,因此(b)有点笨拙,但如果你只需要在几个性能敏感的地方就可以使用它。所以大部分时间,答案是"我想要记忆位置"是"使用vector
。"
答案 1 :(得分:10)
Haskell是一种非常高级的语言,你问一个关于极低级细节的问题。
总的来说,Haskell的性能可能类似于任何垃圾收集语言,如Java或C#。特别是,Haskell具有可变数组,其性能与任何其他数组相似。 (您可能需要未装箱的阵列来匹配C性能。)
对于类似折叠的东西,如果最终结果类似于机器整数,则可能在整个循环期间最终在处理器寄存器中结束。因此,最终的机器代码几乎与“C中连续访问的变量”完全相同。 (如果结果是字典或其他东西,那么可能不是。但这也和C一样。)
更一般地说,如果locallity对您很重要,那么任何垃圾收集语言可能都不是您的朋友。但是,同样,您可以使用未装箱的数组来解决这个问题。
所有这些谈话都很棒,但是如果你真的想知道特定的Haskell程序有多快,基准测试。事实证明,编写良好的Haskell程序通常非常快。 (就像大多数编译语言一样。)
补充:您可以要求GHC以Core格式输出部分编译的代码,该代码比Haskell低,但比机器代码高。这让你可以看看编译器决定做什么(特别是内容已被内联的地方,删除了抽象的地方等等)。这可以帮助你找出最终代码的样子,无需一直到机器代码。