Почему hGetBuf, hPutBuf и т.д. Выделяют память?

В процессе выполнения простого бенчмаркинга я наткнулся на то, что меня удивило. Возьмите этот фрагмент из 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

heap profile by moduleheap 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

Я попытаюсь угадать, основываясь на code

Время выполнения пытается оптимизировать небольшие чтения и записи, поэтому он поддерживает внутренний буфер. Если ваш буфер длится 1 байт, он будет неэффективным, чтобы использовать его по-разному. Таким образом, внутренний буфер используется для чтения большего количества данных. Это, вероятно, ~ 32 КБ. Плюс что-то подобное для написания. Плюс ваш собственный буфер.

В коде есть оптимизация - если вы указываете буфер больше, чем внутренний, а затем пуст, он будет использовать ваш буфер правильно. Но внутренний буфер уже выделен, поэтому он будет не меньше использования памяти. Я не знаю, как распустить внутренний буфер, но вы можете открыть запрос функции, если это важно для вас.

(Я понимаю, что моя догадка может быть абсолютно неправильной.)

ADD:

Этот, кажется, выделяет, но я до сих пор не знаю почему.

В чем вы нуждаетесь, максимальное использование памяти или количество выделенных байтов?

c_read - это функция C, она не выделяется на кучу haskell (но может выделяться на кучу C).

readRawBufferPtr является функцией Haskell, и обычно для функций haskell выделяется много памяти, что быстро становится мусором. Просто из-за неизменности. Обычно программа haskell выделяет, например, 100 ГБ, в то время как использование памяти составляет менее 1 МБ.