2014-10-13 2 views
14

В процессе выполнения некоторых простых бенчмаркинга я наткнулся на то, что меня удивило. Возьмите этот отрывок из Network.Socket.Splice:Почему hGetBuf, hPutBuf и т. Д. Выделяют память?

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 module heap profile by type

я должен предположить, что это нарочно ... но я понятия не имею, что может быть эта цель. Еще хуже: я просто достаточно умен, чтобы получить этот профиль, но не достаточно умный, чтобы точно определить, что выделяется.

Любая помощь по этим линиям будет оценена по достоинству.


UPDATE: Я сделал еще несколько профилирование с двумя резко упрощенных testcases. Первый TestCase непосредственно использует опа чтения/записи из 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 

Как вы бы надеяться, это не выделяет никакой памяти на куче каждый раз через петлю. Второй TestCase использует опа чтения/записи из 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) 

UPDATE # 2: мне посоветовали подать это как ошибка в GHC Trac ... Я до сих пор не уверен, что на самом деле является об ошибке (в отличие от преднамеренного поведения, известное ограничение, или какой-то), но здесь это: https://ghc.haskell.org/trac/ghc/ticket/9696

+2

Разве вы не выделяете память для создания 'bytes'? –

+0

@GabrielGonzalez Я так не думаю ... Когда я профилю с '+ RTS -hy', доминирующим типом в куче является' ARR_WORDS'. Тип 'bytes' должен быть' Int' (количество прочитанных байтов). – mergeconflict

+2

@mergeconflict https://stackoverflow.com/questions/7241470/what-is-arr-words-in-a-ghc-heap-profile#7241686 утверждает, что ARR_WORDS соответствует ByteArray #. Я не знаю, почему так много выделяется, но симпатичное плато в профиле указывает, что программа работает в постоянном пространстве. – cheecheeo

ответ

0

Похоже, что вывод: it's a bug.

+0

На самом деле это не ошибка, просто упущенная возможность для оптимизации. –

1

попробую угадать на основе code

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

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

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

ADD:

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

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

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

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

+0

Моя забота - это стоимость ЦП для распределения и GC, а не максимальное использование памяти. Например: копаясь в реализации 'readRawBufferPtr', похоже, что в вызове' throwErrnoIfMinus1RetryMayBlock' происходит распределение кучи; если я использую это в жесткой внутренней петле, я хочу, чтобы этот вызов тратил как можно меньше циклов. Я не думаю, что это пример распределения из-за неизменности/чистого функционального стиля. – mergeconflict

+1

@mergeconflict: похоже, что распределение связано с использованием функций более высокого порядка ('throwErrnoIfMinus1RetryMayBlock', как вы упоминаете), которые не оптимизируются. Однако, если вас беспокоит стоимость распределения ЦП, тогда вы должны быть обеспокоены стоимостью ЦП всех выходов GHC и не фокусироваться на распределении просто потому, что его легко измерить. Выделение - две инструкции: увеличивайте указатель и проверяйте его на ограничение кучи. Многие результаты GHC хуже, чем это, и все это должно быть незначительным по сравнению с выполнением системного вызова ('read'). –

+0

@mergeconflict Итак, проблема в производительности? Вероятно, вы должны отразить это в теле вопроса и названии. И почему вы считаете, что распределение памяти имеет какое-либо отношение к производительности в вашем случае? Это может быть узким местом, но это требует аргументов. – Yuras

Смежные вопросы