2014-10-13 6 views
14

Nel processo di fare un semplice benchmarking, mi sono imbattuto in qualcosa che mi ha sorpreso. Prendete questo frammento da Network.Socket.Splice:Perché hGetBuf, hPutBuf, ecc. Allocano memoria?

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) 

Ci si aspetterebbe che hGetBufSome e hPutBuf qui non avrebbe bisogno di allocare la memoria, come scrivono in e leggere da un buffer di pre-assegnati. Il docs sembrano sostenere questa intuizione up ... Ma, ahimè:

         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

devo assumere questo è apposta ... ma non ho idea di quello che potrebbe essere quello scopo. Ancora peggio: sono appena abbastanza intelligente da ottenere questo profilo, ma non abbastanza intelligente da capire esattamente cosa viene assegnato.

Qualsiasi aiuto lungo quelle linee sarebbe apprezzato.


UPDATE: Ho fatto un po 'di profilatura con due casi di test drasticamente semplificati. Il primo testcase utilizza direttamente i ops di lettura/scrittura da 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 

Come ci si spera, questa alloca alcuna memoria sul mucchio ogni volta attraverso il ciclo. Il secondo testcase utilizza i ops di lettura/scrittura da 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: mi è stato consigliato di presentare questo come un bug in GHC Trac ... io non sono ancora sicuro che in realtà è un bug (al contrario di comportamento intenzionale, una limitazione nota, o qualsiasi altra cosa), ma qui è: https://ghc.haskell.org/trac/ghc/ticket/9696

+2

Non stai allocando memoria per creare 'byte'? –

+0

@GabrielGonzalez Io non la penso così ... Quando profilo con '+ RTS -hy', il tipo dominante nell'heap è' ARR_WORDS'. Il tipo di 'byte 'dovrebbe essere' Int' (il numero di byte letti). – mergeconflict

+2

@mergeconflict https://stackoverflow.com/questions/7241470/what-is-arr-words-in-a-ghc-heap-profile#7241686 afferma che ARR_WORDS corrisponde a ByteArray #. Non so perché venga assegnato così tanto, ma il bel plateau nel profilo indica che il programma gira in uno spazio costante. – cheecheeo

risposta

0

Sembra che la conclusione sia: it's a bug.

+0

Non è proprio un bug, solo un'opportunità persa per l'ottimizzazione. –

1

cercherò di indovinare in base alla code

Runtime tenta di ottimizzare le letture e le scritture di piccole dimensioni, quindi mantiene il buffer interno. Se il buffer è lungo 1 byte, sarà inefficiente utilizzarlo in modo diretto. Quindi il buffer interno viene utilizzato per leggere una porzione più grande di dati. Probabilmente è lungo ~ 32 Kb. Più qualcosa di simile per la scrittura. Più il tuo buffer.

Il codice ha un'ottimizzazione: se si fornisce un buffer più grande di quello interno e il successivo è vuoto, utilizzerà il buffer in modo diretto. Ma il buffer interno è già allocato, quindi non utilizzerà meno la memoria. Non so come disattivare il buffer interno, ma puoi aprire la richiesta di funzionalità se è importante per te.

(mi rendo conto che la mia ipotesi può essere totalmente sbagliata.)

ADD:

Questo uno non sembra per allocare, ma io ancora non so perché.

Qual è il problema, l'utilizzo massimo della memoria o il numero di byte allocati?

c_read è una funzione C, non assegna sul mucchio di haskell (ma può allocare su C mucchio.)

readRawBufferPtr è funzione Haskell, ed è usuale per funzioni Haskell di allocare molta memoria, che diventa rapidamente una spazzatura. Semplicemente per immutabilità. È comune che il programma haskell assegni e.g 100Gb mentre l'utilizzo della memoria è inferiore a 1Mb.

+0

La mia preoccupazione è il costo della CPU di allocazione e GC, non l'utilizzo massimo della memoria. Per esempio: scavando nell'implementazione di 'readRawBufferPtr', sembra che ci sia l'allocazione dell'heap nella chiamata a" throwErrnoIfMinus1RetryMayBlock'; se sto usando questo in un anello chiuso, voglio che questa chiamata spenda il minor numero possibile di cicli. Non penso che questo sia un esempio di allocazione a causa dell'immutabilità/puro stile funzionale. – mergeconflict

+1

@mergeconflict: Sembra che l'allocazione sia dovuta all'uso di funzioni di ordine superiore ('throwErrnoIfMinus1RetryMayBlock', come si menziona) che non vengono ottimizzate. Tuttavia, se si è preoccupati del costo di allocazione della CPU, si dovrebbe essere preoccupati del costo della CPU di tutti gli output GHC e non concentrarsi sull'allocazione semplicemente perché è facile da misurare. Un'allocazione è di due istruzioni: incrementa un puntatore e testalo contro il limite di heap. Un sacco di output di GHC è peggiore di quello, e dovrebbe essere tutto trascurabile rispetto a fare la chiamata di sistema ('leggi'). –

+0

@mergeconflict Quindi la preoccupazione è la prestazione? Probabilmente dovresti riflettere questo nella domanda corpo e titolo. E perché pensi che l'allocazione della memoria abbia qualcosa a che fare con le prestazioni nel tuo caso? Potrebbe essere il collo di bottiglia, ma questo richiede argomenti. – Yuras

Problemi correlati