Uma mudança na minha biblioteca tornou muito mais lenta. Perfil não está me ajudando. Qual poderia ser o motivo da desaceleração?

9

Meu problema, resumidamente

Eu fiz uma alteração na minha biblioteca, agora é muito mais lenta, mas não consigo descobrir onde ela gasta todo esse tempo adicional. Relatórios de criação de perfil não estão ajudando. Por favor, ajude-me a descobrir qual poderia ser o motivo.

Algum Contexto

Eu fiz uma biblioteca cliente do Redis chamada Hedis e tenho um programa de benchmark para isso. Agora, fiz algumas alterações internas na biblioteca para limpar a arquitetura. Isso fez com que o desempenho (em solicitações Redis por segundo, conforme medido pelo referido benchmark) caísse em um fator de cerca de 2,5.

O benchmark abre 50 conexões de rede a um servidor Redis no host local. As conexões são tratadas de maneira diferente entre as duas versões:

  • A versão rápida usa um thread por conexão (assim, o benchmark tem 50 threads em execução simultaneamente). Lê da tomada manipular usando unsafeInterleaveIO (eu descrevo minha abordagem em traços em um post do blog aqui . Eu estava um pouco infeliz com a arquitetura, por isso mudei as coisas para
  • a versão lenta que usa três threads por conexão. Eles se comunicam através de dois Chan s (150 threads em execução no benchmark).

Mais algumas informações que podem ser relevantes:

  • Compilado com o GHC 7.2.2.
  • O programa de benchmark é inalterado entre as duas versões, de modo que a rede o tráfego é o mesmo.
  • Ambas as versões usam o tempo de execução single-threaded (compilado sem -threaded ).
  • Todos os encadeamentos são criados chamando forkIO . Não o mais caro forkOS .

Resultados da criação de perfil

A criação de perfis não me dá uma razão clara para a queda no desempenho. De acordo com o relatório de criação de perfil, as duas versões gastam mais de 99% do tempo em System.IO.hFlush e Data.ByteString.hGetSome . O número de vezes que hFlush e hGetSome são chamados é o mesmo em ambas as versões. Como o tráfego de rede também é o mesmo em ambos os casos, essas funções não podem ser a razão da lentidão.

A única diferença significativa que posso medir entre as duas versões é o que time (o utilitário Unix) me diz: a versão lenta (com três vezes mais threads) gasta significativamente mais tempo em "sys" em vez de "usuário ", em comparação com a versão rápida. O flag GHC +RTS -s relata isso como produtividade reduzida.

Aqui estão as saídas do programa para ambas as versões com o sinalizador +RTS -s :

Referência da versão rápida

$ time ./dist/build/hedis-benchmark/hedis-benchmark +RTS -s -p
ping                   33305.29 Req/s
get                    25802.92 Req/s
mget                   18215.94 Req/s
ping (pipelined)      268994.36 Req/s
   5,118,163,904 bytes allocated in the heap
     185,075,608 bytes copied during GC
       4,084,384 bytes maximum residency (39 sample(s))
         916,544 bytes maximum slop
              10 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0      7416 colls,     0 par    0.38s    0.40s     0.0001s    0.0003s
  Gen  1        39 colls,     0 par    0.03s    0.03s     0.0007s    0.0009s

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time    7.93s  ( 12.34s elapsed)
  GC      time    0.41s  (  0.43s elapsed)
  RP      time    0.00s  (  0.00s elapsed)
  PROF    time    0.00s  (  0.00s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time    8.33s  ( 12.76s elapsed)

  %GC     time       4.9%  (3.3% elapsed)

  Alloc rate    645,587,554 bytes per MUT second

  Productivity  95.1% of total user, 62.1% of total elapsed


real    0m12.772s
user    0m8.334s
sys     0m4.424s

Referência da versão lenta

$ time ./dist/build/hedis-benchmark/hedis-benchmark +RTS -s -p
ping                   11457.83 Req/s
get                    11169.64 Req/s
mget                    8446.96 Req/s
ping (pipelined)      130114.31 Req/s
   6,053,055,680 bytes allocated in the heap
   1,184,574,408 bytes copied during GC
       9,750,264 bytes maximum residency (198 sample(s))
       2,872,280 bytes maximum slop
              26 MB total memory in use (0 MB lost due to fragmentation)

                                    Tot time (elapsed)  Avg pause  Max pause
  Gen  0      9105 colls,     0 par    2.11s    2.14s     0.0002s    0.0006s
  Gen  1       198 colls,     0 par    0.23s    0.24s     0.0012s    0.0093s

  INIT    time    0.00s  (  0.00s elapsed)
  MUT     time   10.99s  ( 27.92s elapsed)
  GC      time    2.34s  (  2.38s elapsed)
  RP      time    0.00s  (  0.00s elapsed)
  PROF    time    0.00s  (  0.00s elapsed)
  EXIT    time    0.00s  (  0.00s elapsed)
  Total   time   13.33s  ( 30.30s elapsed)

  %GC     time      17.6%  (7.8% elapsed)

  Alloc rate    550,656,490 bytes per MUT second

  Productivity  82.4% of total user, 36.3% of total elapsed


real    0m30.305s
user    0m13.333s
sys     0m16.964s

Você tem alguma idéia ou sugestão de onde esse tempo adicional pode vir?

    
por informatikr 31.01.2012 в 11:17
fonte

2 respostas

3

De acordo com o relatório de criação de perfil, a maior parte do tempo é gasto em hFlush e hGetSome . De acordo com time , a versão lenta leva muito mais tempo sys. Assim, minha hipótese é que muito tempo é gasto bloqueado e aguardando, seja aguardando mais entrada ou bloqueando e desbloqueando tópicos.

Aqui está a primeira coisa que eu faria: compilar o código com -threaded e ver o que acontece. O tempo de execução encadeado usa um gerenciador de E / S completamente diferente, e eu suspeito fortemente que essa única alteração irá resolver o seu problema.

    
por John L 01.02.2012 / 11:54
fonte
2

Meu palpite é que isso tem a ver com a sobrecarga de Chan .

Meu primeiro pensamento foi o aumento do tempo de GC, mas isso não parece ser o caso. Então, meu segundo pensamento é que talvez todo o bloqueio e desbloqueio envolvidos no uso de Chan (que é implementado em cima de MVar ) seja o problema. Mas isso ainda é apenas um palpite.

Você pode tentar TChan (ou seja, STM) e ver se isso faz a menor diferença. (Talvez você possa codificar um pequeno esqueleto apenas para comparar os dois e ver se é onde está o problema, em vez de reimplementar seu código "real").

Fora isso, estou sem ideias.

    
por MathematicalOrchid 31.01.2012 / 12:03
fonte