使用Repa在“循环2-D阵列”的最佳方式

时间:2011-05-09 16:15:01

标签: arrays performance haskell profiling repa

3 个答案:

答案 0 :(得分:64)

代码审核备注

  • 47.8%的时间花在GC上。
  • 1.5G在堆上分配(!)
  • 修复代码看起来比列表代码更复杂 lot
  • 发生了大量并行GC
  • 我可以在-N4机器上获得高达300%的效率
  • 使用更多类型的签名可以更容易分析......
  • rsize未使用(看起来很贵!)
  • 你将修复数组转换为向量,为什么?
  • (**)的所有用途都可以由(^)上更便宜的Int替换。
  • 有一些可疑的大型常量列表。这些都必须转换为数组 - 这似乎很昂贵。
  • any (==True)or
  • 相同

时间分析

COST CENTRE                    MODULE               %time %alloc

squared_diff                   Main                  25.0   27.3
insideParticle                 Main                  13.8   15.3
sum_squared_diff               Main                   9.8    5.6
rcoords                        Main                   7.4    5.6
particle_extended              Main                   6.8    9.0
particle_slice                 Main                   5.0    7.6
insideParticles                Main                   5.0    4.4
yslice                         Main                   3.6    3.0
xslice                         Main                   3.0    3.0
ssd_vec                        Main                   2.8    2.1
**^                            Main                   2.6    1.4

表明,您的函数squared_diff有点可疑:

squared_diff :: Array DIM2 Double
squared_diff = deepSeqArrays [rcoords,particle_extended]
                    ((force2 rcoords) -^ (force2 particle_extended)) **^ 2    

虽然我没有看到任何明显的修复。

空间分析

在空间配置文件中没有太多惊人的东西:你清楚地看到列表阶段,然后是矢量阶段。列表阶段分配了一大部分,然后回收。

enter image description here

按类型分解堆,我们最初看到很多列表和元组被分配(按需),然后分配并保存了一大块数组:

enter image description here

再一次,有点像我们期望看到的......数组内容的分配特别不是列表代码(实际上总体上要少一些),但运行起来只需要更长的时间。

使用保留器分析检查空间泄漏:

enter image description here

那里有一些有趣的东西,但没什么好吃的。 zcoords保留列表程序执行的长度,然后为修复运行分配一些数组(SYSTEM)。

检查核心

所以在这一点上我首先假设你确实在列表和数组中实现了相同的算法(即在数组的情况下没有进行额外的工作),并且没有明显的空间泄漏。所以我怀疑是非常优化的修复代码。让我们看看核心(ghc-core

  • 基于列表的代码看起来很好。
  • 数组代码看起来合理(即出现未装箱的基元),但非常复杂,而且很多。

内联所有CAF

我为所有顶级数组定义添加了内联编译指示,希望删除一些CAFS,并让GHC更难以优化数组代码。这确实使GHC难以编译模块(在处理模块时分配高达4.3G和10分钟)。这对我来说是一个线索,GHC以前不能很好地优化这个程序,因为当我增加阈值时,它会有新的东西。

操作

  • 使用-H可以减少在GC中花费的时间。
  • 尝试消除从列表到repas到矢量的转换。
  • 所有这些CAF(顶级常量数据结构)都有点奇怪 - 真正的程序不会是顶级常量列表 - 实际上,这个模块在病理学上是这样的,导致许多值保留在长期,而不是被优化。向内浮动本地定义。
  • 向Repa的作者Ben Lippmeier寻求帮助,特别是因为有一些时髦的优化问题正在发生。

答案 1 :(得分:7)

我将代码更改为强制rcoordsparticle_extended,并且发现我们在其中直接失去了大部分时间:

COST CENTRE                    MODULE               %time %alloc

rcoords                        Main                  32.6   34.4
particle_extended              Main                  21.5   27.2
**^                            Main                   9.8   12.7

对此代码的最大改进显然是以更好的方式生成这两个常量输入。

请注意,这基本上是一个懒惰的流式算法,而你失去时间的是一次性分配至少两个24361803元素数组的沉没成本,然后可能至少分配一次或两次或者放弃分享。我认为,对于这个代码来说,最好的情况是使用非常好的优化器和大量的重写规则,将大致匹配列表版本(也可以非常容易地并行化)。

我认为Ben和amp;合。我会对这个基准测试感兴趣,但我绝对怀疑这对于一个严格的数组库来说不是一个好的用例,而我怀疑matlab隐藏了一些聪明的优化它的ngrid函数(优化,我' ll grant,这可能对移植到port有用。)

修改

这是一种快速而又脏的方法来并行化列表代码。导入Control.Parallel.Strategies,然后将numberInsideParticles写为:

numberInsideParticles particles coords = length $ filter id $ 
    withStrategy (parListChunk 2000 rseq) $ P.map (insideParticles particles) coords

这显示了良好的加速,因为我们扩展核心(一个核心12s到8s时3.7s),但火花创建的开销意味着即使是8个核心,我们也只匹配单核心非并行版本。我尝试了一些替代策略并得到了类似的结果。同样,我不确定我们可以做多少比这里的单线程列表版本更好。由于每个粒子的计算都很便宜,我们主要强调的是分配,而不是计算。我想象的这样一个大赢家将是矢量化计算,而且据我所知,它几乎需要手工编码。

另请注意,并行版本大约70%的时间花在GC上,而单核版本花费1%的时间在那里(即分配尽可能地被有效地融合掉)。

答案 2 :(得分:7)

我已经添加了一些关于如何优化Haskell wiki的Repa程序的建议: http://www.haskell.org/haskellwiki/Numeric_Haskell:_A_Repa_Tutorial#Optimising_Repa_programs