在做一些简单的基准测试的过程中,我遇到了让我感到惊讶的事情。从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)
可以预期hGetBufSome
和hPutBuf
这里不需要分配内存,因为它们写入预读缓冲区并从中读取。 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
答案 0 :(得分:1)
我会根据code
尝试猜测运行时尝试优化小的读写操作,因此它维护内部缓冲区。如果您的缓冲区长度为1个字节,则直接使用它将是低效的。因此内部缓冲区用于读取更大的数据块。它可能长约32Kb。加上类似的写作。加上你自己的缓冲区。
代码有一个优化 - 如果你提供的缓冲区大于内部缓冲区,而后者是空的,它将直接使用你的缓冲区。但内部缓冲区已经分配,因此内存使用量不会减少。我不知道如何禁用内部缓冲区,但是如果它对您很重要,您可以打开功能请求。
(我意识到我的猜测完全错了。)
添加强>
这个似乎确实分配了,但我仍然不知道为什么。
您关注的是什么,最大内存使用量或分配的字节数?
c_read
是一个C函数,它不会在haskell的堆上分配(但可以在C堆上分配。)
readRawBufferPtr
是Haskell函数,并且haskell函数通常会分配大量内存,很快就会变成垃圾。仅仅因为不变性。当内存使用量低于1Mb时,haskell程序通常会分配100Gb。
答案 1 :(得分:0)
结论似乎是:it's a bug。