This is the V0.1 (#62)

* This is the V0.1

Need some grammar corrections, but I think that's a good first pt-br version!

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

* Update performance-ptbr.md

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>

Co-authored-by: Daniel Fireman <danielfireman@gmail.com>
This commit is contained in:
Marcus Tenorio 2020-07-28 15:31:25 -03:00 committed by Damian Gryski
parent 7d0a251d80
commit cbae90105d

View File

@ -462,3 +462,545 @@ Use média geométrica para comparar grupos de benchmarks.
Avaliando a precisão do benchmark:
* <http://www.brendangregg.com/blog/2018-06-30/benchmarking-checklist.html>
### Melhoria do programa.
A melhoria do programa costumava ser uma forma de arte, mas os compiladores ficaram melhores. Portanto, agora os compiladores podem otimizar o código melhor do que complicar aquele código.
O compilador Go ainda tem um longo caminho a percorrer para corresponder ao gcc e ao clang, mas isso significa que você precisa
ter cuidado ao ajustar e principalmente ao atualizar as versões de Go para que seu código não se torne "pior".
Definitivamente, há casos em que os ajustes para solucionar a falta de uma otimização específica do compilador
façam o código se tornar mais lento depois que o compilador foi aprimorado.
Minha implementação de cifra RC6 teve um aumento de velocidade de 10% para o loop interno apenas mudando para `encoding / binary` e` math / bits` em vez de usar uma de minhas versões feitas manualmente.
Da mesma forma, o pacote `compress / bzip2` foi acelerado ao mudar para [código mais simples que o compilador conseguiu otimizar] (https://github.com/golang/go/commit/9eb219480e8de08d380ee052b7bff293856955f8)
Se você estiver trabalhando em torno de um tempo de execução específico ou geração de código do compilador sempre documente sua alteração com um link para a edição anterior. Este link permitirá que você revisite rapidamente sua otimização depois que o bug for corrigido.
Lute contra a tentação de "dicas de desempenho" baseadas no folclore cult, ou até
generalizar demais a partir de sua própria experiência. Cada bug de desempenho precisa ser
abordado por seus próprios méritos. Mesmo se algo funcionou anteriormente, certifique-se de criar um perfil para garantir que a correção ainda seja aplicável. Seu trabalho pode orientá-lo, mas não aplique otimizações anteriores às cegas.
A melhoria do programa é um processo iterativo. Continue revisitando seu código e vendo
que mudanças podem ser feitas. Verifique se você está progredindo em cada etapa.
Freqüentemente, uma melhoria permitirá que outras sejam feitas. (Agora que eu não estou
fazendo A, eu posso simplificar B fazendo C em vez disso). Isso significa que você precisa manter-se
olhando para a foto inteira e não fique muito obcecado com um pequeno conjunto de
linhas.
Depois de escolher o algoritmo certo, a melhoria do programa é o processo de
melhorar a implementação desse algoritmo. Na notação Grande-O, isso é
o processo de redução das constantes associadas ao seu programa.
Toda a melhoria no programa ou vai tornar mais rápido algo que era mais lento ou fazer algo que é lento uma menor quantidade de vezes.
As mudanças algorítmicas também se enquadram nessas categorias, mas veremos mudanças menores. Exatamente como você faz isso varia conforme as tecnologias mudam.
Como exemplo, fazer uma coisa lenta rapidamente pode ser substituir SHA1 ou `hash/fnv1` por uma função hash mais rápida. Fazer um processo lento menos vezes pode ser salvar o resultado do cálculo de hash de um arquivo grande para que você não precise fazer isso várias vezes.
Mantenha comentários. Se algo não precisar ser feito, explique o porquê. Freqüentemente, ao otimizar um algoritmo, você descobrirá etapas que não precisam ser executadas sob algumas circunstâncias. Documente-as. Outra pessoa pode pensar que é um bug.
> Programas vazios dão a resposta errada em pouco tempo.
>
> É fácil ser rápido se você não precisa estar correto.
A "correção" pode depender do problema. Os algoritmos heurísticos mais corretos na maioria das vezes podem ser rápidos, assim como os algoritmos que adivinham e melhoram, permitindo que você pare quando atingir um limite aceitável.
Casos comuns de cache:
Todos conhecemos o memcache, mas também existem caches em processo. O uso de um cache em processo economiza o custo da chamada de rede e o custo da serialização. Por outro lado, isso aumenta a pressão do GC, pois há mais memória para acompanhar. Você também precisa considerar estratégias de remoção, invalidação de cache e segurança de threads.
Um cache externo geralmente lida com o despejo, mas a invalidação do cache permanece um problema. As condições de corrida também podem ser um problema com caches externos, pois se torna um estado mutável efetivamente compartilhado entre goroutines diferentes no mesmo serviço ou até diferentes instâncias de serviço se o cache externo for compartilhado.
Um cache salva as informações que você acabou de gastar em tempo de computação, na esperança de poder reutilizá-las novamente em breve e economizar tempo. Um cache não precisa ser complexo. Mesmo o armazenamento de um único item - a consulta / resposta vista mais recentemente - pode ser uma grande vitória, como visto no exemplo `time.Parse ()` abaixo.
Com caches, é importante comparar o custo (em termos da complexidade real do relógio e do código) da sua lógica de cache para simplesmente buscar ou recalcular os dados.
Os algoritmos mais complexos que oferecem taxas de acerto mais altas geralmente não são baratos. A remoção aleatória de cache é simples e rápida e pode ser eficaz em muitos
casos. Da mesma forma, a inserção aleatória * de cache * pode limitar seu cache apenas a itens populares com lógica mínima. Embora estes possam não ser tão eficazes quanto os
algoritmos mais complexos, a grande melhoria será adicionar um cache em primeiro lugar: escolher exatamente qual algoritmo de cache oferece apenas pequenas melhorias.
É importante avaliar sua escolha do algoritmo de remoção de cache com rastreamentos do mundo real. Se, no mundo real, as solicitações repetidas são suficientemente raras, pode
ser mais caro manter as respostas em cache do que simplesmente recalculá-las quando necessário. Eu tive serviços em que o teste com dados de produção mostrou que nem um cache ideal valeu a pena. simplesmente não tivemos solicitações repetidas suficientes para fazer sentido a complexidade adicional de um cache.
A taxa de acertos esperados do cache é importante. Você deseja exportar a proporção para sua pilha de monitoramento. A alteração das taxas mostrará uma mudança no tráfego.
Chegou a hora de revisitar o tamanho do cache ou a política de expiração.
Um cache grande pode aumentar a pressão do GC. No extremo (pouca ou nenhuma remoção, cache de todas as solicitações para uma função cara), isso pode se transformar em [memorização] (https://en.wikipedia.org/wiki/Memoization)
Melhoria do programa:
Melhoria do programa é a arte de melhorar iterativamente um programa em pequenas etapas.
Egon Elbre apresenta seu procedimento:
* Crie uma hipótese de por que seu programa é lento.
* Crie N soluções para resolvê-lo
* Experimente todos e mantenha o mais rápido.
* Mantenha o segundo mais rápido por precaução.
* Repetir.
As afinações podem assumir várias formas.
* Se possível, mantenha a antiga implementação para teste.
* Se não for possível, gere casos de teste de ouro suficientes para comparar a saída.
* "Suficiente" significa incluir casos extremos, pois esses são os que provavelmente serão afetados pelo ajuste
com o objetivo de melhorar o desempenho no caso geral.
* Explorar uma identidade matemática:
* Observe que implementar e otimizar cálculos numéricos é quase seu próprio campo
* <https://github.com/golang/go/commit/ed6c6c9c11496ed8e458f6e0731103126ce60223>
* <https://gist.github.com/dgryski/67e6a7ff94c3a1add30eb26ec0ad8b0f>
* multiplicação com adição
* use WolframAlpha, Maxima, sympy e similares para especializar, otimizar ou criar tabelas de pesquisa
* (Além disso, https://users.ece.cmu.edu/~franzf/papers/gttse07.pdf)
* movendo-se da matemática do ponto flutuante para a matemática inteira
* ou mandelbrot removendo sqrt ou lttb removendo abs, `a <b / c` =>` a * c <b`
* considere diferentes representações numéricas: ponto fixo, ponto flutuante, números inteiros (menores),
* extravagante: números inteiros com acumuladores de erro (por exemplo, linha e círculo de Bresenham), números com várias bases / sistemas de números redundantes
* "pague apenas pelo que você usa, não pelo que você poderia ter usado"
* zero apenas parte de uma matriz, em vez da coisa toda
* melhor feito em pequenas etapas, algumas declarações de cada vez
* checagens baratas antes de checagens caras:
* por exemplo, strcmp antes da regexp, (q.v., filtro de bloom antes da consulta)
"faça coisas caras menos vezes"
* casos comuns antes de casos raros
ou seja, evite testes extras que sempre falham
* desenrolar ainda eficaz: https://play.golang.org/p/6tnySwNxG6O
* tamanho do código. sobrecarga de teste de ramificação vs
* usar deslocamentos em vez de atribuição de fatia pode ajudar com verificações de limites, dependências de dados e geração de código (menos para copiar no loop interno).
* remova verificações de limites e verificações nulas dos loops: https://go-review.googlesource.com/c/go/+/151158
* outros truques para o passe de prova
* é aqui que pedaços de prazer para um hacker caem
Muitas dicas de desempenho do folclore para ajuste dependem de compiladores de otimização insuficiente e incentivam o programador a fazer essas transformações manualmente. Os
compiladores usam deslocamentos de bits em vez de multiplicar ou dividir por uma potência de dois há 15 anos - ninguém deve fazer isso manualmente. Da mesma forma, içar
cálculos invariantes a partir de loops, desenrolamento básico do loop, eliminação de subexpressão e muitos outros são feitos automaticamente pelo gcc e clang e similares. O compilador da Go faz muitas delas e continua a melhorar. Como sempre, faça uma referência antes de se comprometer com a nova versão.
As transformações que o compilador não pode fazer dependem de você saber coisas sobre o algoritmo,
seus dados de entrada, invariantes em seu sistema e outras suposições que você pode fazer,
além de levar em consideração esse
conhecimento implícito para remover ou alterar as etapas na estrutura de dados.
Toda otimização codifica uma suposição sobre seus dados.
Estes *devem* ser documentados e, melhor ainda, testados.
Essas suposições serão onde o seu programa falha, diminui a velocidade
ou começa a retornar dados incorretos à medida que o sistema evolui.
As melhorias no ajuste do programa são cumulativas. 5x 3% de melhorias são 15% de melhoria.
Ao fazer otimizações, vale a pena pensar na melhoria de desempenho esperada.
Substituir uma função de hash por uma mais rápida é uma constante melhoria do fator.
Compreender seus requisitos e onde eles podem ser alterados pode levar a melhorias de desempenho.
Um problema apresentado no canal \#performance Gophers Slack foi a quantidade de tempo gasto na criação de um identificador exclusivo para um mapa de pares de chave/valor de
string. A solução original era extrair as chaves, classificá-las e passar a sequência resultante para uma função hash. A solução melhorada que encontramos foi o hash individual
das chaves/valores à medida que foram adicionados ao mapa, e então xor todos esses hashes juntos para criar o identificador.
Aqui está um exemplo de especialização.
Digamos que estamos processando um grande arquivo de log por um único dia,
e cada linha começa com um carimbo de hora.
```
Sun 4 Mar 2018 14:35:09 PST <...........................>
```
Para cada linha, nós iremos chamar `time.Parse()` para transformá-la em época. Se
o perfilamento nos mostra que `time.Parse()` é um gargalo, nós temos algumas opções para
speed things up.
A mais fácil é manter um cache contendo uma única linha, o tempo anterior e a época associada. Enquanto o nosso arquivo de log tiver múltiplas linhas para um mesmo segundo, teremos uma vitória. Para o caso de um arquivo de log com 10 milhões de linhas, essa estratégia reduz o número de chamadas caras `time.Parse()` de 10.000.000 to 86.400 -- uma para cada segundo único.
TODO: exemplo de código para cache de item único
Podemos fazer mais? Como sabemos exatamente em que formato os carimbos de data e hora estão *e* em que todos caem em um único dia, podemos escrever uma lógica de análise de
tempo personalizada que leva isso em consideração. Podemos calcular a época da meia-noite, extrair horas, minutos e segundos da sequência do carimbo de data/hora - todos eles
estarão em desvios fixos na sequência - e fazer algumas contas inteiras.
TODO: exemplo de código para versão de deslocamento de string
Nas minhas avaliações, isso reduziu o tempo de análise de 275ns/op para 5ns/op. (Obviamente, mesmo com 275 ns/op, é mais provável que você seja bloqueado na E/S e não na CPU para analisar o tempo.)
O algoritmo geral é lento porque precisa lidar com mais casos. Seu algoritmo pode ser mais rápido porque você sabe mais sobre o seu problema. Mas o código está mais intimamente
ligado ao que você precisa. É muito mais difícil atualizar se o formato da hora mudar.
Otimização é especialização, e códigos especializados são mais frágeis de serem alterados que códigos de uso geral.
As implementações da biblioteca padrão precisam ser "rápidas o suficiente" para a maioria dos casos. Se você tiver necessidades de desempenho mais altas, provavelmente
precisará de implementações especializadas.
Faça um perfil regularmente para garantir o rastreamento das características de desempenho do seu sistema e esteja preparado para otimizar novamente conforme o tráfego muda.
Conheça os limites do seu sistema e tenha boas métricas que permitem prever quando você atingirá esses limites.
Quando o uso do aplicativo é alterado, diferentes partes podem se tornar pontos de acesso. Revise as otimizações anteriores e decida se ainda valem a pena e volte para um
código mais legível, quando possível. Eu tinha um sistema que otimizava o tempo de inicialização do processo com um conjunto complexo de mmap, refletir e inseguro. Depois que
alteramos a maneira como o sistema foi implantado, esse código não era mais necessário e eu o substituí por operações regulares de arquivos muito mais legíveis.
### Resumo do fluxo de trabalho de otimização ( Ou melhoria de perfomance!)
Todas as otimizações devem seguir estas etapas:
1. determine seus objetivos de desempenho e confirme que não os está atingindo
1. Avalie para identificar as áreas a serem melhoradas.
* Pode ser CPU, alocações de heap ou bloqueio de goroutine.
1. Avaliação para determinar a velocidade que sua solução fornecerá usando
a estrutura de benchmarking integrada (<http://golang.org/pkg/testing/>)
* Verifique se você está avaliando a coisa certa em seu alvo
sistema operacional e arquitetura.
1. Avalie novamente depois para verificar se o problema desapareceu
1. use <https://godoc.org/golang.org/x/perf/benchstat> ou
<https://github.com/codahale/tinystat> para verificar se um conjunto de checagens
são 'suficientemente' diferentes para que uma otimização valha o acréscimo
complexidade do código.
1. use <https://github.com/tsenart/vegeta> para carregar serviços http de teste
(+ outros extravagantes: k6, fortio, fbender)
- se possível, teste a aceleração / desaceleração além da carga em estado estacionário
1. verifique se seus números de latência fazem sentido
TODO: mencione github.com/aclements/perflock como ferramenta de redução de ruído da CPU
O primeiro passo é importante. Informa quando e onde começar a otimizar.
Mais importante, ele também informa quando parar. Praticamente todas as otimizações
adicione complexidade de código em troca de velocidade. E você pode *sempre* criar código
Mais rápido. É um ato de equilíbrio.
## Garbage Collection
Você paga pela alocação de memória mais de uma vez. O primeiro é obviamente quando você o aloca. Mas você também paga sempre que o garbage collector é executado.
> Reduce/Reuse/Recycle.
> -- <cite>@bboreham</cite>
* Alocações de pilha vs. heap
* O que causa alocações de heap?
* Compreendendo a análise de escape (e a limitação atual)
* / debug / pprof / heap e -base
* Design da API para limitar alocações:
* permitir a passagem de buffers para que o chamador possa reutilizar em vez de forçar uma alocação
* você pode até modificar uma fatia no lugar com cuidado enquanto a digitaliza
* passar uma estrutura pode permitir que o chamador empilhe a alocação
* redução de ponteiros para reduzir o tempo de verificação do gc
* fatias sem ponteiro
* mapas com chaves e valores sem ponteiro
* GOGC
* reutilização de buffer (sync.Pool vs ou personalizado via go-slab, etc)
* fatiamento x deslocamento: as gravações do ponteiro enquanto o GC está sendo executado precisam de uma barreira de gravação: https://github.com/golang/go/commit/b85433975aedc2be2971093b6bbb0a7dc264c8fd
* nenhuma barreira de gravação se estiver gravando na pilha https://github.com/golang/go/commit/2140975ebde164ea1eaa70fc72775c03567f2bc9
* use variáveis de erro em vez de errors.New () / fmt.Errorf () no site de chamada (desempenho ou estilo? a interface requer ponteiro, para que ele escape para a pilha de qualquer maneira)
* use erros estruturados para reduzir a alocação (passe o valor da estrutura), crie uma string no momento da impressão de erro
* classes de tamanho
* cuidado ao fixar alocação maior com substrings ou fatias menores
## Tempo de execução e compilador
* custo de chamadas via interfaces (chamadas indiretas no nível da CPU)
* runtime.convT2E / runtime.convT2I
* asserções de tipo vs. comutadores de tipo
* defer
* implementações de mapas de casos especiais para ints, strings
* mapa para byte / uint16 não otimizado; use uma fatia.
* Você pode falsificar um float64 otimizado com math.Float {32,64} {from,} bits, mas cuidado com os problemas de igualdade de float
* https://github.com/dgryski/go-gk/blob/master/exact.go diz 100x mais rápido; precisa de benchmarks
* eliminação de verificação de limites
* [] byte <-> cópias de string, otimizações de mapa
* o intervalo de dois valores copiará uma matriz, use a fatia:
* <https://play.golang.org/p/4b181zkB1O>
* <https://github.com/mdempsky/rangerdanger>
* use concatenação de strings em vez de fmt.Sprintf sempre que possível; tempo de execução otimizou rotinas para ele
## Unsafe
* E todos os perigos que a acompanham
* Usos comuns para inseguros
* mmap'ing arquivos de dados
* struct padding
* mas nem sempre suficientemente rápido para justificar o custo de complexidade / segurança
* mas "fora da pilha", tão ignorado pelo gc (mas uma fatia sem ponteiros)
* precisa pensar no formato de serialização: como lidar com ponteiros, indexação (mph, cabeçalho do índice)
* desserialização rápida
* protocolo de conexão binária para estruturar quando você já possui o buffer
* string <-> conversão de fatia, [] byte <-> [] uint32, ...
* int para boolear hack inseguro (mas cmov) (mas! = 0 também é livre de ramificação)
* preenchimento:
- https://dave.cheney.net/2015/10/09/padding-is-hard
- http://www.catb.org/esr/structure-packing/#_go_and_rust
- https://golang.org/ref/spec#Size_and_alignment_guarantees
- https://github.com/dominikh/go-tools structlayout, structlayout-optimize
- escreva testes para o layout da estrutura com inseguro. Offsetof para perceber quebras de inseguras ou asm
## Dicas comuns com a biblioteca padrão
* time.After () vaza até disparar; use t: = NewTimer (); t.Stop () / t.Reset ()
* Reutilizando conexões HTTP ...; garantir que o corpo seja drenado (questão nº?)
* rand.Int () e seus amigos são 1) protegidos por mutex e 2) caros para criar
* considere a geração alternativa de números aleatórios (go-pcgr, xorshift)
* binary.Read e binary.Write usam reflexão e são lentos; faça à mão. (https://github.com/conformal/yubikey/commit/613e3b04ae2eeb78e6a19636b8ff8e9106d2e7bc)
* use strconv em vez de fmt, se possível
* Use `strings.EqualFold (str1, str2)` em vez de `strings.ToLower (str1) == strings.ToLower (str2)` ou `strings.ToUpper (str1) == strings.ToUpper (str2)` para comparar eficientemente cordas, se possível.
* ...
## Implementações alternativas
Substituições populares para pacotes de bibliotecas padrão:
* codificação / json -> [ffjson] (https://github.com/pquerna/ffjson), [easyjson] (https://github.com/mailru/easyjson), [jingo] (https: // github. com / bet365 / jingo) (apenas codificador), etc
* net / http
* [fasthttp] (https://github.com/valyala/fasthttp/) (mas API incompatível, não compatível com RFC de maneiras sutis)
* [activationprouter] (https://github.com/julienschmidt/httprouter) (tem outros recursos além da velocidade; nunca vi roteamento nos meus perfis)
* regexp -> [ragel] (https://www.colm.net/open-source/ragel/) (ou outro pacote de expressões regulares)
* serialização
* encoding / gob -> <https://github.com/alecthomas/go_serialization_benchmarks>
* protobuf -> <https://github.com/gogo/protobuf>
* todos os formatos de serialização têm vantagens: escolha um que corresponda ao que você precisa
- Grave carga de trabalho pesada -> velocidade de codificação rápida
- Carga de trabalho pesada -> velocidade de decodificação rápida
- Outras considerações: tamanho codificado, compatibilidade de idioma / ferramentas
- compensações de formatos binários compactos vs. formatos de texto autoexplicativos
* database / sql -> possui trocas que afetam o desempenho
* procure drivers que não o utilizem: jackx / pgx, crawshaw sqlite, ...
* gccgo (referência!), gollvm (WIP)
* container / list: use uma fatia (quase sempre)
## cgo
> cgo não é go
> - <cite> Rob Pike </cite>
* Características de desempenho de chamadas cgo
* Truques para reduzir os custos: lote
* Regras sobre a passagem de ponteiros entre Go e C
* arquivos syso (detector de corrida, dev.boringssl)
## Técnicas avançadas
Técnicas específicas para a arquitetura executando o código
* introdução aos caches da CPU
* falésias de desempenho
* construir intuição em torno das linhas de cache: tamanhos, preenchimento, alinhamento
* Ferramentas do SO para visualizar erros de cache (perf)
* mapas vs. fatias
* Layouts SOA vs AOS: linha principal x coluna principal; quando você tem um X, precisa de outro X ou de Y?
* localidade temporal e espacial: use o que você tem e o que está por perto o máximo possível
* reduzindo a perseguição do ponteiro
pré-busca explícita de memória; freqüentemente ineficaz; falta de intrínseca significa sobrecarga de chamada de função (removida do tempo de execução)
* faça os primeiros 64 bytes da sua estrutura contar
* previsão de ramificação
* remova ramos dos circuitos internos:
se um {for {}} else {for {}}
ao invés de
para {if a {} else {}}
benchmark devido à previsão de ramificação
estrutura para evitar ramificação
se i% 2 == 0 {
evens ++
} outro {
odds ++
}
conta [i & 1] ++
"código sem ramificação", referência; nem sempre mais rápido, mas frequentemente mais difícil de ler
TODO: classe ASCII conta exemplo, com benchmarks
* a classificação dos dados pode ajudar a melhorar o desempenho por meio da localização do cache e da previsão de ramificação, mesmo levando em consideração o tempo necessário para classificar
* sobrecarga de chamada de função: o inliner está melhorando
* reduz cópias de dados (inclusive para grandes listas repetidas de parâmetros de função)
* Comentário sobre os números de 2002 de Jeff Dean (mais atualizações)
* cpus ficaram mais rápidos, mas a memória não se manteve
TODO: pouco comentário sobre otimização livre de alinhamento de código (ou não otimização)
## Concorrência
* Descubra quais peças podem ser feitas em paralelo e quais devem ser seqüenciais
* goroutines são baratas, mas não gratuitas.
* Otimizando código multiencadeado
* compartilhamento falso -> tamanho do pad ao cache da linha
* compartilhamento verdadeiro -> fragmentação
* Sobreposição com a seção anterior sobre caches e compartilhamento falso / verdadeiro
* Sincronização preguiçosa; é caro, portanto, duplicar o trabalho pode ser mais barato
* coisas que você pode controlar: número de trabalhadores, tamanho do lote
Você precisa de um mutex para proteger o estado mutável compartilhado. Se você tem muitos mutex
contenção, você precisa reduzir o compartilhado ou o mutável. Dois
maneiras de reduzir o compartilhado são 1) fragmentar os bloqueios ou 2) processar independentemente
e combine depois. Para reduzir mutável: bem, faça sua estrutura de dados
somente leitura. Você também pode reduzir o tempo que os dados precisam ser compartilhados, reduzindo
a seção crítica - segure a trava o mínimo necessário. Às vezes, um RWMutex
será suficiente, embora observe que eles são mais lentos, mas permitem vários
leitores em.
Se você estiver compartilhando os bloqueios, tenha cuidado com as linhas de cache compartilhadas. Você precisará preencher para evitar oscilações na linha de cache entre os processadores.
var stripe [8] struct {sync.Mutex; _ [7] uint64} // mutex é de 64 bits; preenchimento preenche o restante do cacheline
Não faça nada caro em sua seção crítica, se puder ajudá-lo. Isso inclui coisas como E / S (que são baratas, mas lentas).
TODO: como decompor o problema por simultaneidade
TODO: razões pelas quais a implementação paralela pode ser mais lenta (sobrecarga de comunicação, o melhor algoritmo é seqüencial, ...)
## Assembly
* Coisas sobre como escrever código de montagem para o Go
* compiladores melhoram; A barra está alta
* substitua o mínimo possível para causar impacto; o custo de manutenção é alto
* boas razões: instruções SIMD ou outras coisas fora do que o Go e o compilador podem fornecer
* muito importante para avaliar: as melhorias podem ser enormes (10x para a rodovia)
zero (go-speck / rc6 / farm32) ou até mais lento (sem inlining)
* rebenchmark com novas versões para ver se você já pode excluir seu código
* TODO: link para 1.11 patches removendo código asm
* sempre tenha a versão pure-Go (purego build tag): testing, arm, gccgo
* breve introdução à sintaxe
* como digitar o ponto do meio
* convenção de chamada: tudo está na pilha, seguido pelos valores de retorno.
- tudo está na pilha, seguido pelos valores de retorno
- isso pode mudar https://github.com/golang/go/issues/18597
- https://science.raphael.poss.name/go-calling-convention-x86-64.html
* usando opcodes não suportados pelo asm (asm2plan9, mas isso está ficando mais raro)
* observações sobre por que a montagem em linha é difícil: https://github.com/golang/go/issues/26891
* todas as ferramentas para facilitar isso:
- asmfmt: gofmt para montagem https://github.com/klauspost/asmfmt
- c2goasm: converte o assembly de gcc / clang para goasm https://github.com/minio/c2goasm
- go2asm: converta ir para montagem, você pode vincular https://rsc.io/tmp/go2asm
- peachpy / avo: assembler de nível superior em python (peachpy) ou Go (avo)
- diferenças acima
* https://github.com/golang/go/wiki/AssemblyPolicy
* Design do Go Assembler: https://talks.golang.org/2016/asm.slide
## Otimizando um serviço inteiro
Na maioria das vezes, você não recebe uma única rotina vinculada à CPU.
Esse é o caso fácil. Se você possui um serviço para otimizar, precisa procurar
em todo o sistema. Monitoramento. Métricas. Registre muitas coisas ao longo do tempo
para que você possa vê-los piorando e para ver o impacto que seu
mudanças têm em produção.
tip.golang.org/doc/diagnostics.html
* referências para o design do sistema: Livro SRE, design prático do sistema distribuído
* ferramentas extras: mais registro + análise
* As duas regras básicas: acelerar as coisas lentas ou fazê-las com menos frequência.
* rastreamento distribuído para rastrear gargalos em um nível superior
* padrões de consulta para consultar um único servidor em vez de em massa
* seus problemas de desempenho podem não ser o seu código, mas você precisará contorná-los de qualquer maneira
* https://docs.microsoft.com/en-us/azure/architecture/antipatterns/
## Ferramentas
### Introdução aoProfilingpr
Este é um guia rápido para usar as ferramentas do pprof. Existem muitos outros guias disponíveis sobre isso.
Confira https://github.com/davecheney/high-performance-go-workshop.
TODO (dgryski): vídeos?
1. Introdução ao pprof
* vá ferramenta pprof (e <https://github.com/google/pprof>)
1. Escrever e executar (micro) benchmarks
* pequenos, como testes de unidade
* perfil, extrair código quente para benchmark, otimizar benchmark, perfil.
* -cpuprofile / -memprofile / -benchmem
* 0,5 ns / op significa que ele foi otimizado -> como evitar
* dicas para escrever boas marcas de microbench (remova trabalhos desnecessários, mas adicione linhas de base)
1. Como ler saída pprof
1. Quais são as diferentes partes do tempo de execução que aparecem
* malloc, trabalhadores do gc
* tempo de execução. \ _ ExternalCode
1. Macro-benchmarks (criação de perfil na produção)
* maiores, como testes de ponta a ponta
* net / http / pprof, muxer de depuração
* por amostragem, atingir 10 servidores a 100Hz é o mesmo que atingir 1 servidor a 1000Hz
1. Usando -base para observar diferenças
1. Opções de memória: -inuse_space, -inuse_objects, -alloc_space, -alloc_objects
1. Perfil na produção; localhost + tsh ssh, cabeçalhos de autenticação, usando curl.
1. Como ler gráficos de chama
### Tracer
### Veja algumas ferramentas mais interessantes / avançadas
* outras ferramentas em /x/ perf
* perf (perf2pprof)
* intel vtune / amd codexl / apple instruments
* https://godoc.org/github.com/aclements/go-perf
Apêndice: Implementando documentos de pesquisa
Dicas para implementar documentos: (Para `algoritmo`, leia também` estrutura de dados`)
* Não. Comece com a solução óbvia e estruturas de dados razoáveis.
Os algoritmos "modernos" tendem a ter complexidades teóricas mais baixas, mas alta constante
fatores e muita complexidade de implementação. Um dos exemplos clássicos de
isso é montes de Fibonacci. Eles são notoriamente difíceis de acertar e têm
um enorme fator constante. Existem vários artigos publicados comparando
implementações de heap diferentes em diferentes cargas de trabalho e, em geral, as
ou montes implícitos de 8 árias aparecem sempre no topo. E mesmo nos casos
onde a pilha de Fibonacci deve ser mais rápida (devido a O (1) "chave de diminuição"),
experimentos com o algoritmo de busca profunda da Dijkstra mostram que é mais rápido
quando eles usam a remoção e adição direta de heap.
Da mesma forma, treaps ou skiplists vs. as árvores mais complexas de vermelho-preto ou AVL.
No hardware moderno, o algoritmo "mais lento" pode ser rápido o suficiente ou até
Mais rápido.
> O algoritmo mais rápido pode frequentemente ser substituído por um que é quase tão rápido e muito mais fácil de entender.
>
> - <cite> Douglas W. Jones, Universidade de Iowa </cite>
A complexidade adicional deve ser suficiente para que o retorno valha a pena.
Outro exemplo são os algoritmos de remoção de cache. Algoritmos diferentes podem ter
complexidade muito mais alta, apenas para uma pequena melhoria na taxa de acertos. Claro,
talvez não seja possível testá-lo até que você tenha uma implementação funcional e
integrou-o ao seu programa.
Às vezes, o trabalho tem gráficos, mas é muito parecido com a tendência
publicando apenas resultados positivos, estes tenderão a ser distorcidos em favor de
mostrando o quão bom é o novo algoritmo.
* Escolha o papel certo.
* Procure o artigo que seu algoritmo pretende superar e implementar.
Freqüentemente, artigos anteriores serão mais fáceis de entender e necessariamente terão
algoritmos mais simples.
Nem todos os papéis são bons.
Veja o contexto em que o artigo foi escrito. Determine suposições sobre
hardware: espaço em disco, uso de memória etc. Alguns papéis mais antigos
tradeoffs diferentes que eram razoáveis nos anos 70 ou 80, mas não
necessariamente se aplica ao seu caso de uso. Por exemplo, o que eles determinam ser
memória "razoável" versus trocas de uso de disco. Os tamanhos de memória agora são pedidos de
magnitude maior e os SSDs alteraram a penalidade de latência pelo uso do disco.
Da mesma forma, alguns algoritmos de streaming são projetados para hardware de roteador, o que
pode dificultar a tradução em software.
Verifique se as suposições que o algoritmo faz sobre seus dados são mantidas.
Isso vai demorar um pouco. Você provavelmente não deseja implementar o
primeiro artigo que você encontrar.
* Certifique-se de entender o algoritmo. Isso parece óbvio, mas será
impossível depurar de outra maneira.
<https://blizzard.cs.uwaterloo.ca/keshav/home/Papers/data/07/paper-reading.pdf>
Um bom entendimento pode permitir que você extraia a ideia-chave do documento
e, possivelmente, aplique exatamente isso ao seu problema, que pode ser mais simples do que
reimplementando a coisa toda.
* O documento original de uma estrutura ou algoritmo de dados nem sempre é o melhor. Trabalhos posteriores podem ter melhores explicações.
* Alguns trabalhos publicam código fonte de referência com o qual você pode comparar, mas
1) o código acadêmico é quase universalmente terrível
2) cuidado com as restrições de licença ("apenas para fins de pesquisa")
3) cuidado com os erros; casos extremos, verificação de erros, desempenho etc.
Também procure outras implementações no GitHub: elas podem ter os mesmos (ou diferentes!) Bugs que o seu.
Outros recursos sobre este tópico:
* <https://www.youtube.com/watch?v=8eRx5Wo3xYA>
* <http://codecapsule.com/2012/01/18/how-to-implement-a-paper/>