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