【问题标题】:Why do hGetBuf, hPutBuf, etc. allocate memory?hGetBuf、hPutBuf等为什么要分配内存?
【发布时间】:2014-12-07 15:41:04
【问题描述】:

在做一些简单的基准测试的过程中,我遇到了一些令我惊讶的事情。从Network.Socket.Splice 获取这个sn-p:

hSplice :: Int -> Handle -> Handle -> IO ()
hSplice len s t = do
  a <- mallocBytes len :: IO (Ptr Word8)
  finally
    (forever $! do
       bytes <- hGetBufSome s a len
       if bytes > 0
         then hPutBuf t a bytes
         else throwRecv0)
    (free a)

人们会期望这里的hGetBufSomehPutBuf 不需要分配内存,因为它们写入和读取预先分配的缓冲区。 docs 似乎支持这种直觉......但是唉:

                                        individual     inherited
COST CENTRE                            %time %alloc   %time %alloc      bytes

 hSplice                                 0.5    0.0    38.1   61.1       3792
  hPutBuf                                0.4    1.0    19.8   29.9   12800000
   hPutBuf'                              0.4    0.4    19.4   28.9    4800000
    wantWritableHandle                   0.1    0.1    19.0   28.5    1600000
     wantWritableHandle'                 0.0    0.0    18.9   28.4          0
      withHandle_'                       0.0    0.1    18.9   28.4    1600000
       withHandle'                       1.0    3.8    18.8   28.3   48800000
        do_operation                     1.1    3.4    17.8   24.5   44000000
         withHandle_'.\                  0.3    1.1    16.7   21.0   14400000
          checkWritableHandle            0.1    0.2    16.4   19.9    3200000
           hPutBuf'.\                    1.1    3.3    16.3   19.7   42400000
            flushWriteBuffer             0.7    1.4    12.1    6.2   17600000
             flushByteWriteBuffer       11.3    4.8    11.3    4.8   61600000
            bufWrite                     1.7    6.9     3.0    9.9   88000000
             copyToRawBuffer             0.1    0.2     1.2    2.8    3200000
              withRawBuffer              0.3    0.8     1.2    2.6   10400000
               copyToRawBuffer.\         0.9    1.7     0.9    1.7   22400000
             debugIO                     0.1    0.2     0.1    0.2    3200000
            debugIO                      0.1    0.2     0.1    0.2    3200016
  hGetBufSome                            0.0    0.0    17.7   31.2         80
   wantReadableHandle_                   0.0    0.0    17.7   31.2         32
    wantReadableHandle'                  0.0    0.0    17.7   31.2          0
     withHandle_'                        0.0    0.0    17.7   31.2         32
      withHandle'                        1.6    2.4    17.7   31.2   30400976
       do_operation                      0.4    2.4    16.1   28.8   30400880
        withHandle_'.\                   0.5    1.1    15.8   26.4   14400288
         checkReadableHandle             0.1    0.4    15.3   25.3    4800096
          hGetBufSome.\                  8.7   14.8    15.2   24.9  190153648
           bufReadNBNonEmpty             2.6    4.4     6.1    8.0   56800000
            bufReadNBNonEmpty.buf'       0.0    0.4     0.0    0.4    5600000
            bufReadNBNonEmpty.so_far'    0.2    0.1     0.2    0.1    1600000
            bufReadNBNonEmpty.remaining  0.2    0.1     0.2    0.1    1600000
            copyFromRawBuffer            0.1    0.2     2.9    2.8    3200000
             withRawBuffer               1.0    0.8     2.8    2.6   10400000
              copyFromRawBuffer.\        1.8    1.7     1.8    1.7   22400000
            bufReadNBNonEmpty.avail      0.2    0.1     0.2    0.1    1600000
           flushCharReadBuffer           0.3    2.1     0.3    2.1   26400528

我不得不假设这是故意的……但我不知道那个目的可能是什么。更糟糕的是:我只是勉强能得到这个配置文件,但还不够聪明,无法准确地弄清楚分配了什么。

我们将不胜感激。


更新:我用两个大大简化的测试用例做了更多的分析。第一个测试用例直接使用来自System.Posix.Internals的读/写操作:

echo :: Ptr Word8 -> IO ()
echo buf = forever $ do
  threadWaitRead $ Fd 0
  len <- c_read 0 buf 1
  c_write 1 buf (fromIntegral len)
  yield

正如您所希望的,每次循环都不会在堆上分配内存。第二个测试用例使用来自GHC.IO.FD 的读/写操作:

echo :: Ptr Word8 -> IO ()
echo buf = forever $ do
  len <- readRawBufferPtr "read" stdin buf 0 1
  writeRawBufferPtr "write" stdout buf 0 (fromIntegral len)

更新 #2: 有人建议我将此作为 GHC Trac 中的错误提交...我仍然不确定它实际上 是否是一个错误(相反故意行为、已知限制或其他)但这里是:https://ghc.haskell.org/trac/ghc/ticket/9696

【问题讨论】:

  • 你不是分配内存来创建bytes吗?
  • @GabrielGonzalez 我不这么认为...当我使用+RTS -hy 进行概要分析时,堆上的主要类型是ARR_WORDSbytes 的类型应该是Int(读取的字节数)。
  • @mergeconflict stackoverflow.com/questions/7241470/… 声明 ARR_WORDS 对应于 ByteArray#。我不知道为什么要分配这么多,但配置文件中漂亮的平台表明程序在恒定空间中运行。
  • @cheecheeo 是的,没错。我没有空间泄漏;相反,似乎每次循环都在分配和释放一个或多个小缓冲区。
  • 您是否分析过执行其他操作(可能是无操作)的代码并验证您获得了不同的行为?

标签: haskell io profiling


【解决方案1】:

我会根据code尝试猜测

运行时尝试优化小型读取和写入,因此它维护内部缓冲区。如果你的缓冲区是 1 字节长,那么直接使用它是低效的。因此内部缓冲区用于读取更大的数据块。它可能长约 32Kb。加上类似的写作。加上你自己的缓冲区。

代码有一个优化——如果你提供的缓冲区比内部缓冲区大,而后者是空的,它将直接使用你的缓冲区。但是内部缓冲区已经分配了,所以它不会减少内存使用。我不知道如何禁用内部缓冲区,但如果它对您很重要,您可以打开功能请求。

(我意识到我的猜测可能完全错误。)

添加:

这个好像确实分配了,但是我还是不知道为什么。

您关心的是最大内存使用量还是分配的字节数?

c_read是一个C函数,它不在haskell的堆上分配(但可能在C堆上分配。)

readRawBufferPtr 是 Haskell 函数,haskell 函数通常会分配大量内存,这很快就会变成垃圾。仅仅因为不变性。 haskell 程序在内存使用量低于 1Mb 时分配例如 100Gb 是很常见的。

【讨论】:

  • 我关心的是分配和 GC 的 CPU 成本,而不是最大内存使用量。例如:深入研究readRawBufferPtr 的实现,似乎在对throwErrnoIfMinus1RetryMayBlock 的调用中发生了堆分配;如果我在紧密的内部循环中使用它,我希望这个调用花费尽可能少的周期。由于不变性/纯函数式风格,我不认为这是分配示例。
  • @mergeconflict:看起来分配是由于使用了没有得到优化的高阶函数(throwErrnoIfMinus1RetryMayBlock,正如你提到的)。但是,如果您关心分配的 CPU 成本,那么您应该关注 GHC 输出的所有 CPU 成本,而不是仅仅因为它易于衡量而关注分配。分配是两条指令:增加一个指针并根据堆限制对其进行测试。许多 GHC 的输出比这更糟糕,与执行系统调用 (read) 相比,它应该可以忽略不计。
  • @mergeconflict 所以关注的是性能?您可能应该在问题正文和标题中反映这一点。为什么您认为内存分配与您的情况下的性能有关?这可能是瓶颈,但这需要争论。
【解决方案2】:

似乎结论是:it's a bug

【讨论】:

  • 这不是真正的错误,只是错过了优化的机会。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-04-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-08
相关资源
最近更新 更多