【问题标题】:Making sense from GHC profiler从 GHC 分析器中理解
【发布时间】:2017-02-23 22:57:44
【问题描述】:

我试图从 GHC 分析器中理解。有一个相当简单的应用程序,它使用 werqlens-aeson 库,在学习 GHC 分析时,我决定尝试一下。

使用不同的选项(time 工具、+RTS -p -RTS+RTS -p -h)我获得了完全不同数量的内存使用量。有了所有这些数字,我现在完全无法理解发生了什么,以及应用程序实际使用了多少内存。

这种情况让我想起了 Arthur Bloch 的那句话:“一个有手表的人知道现在几点。一个有两个手表的人永远不确定。”

请您给我建议一下,我如何阅读所有这些数字,以及每个数字的含义。

这里是数字:

time -l 报告大约 19M

#/usr/bin/time -l ./simple-wreq
...
        3.02 real         0.39 user         0.17 sys
  19070976  maximum resident set size
         0  average shared memory size
         0  average unshared data size
         0  average unshared stack size
     21040  page reclaims
         0  page faults
         0  swaps
         0  block input operations
         0  block output operations
        71  messages sent
        71  messages received
      2991  signals received
        43  voluntary context switches
      6490  involuntary context switches

使用+RTS -p -RTS 标志报告大约92M。虽然它说“total alloc”,但对我来说似乎很奇怪,像这样一个简单的应用程序可以分配和释放 91M

# ./simple-wreq +RTS -p -RTS      
# cat simple-wreq.prof
        Fri Oct 14 15:08 2016 Time and Allocation Profiling Report  (Final)

           simple-wreq +RTS -N -p -RTS

        total time  =        0.07 secs   (69 ticks @ 1000 us, 1 processor)
        total alloc =  91,905,888 bytes  (excludes profiling overheads)

COST CENTRE                             MODULE                          %time %alloc

main.g                                  Main                             60.9   88.8
MAIN                                    MAIN                             24.6    2.5
decodeLenient/look                      Data.ByteString.Base64.Internal   5.8    2.6
decodeLenientWithTable/fill             Data.ByteString.Base64.Internal   2.9    0.1
decodeLenientWithTable.\.\.fill         Data.ByteString.Base64.Internal   1.4    0.0
decodeLenientWithTable.\.\.fill.\       Data.ByteString.Base64.Internal   1.4    0.1
decodeLenientWithTable.\.\.fill.\.\.\.\ Data.ByteString.Base64.Internal   1.4    3.3
decodeLenient                           Data.ByteString.Base64.Lazy       1.4    1.4


                                                                                                             individual     inherited
COST CENTRE                                              MODULE                            no.     entries  %time %alloc   %time %alloc

MAIN                                                     MAIN                              443           0   24.6    2.5   100.0  100.0
 main                                                    Main                              887           0    0.0    0.0    75.4   97.4
  main.g                                                 Main                              889           0   60.9   88.8    75.4   97.4
   object_                                               Data.Aeson.Parser.Internal        925           0    0.0    0.0     0.0    0.2
    jstring_                                             Data.Aeson.Parser.Internal        927          50    0.0    0.2     0.0    0.2
   unstream/resize                                       Data.Text.Internal.Fusion         923         600    0.0    0.3     0.0    0.3
   decodeLenient                                         Data.ByteString.Base64.Lazy       891           0    1.4    1.4    14.5    8.1
    decodeLenient                                        Data.ByteString.Base64            897         500    0.0    0.0    13.0    6.7
....

+RTS -p -hhp2ps 显示以下图片和两个数字:标题中的 114K 和图表上大约 1.8Mb 的内容。

而且,以防万一,这里是应用程序:

module Main where

import Network.Wreq
import Control.Lens
import Data.Aeson.Lens
import Control.Monad

main :: IO ()
main = replicateM_ 10 g
  where 
    g = do
        r <- get "http://httpbin.org/get"
        print $ r ^. responseBody
                   . key "headers"
                   . key "User-Agent"
                   . _String

更新 1:感谢大家的精彩回复。正如建议的那样,我添加了+RTS -s 输出,因此为每个阅读它的人构建了整个图片。

#./simple-wreq +RTS -s
...
     128,875,432 bytes allocated in the heap
      32,414,616 bytes copied during GC
       2,394,888 bytes maximum residency (16 sample(s))
         355,192 bytes maximum slop
               7 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0       194 colls,     0 par    0.018s   0.022s     0.0001s    0.0022s
  Gen  1        16 colls,     0 par    0.027s   0.031s     0.0019s    0.0042s

UPDATE 2:可执行文件的大小:

#du -h simple-wreq
63M     simple-wreq

【问题讨论】:

    标签: haskell profiling ghc


    【解决方案1】:

    time -l 正在显示操作系统(显然)看到的进程的(驻留,即未换出)大小。这包括 Haskell 堆的最大大小的两倍(由于 GHC 的 GC 工作方式),加上 RTS 或其他 C 库分配的任何其他内容,加上可执行文件本身的代码以及它所依赖的库等。我我猜在这种情况下,19M 的主要贡献者是您的可执行文件的大小。

    total alloc 是分配到 Haskell 堆上的总量。它根本不是最大堆大小的度量(人们通常所说的“我的程序使用了多少内存”)。分配非常便宜,Haskell 程序的典型分配速率约为 1GB/s。

    hp2ps 输出“114,272 字节 x 秒”的标头中的数字又完全不同:它是图形的积分,以字节 * 秒为单位,而不是字节。例如,如果您的程序在 10 MB 结构上保持 4 秒,那么这将导致该数字增加 40 MB*s。

    图中显示的 1.8 MB 左右的数字是 Haskell 堆的实际最大大小,这可能是您最感兴趣的数字。

    您省略了有关程序执行的最有用的数字来源,即使用+RTS -s 运行它(这甚至不需要使用分析构建它)。

    【讨论】:

      【解决方案2】:

      戴表的人知道现在几点。一个有两块手表的人永远都不确定。

      啊,但是两只手表代表什么?两者都意味着以UTC显示当前时间吗?还是其中一个应该显示 UTC 时间,另一个显示火星上某个点的时间?只要它们同步,第二种情况就没有问题了,对吧?

      这正是这里发生的事情。您比较不同的内存测量:

      • 最大居住时间
      • 分配的内存总量

      最大驻留是您的程序在给定时间使用的最大内存量。那是 19MB。但是,分配的内存总量要多得多,因为这就是 GHC 的工作方式:它为垃圾收集的对象“分配”内存,这几乎是所有未解包的对象。

      让我们检查一个 C 示例:

      int main() {
         int i;
         char * mem;
      
         for(i = 0; i < 5; ++i) {
            mem = malloc(19 * 1000 * 1000);
            free(mem);
         }
         return 0;
      }
      

      每当我们使用malloc 时,我们都会分配 19 兆字节的内存。但是,我们在之后立即释放内存。因此,我们曾经拥有的最大内存量是 19 兆字节(堆栈和程序本身还要多一点)。

      但是,我们总共分配了 5 * 19M,总共 95M。尽管如此,我们仍然可以用 20 兆的 RAM 运行我们的小程序。这就是总分配内存最大驻留之间的区别。请注意,按时间报告的驻留始终至少为du &lt;executable&gt;,因为它也必须驻留在内存中。

      话虽如此,生成统计数据的最简单方法是-s,它将显示从 Haskell 程序的角度来看最大驻留时间是多少。在您的情况下,它将是 1.9M,即您的堆配置文件中的数字(或由于配置文件而增加一倍)。是的,Haskell 可执行文件往往会变得非常大,因为库是静态链接的。

      【讨论】:

      • 感谢 Zeta 的回复。我还有一个问题。二进制文件大小为 63M。这是否意味着二进制文件没有以某种方式完全加载到内存中?
      • @Slabko:我目前没有 Unix 环境来检查 time -l 的语义,所以这可能不是 100% 正确的。但是,我还没有找到任何描述 -l 的实际手册页。你用的是哪个time
      • 我使用 Mac OS 10.11 和 time 的嵌入式版本。它使用 POSIX 的getrusage。有趣的是,getusage 以 kb 为单位返回大小,而 19070976 kb 是一个非常大的数字。我在 linux 上尝试了 GNU time;二进制大小为 48M,它仍然声称最大驻留集大小等于 32820Kb 但我认为,我的问题得到了很好的答案,现在我明白了应该如何读取 GHC 分析器的数字。我认为time 的工作方式与我对 Haskell 内存优化的理解无关。非常感谢您的帮助,非常感谢!
      • 整个可执行文件不一定需要在运行时加载。如果可执行文件有一个符号表,它可能比实际的代码和数据大。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-17
      • 2017-09-19
      • 1970-01-01
      • 2011-05-07
      相关资源
      最近更新 更多