Haskell PINNED或STACK存储器可提高性能

问题描述

我试图从有时在GHC(8.4.3)中进行的优化中受益,在该优化中,将大量数据的“构建”放入PINNED内存中。 (我在这里可能不掌握所有正确的术语)。这是一个简单的示例:

Pinned1.hs:

main = print $ sum $ tail ([1..100000000] :: [Int])

然后:

ghc -O2 Pinned1.hs -prof -rtsopts
Pinned1 +RTS -hc -p -xt
hp2ps -e8in -c Pinned1.hp

显示〜40K固定,几乎没有堆栈使用,Pinned1 +RTS -hd -p -xt显示〜40K是ARR_WORDS。

Pinned1.prof显示:

total time  =        2.14 secs   (2137 ticks @ 1000 us,1 processor)
total alloc = 8,000,046,088 bytes  (excludes profiling overheads)

看过-sdump-simpl,我可以看到导致这种情况的那种代码。这是一个稍微复杂些的示例,它从Core反向翻译为Haskell代码,发生相同的事情:

Pinned2.hs:

main = print $ sum $ snd $ wgoC 1 0
wgoC :: Int -> Int -> (Int,[Int])
wgoC n finalState =
  let (nxt,ys') = case n of 100000000 -> (finalState,[])
                             _         -> wgoC (n+1) finalState
  in (n,n + nxt * 9: ys')

wgoC将下一个n返回,该n用于计算列表中的值。它报告大约40K PINNED / ARR_WORDS内存,几乎没有堆栈,并且此配置文件输出:

total time  =        5.50 secs   (5500 ticks @ 1000 us,1 processor)
total alloc = 16,800,112 bytes  (excludes profiling overheads)

但是,这:

Pinned3.hs:

main = print $ sum $ snd $ wgoD 1 0
wgoD :: Int -> Int -> (Int,[Int])
wgoD n finalState =
  let (ttl',[])
                              _         -> wgoD (n+1) finalState
  in (ttl' + n,n + (ttl' + n) * 9 : ys')

2分钟后无法完成。它的确仅具有1000000的值,而我看不到PINNED内存和STACK的使用情况(〜100M)。 (我认为正是STACK用法使它以某种方式运行得慢得多)。

我在Pinned2和Pinned3之间看到的主要区别是Pinned3在返回状态中包含来自递归调用的信息(返回对的fst:后续值的累加总和),但是Pinned2仅包含wgoC的参数。

所以我的问题是:

Q1)使用PINNED内存的决定在哪里发生(在compiler pipeline中)? -ddump-simpl和-ddump-cmm都没有明显区别(尽管有点复杂,所以也许我错过了一些东西)。

Q2)PINNED / STACK决策基于什么? (我可以找到的唯一对PINNED的引用,例如this,说它对FFI调用很有用,但似乎也为“优化”采用了它。)

Q3)是否可以通过某种方式修改Pinned3,使其使用PINNED?

Q4)(作为最后的手段)Pinned3是否还有其他一些调整,以便有足够的堆栈空间,并且可以在合理的时间内运行? (天真的,我希望它的性能与Pinned2相似。)

[请注意,我只是在这里试图理解PINNED / STACK机制。我敢肯定还有其他方法可以编写Pinned3,因此它可以很好地融合并且几乎不需要任何内存,但是这个问题并不关乎。]

谢谢!

解决方法

固定内存在这里不起作用。

该程序:

main = print $ sum $ tail ([1..100000000] :: [Int])

不直接使用任何固定内存 。您看到的固定内存来自运行时系统本身的初始化。固定内存由GHC的字节数组基元分配;在用户代码中,当您使用Data.TextData.ByteString两者都使用字节数组进行内部实现时,最有可能看到固定的内存使用情况。对于该程序,我将猜测用于stdin和stdout的I / O缓冲区是固定的,但也许还有其他东西。无论如何,Int的列表不会固定任何内容。

(几乎)所有Haskell程序一样,Pinned1.hs使用大量的堆和大量的堆栈(每个千兆字节),但至关重要的是,释放它的速度与分配的速度一样快(或者如果您更喜欢在谈论堆栈,将其“弹出”与“推动”一样快。 Pinned2.hs也是如此。这些程序运行正常。

Pinned3.hs的问题不是它使用堆栈而不是固定内存,而是使用了比Pinned1Pinned2更多的堆栈,并且无法像弹出一样快地弹出它推动它,所以堆栈堆积。

那么,为什么Pinned3会堆积堆栈?

一般来说,如果递归调用的结果的某些部分是函数应用程序的目标,则堆栈会在递归调用中累积,当它返回 AND 时,评估结果的这一部分本身需要另一个递归呼叫。考虑一下程序:

eatStack 100000000 = 1
eatStack n = 1 + eatStack (n + 1)
main = print $ eatStack 1

其中,编译并运行:

stack ghc -- -O2 -prof -rtsopts EatStack.hs
./EatStack +RTS -hd -p -xt
stack exec hp2ps -- -e8in -c EatStack.hp

产生通常的金字塔形堆栈堆积(峰值为1.4G左右)。这里的问题是,递归eatStack (n+1)的返回值在返回时受函数应用程序\x -> 1 + x的约束,并且计算结果本身需要进一步递归。也就是说,计算eatStack 0要求在调用\x -> 1 + x之前将eatStack 1压入堆栈,该操作只能在调用\x -> 1 + x之前将eatStack 2压入堆栈后返回其结果,等等。结果就是堆栈堆积。

值得注意的是,构造函数应用程序的处理方式有所不同。以下程序:

noStack 100000000 = []
noStack n = 1 : noStack (n + 1)
main = print $ last (noStack 1)

将部分应用的构造函数(:) 1应用于递归结果noStack (n+1),不使用栈。 (它似乎使用了40k的固定,但实际上又是运行时系统。EatStack也使用了40k的固定。)在某些情况下(不在此处),这样的构造函数应用程序可能会导致堆堆积,但实际上并没有通常不会累积堆栈。

对于您的Pinned2Pinned3示例,正在发生类似的事情,尽管显然要复杂一些。首先让我们看一下Pinned2,然后考虑评估wgoC 1 0。匹配大小写并替换参数,评估等效于:

wgoC 1 0 = 
  let (nxt,ys') = wgoC 2 0
  in (1,1 + nxt * 9 : ys')

sum . snd要求列表的第一个元素,即thunk 1 + nxt * 9时,这将强制nxt通过递归调用进行求值。因为此返回值受函数应用程序(即\x -> 1 + x * 9)的约束,所以使用了一些堆栈,但是对递归调用进行了评估:

wgoC 2 0 = 
  let (nxt,ys') = wgoC 3 0
  in (2,2 + nxt * 9 : ys')

立即为nxt调用中的本地绑定wgoC 1 0生成所需的值,即返回的元组fst (wgoC 2 0) = 2的第一个元素,而无需进一步递归。因此,我们取值2,弹出延续\x -> 1 + x * 9,然后将值传递给延续产生1 + 2 * 9 = 19。这给出了列表的第一个元素,没有净栈使用量。列表的其余部分,即ys'调用中的本地绑定wgoC 1 0,仍然处于2 + nxt * 9 : ys'调用的封闭状态wgoC 2 0中。

当需要下一个元素时,我们需要一些堆栈以将递归\x -> 2 + x * 9应用于递归nxt中的结果(nxt,ys') = wgoC 3 0,但这将以相同的方式求值,立即返回nxt = 3和一个ys'的重击,因此延续\x -> 2 + x * 9将从堆栈中弹出并应用于nxt = 3,而无需进一步递归,产生2 + 3 * 9 = 29和一个3 + nxt * 9 : ys'wgoC 3 0调用关闭的笨拙的Pinned3

每个元素都可以被强制使用而不使用净堆栈。我们推送一个延续,然后立即弹出它,并将其应用于不需要本身的递归调用的返回值的一部分,而无需进一步递归。结果是没有净堆栈堆积。

现在,考虑使用wgoD 1 0和呼叫wgoD 1 0 = let (ttl',ys') = wgoD 2 0 in (ttl' + 1,1 + (ttl' + 1) * 9 : ys')

sum . snd

1 + (ttl' + 1) * 9要求列表的第一个元素,即thunk ttl'时,这将强制\x -> 1 + (ttl' + 1) * 9通过递归调用进行求值。因为有一个待处理的函数应用程序wgoD 2 0 = let (ttl',ys') = wgoD 3 0 in (ttl' + 2,2 + (ttl' + 2) * 9 : ys') ,所以将使用一些堆栈。递归调用:

ttl'

只能通过评估返回元组wgoC 1 0的第一部分来为ttl' + 2调用中的本地绑定ttl'提供所需的值,但这需要强制wgoD 3 0通过递归ttl'调用。由于\x -> x + 2会在返回时受函数应用程序wgoD 3 0 = let (ttl',ys') = wgoD 4 0 in (ttl' + 3,3 + (ttl' + 3) * 9 : ys') 的约束,因此我们要多推一些堆栈并继续求值:

ttl'

要获得在wgoD 2 0调用中本地绑定的必需wgoD 3 0,我们需要评估ttl' + 3中返回元组的第一个组成部分,即\x -> x + 3。这是一个函数应用程序ttl',我们将其压入堆栈,应用于递归调用wgoD 4 0返回的Pinned3

因此,\x -> x + 2将一系列连续的\x -> x + 3\x -> x + 4wgoD 2 0'等推入堆栈,所有这些都是为了评估的第一个成分wgoD 100000000 0返回的元组,直到到达finalState = 0才没有机会弹出任何东西,然后,如果有足够的堆栈,它最终会得到一个数字ttl' + n作为第一个元组组件达到目标然后,在应用延续时,所有堆栈都将弹出,我们将拥有列表的第一个元素!

一旦通过,情况就不会那么糟了。至此,所有表达式n + (ttl' + n) * 9均已求值,并且可以在计算表达式Pinned3中重用它们,因此可以相对快速地生成其余元素-因为必须保留其值在某个地方-您还将以与堆栈使用大致相同的速率累积堆使用。

您可以将100000000换成类似10000000(七个零)的内容,并且在合理的时间内运行,并显示金字塔形状的堆栈堆积。它在1.4演出左右达到峰值,然后又回落到零。

在保持{{1}}的算法结构不变的同时,我看不到任何真正简单的“修复”方法。

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...