Haskell 的局部属性是什么?

What are the locality properties of Haskell?

现代 CPU 已经过优化,因此访问和修改内存中的同一位置(时间局部性)以及内存中的连续位置(空间局部性)都是极快的操作。

现在,由于 Haskell 是一种纯粹不可变的语言,您自然不能覆盖现有的内存块,这可能会使 foldl 之类的事情比 for 循环慢得多连续访问的结果变量将在 C 中。

Haskell 是否会在内部采取任何措施来减轻这种性能损失?总的来说,它的位置属性是什么?

Haskell 是一种非常高级的语言,而你问的是一个非常低级细节的问题。

总体而言,Haskell 的性能可能类似于任何垃圾收集语言,如 Java 或 C#。特别是,Haskell 具有可变数组,其性能与任何其他数组相似。 (您可能需要未装箱的数组来匹配 C 性能。)

对于折叠之类的东西,如果最终结果是机器整数之类的东西,那么它可能在整个循环期间都在处理器寄存器中结束。所以最终的机器代码与“C 中连续访问的变量”几乎相同。 (如果结果是字典之类的,那么可能不是。但这也与 C 相同。)

更一般地说,如果本地化对您很重要,那么任何垃圾收集语言可能都不是您的朋友。但是,同样,您可以使用未装箱的数组来解决这个问题。

所有这些讨论都很棒,但是如果您真的想知道特定 Haskell 程序有多快,对其进行基准测试。事实证明,编写良好的 Haskell 程序通常非常快。 (就像大多数编译语言一样。)

补充:你可以要求GHC输出Core格式的部分编译代码,比Haskell低级但比机器码高。这让你 看到 编译器决定做什么(特别是,哪些内容被内联,哪些抽象被删除等)这可以帮助你找出最终代码的内容看起来像,而不必一直深入到机器代码。

一般规则是,对于 "vanilla" Haskell 编程,您几乎无法(如果有的话)控制内存布局和内存位置。

但是,确实存在许多允许此类控制的更高级功能,以及在这些功能之上公开友好抽象的库。 vector library is probably the most popular of the latter. This library provides several fixed-size array types, two of which (Data.Vector.Unboxed and Data.Vector.Storable) 通过将向量及其内容表示为连续的内存数组来为您提供数据局部性。 Data.Vector.Unboxed 甚至包含一个简单的自动 "structure of arrays" 转换——一个未装箱的向量对将表示为一对未装箱的向量,每个向量对的一个分量。

另一个示例是 JuicyPixels library for image processing, which represents images in memory as contiguous bitmaps. This actually bottoms out to Data.Vector.Storable, which exploits a standard facility (Foreign.Storable),用于将用户定义的 Haskell 数据类型与原始字节进行相互转换。

但一般模式是这样的:在 Haskell 中,当您对内存局部性感兴趣时,您会确定哪些数据需要从中受益,并将其捆绑在一个自定义数据类型中,该自定义数据类型的实现已设计提供本地和性能保证。编写这样的数据类型是一项高级任务,但大部分工作已经以可重用的方式完成(例如,请注意 JuicyPixels 主要只是重用 vector)。

另请注意:

  1. vector 提供 stream fusion 优化以在应用嵌套向量转换时消除中间数组。如果您生成一个从 0 到 1,000,000 的向量,过滤掉偶数,将 (^2) 函数映射到它上面并对结果的元素求和,则不会分配任何数组——该库具有将其重写为一个从 0 到 1,000,000 的累加器循环。所以向量的 foldl 不一定比 for 循环慢——可能根本就没有数组!
  2. vector 也提供可变数组。更一般地说,在 Haskell 中,如果你真的坚持,你 可以 覆盖现有内存。它只是 (a) 不是该语言的默认范例,因此 (b) 有点笨拙,但如果您只在一些性能敏感的地方需要它,那绝对易于处理。

所以大多数时候,"I want memory locality"的答案是"use vector."