为什么hGetBuf,hPutBuf等会分配内存?

时间:2014-10-13 06:02:05

标签: haskell io profiling

在做一些简单的基准测试的过程中,我遇到了让我感到惊讶的事情。从Network.Socket.Splice获取此代码段:

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

heap profile by module heap profile by type

我必须假设这是故意的...但我不知道这个目的是什么。更糟糕的是:我只是勉强聪明才能获得这个档案,但还不够聪明,无法弄清楚究竟要分配的是什么。

这些方面的任何帮助都将受到赞赏。


更新:我已经使用两个大大简化的测试用例进行了更多分析。第一个测试用例直接使用来自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

2 个答案:

答案 0 :(得分:1)

我会根据code

尝试猜测

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

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

(我意识到我的猜测完全错了。)

添加

  

这个似乎确实分配了,但我仍然不知道为什么。

您关注的是什么,最大内存使用量或分配的字节数?

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

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

答案 1 :(得分:0)

结论似乎是:it's a bug