Como documentar e ensinar aos outros código "computacionalmente intensivo" otimizado além do reconhecimento?

11

Ocasionalmente, há 1% de código que é intensivamente computacionalmente e precisa do tipo mais pesado de otimização de baixo nível. Exemplos são processamento de vídeo, processamento de imagem e todos os tipos de processamento de sinal, em geral.

Os objetivos são documentar e ensinar as técnicas de otimização, para que o código não se torne insustentável e propenso à remoção por desenvolvedores mais recentes. (*)

(*) Não obstante a possibilidade de que a otimização específica seja completamente inútil em algumas futuras CPUs imprevisíveis, de modo que o código seja excluído de qualquer maneira.

Considerando que as ofertas de software (comercial ou de código aberto) mantêm sua vantagem competitiva ao ter o código mais rápido e fazer uso da mais nova arquitetura de CPU, os criadores de software geralmente precisam ajustar o código para acelerar o processo e obter a mesma saída por um determinado período. lista de tarefas, tolerando uma pequena quantidade de erros de arredondamento.

Normalmente, um gravador de software pode manter muitas versões de uma função como documentação de cada reescrita de otimização / algoritmo que ocorre. Como alguém disponibiliza essas versões para que outras pessoas estudem suas técnicas de otimização?

Palavras-chave:

rwong
fonte
1
Você pode manter as diferentes versões do código comentadas, com muitos comentários dizendo ao leitor o que está acontecendo.
Mike Dunlavey
1
E não diga apenas o que o código está fazendo, mas porque é mais rápido assim. Inclua links para algoritmos, se necessário, seja ele próprio, como wiki, documentos ou recursos disponíveis na Internet (apenas lembre-se de rot-link, nesse caso, pode ser aconselhável copiá-lo em seu próprio sistema de documentos com um link para o original .)
Marjan Venema
1
@ MikeDunlavey: Ai, por favor , não comente. Basta ter várias implementações da mesma função e chamar a que for mais rápida. Dessa forma, você pode facilmente mudar para uma versão diferente do código e fazer o benchmark de todos eles.
sleske
2
@sleske Às vezes, apenas ter mais código binário pode atrasá-lo.
2441111
@quant_dev: Sim, isso pode acontecer. Apenas acho que é importante que o código seja criado e executado (idealmente) regularmente, para mantê-lo atualizado. Talvez compilá-lo apenas no modo de depuração.
sleske

Respostas:

10

Resposta curta

Mantenha as otimizações locais, torne-as óbvias, documente-as bem e facilite a comparação das versões otimizadas entre si e com a versão não otimizada, tanto em termos de código fonte quanto de desempenho em tempo de execução.

Resposta completa

Se essas otimizações realmente são importantes para o seu produto, você precisa saber não apenas por que as otimizações foram úteis antes, mas também fornecer informações suficientes para ajudar os desenvolvedores a saber se serão úteis no futuro.

Idealmente, você precisa consagrar o teste de desempenho ao seu processo de construção, para descobrir quando as novas tecnologias invalidam as otimizações antigas.

Lembrar:

A primeira regra de otimização de programa: não faça isso.

A Segunda Regra da Otimização de Programas (somente para especialistas!): Não faça isso ainda. "

- Michael A. Jackson

Para saber se agora é a hora, é necessário fazer comparações e testes.

Como você mencionou, o maior problema com código altamente otimizado é que é difícil manter, portanto, na medida do possível, é necessário manter as partes otimizadas separadas das partes não otimizadas. Se você faz isso por meio de vínculos em tempo de compilação, chamadas de função virtual em tempo de execução ou algo intermediário, não importa. O que importa é que, quando você executa seus testes, deseja poder testar todas as versões em que está interessado atualmente.

Eu estaria inclinado a construir um sistema de tal maneira que a versão básica não otimizada do código de produção sempre pudesse ser usada para entender a intenção do código e, em seguida, criar diferentes módulos otimizados ao lado deste contendo a versão ou versões otimizadas, documentando explicitamente onde quer que a versão otimizada difere da linha de base. Ao executar seus testes (unidade e integração), você o executa na versão não otimizada e em todos os módulos otimizados atuais.

Exemplo

Por exemplo, digamos que você tenha uma função Fast Fourier Transform . Talvez você tenha uma implementação algorítmica básica fft.ce faça o teste fft_tests.c.

Aí vem o Pentium e você decide implementar a versão de ponto fixo fft_mmx.cusando as instruções da MMX . Mais tarde, o pentium 3 vem e você decidir adicionar uma versão que usa Streaming SIMD Extensions no fft_sse.c.

Agora você deseja adicionar o CUDA , fft_cuda.cmas também com o conjunto de dados de teste usado há anos, a versão CUDA é mais lenta que a versão SSE! Você faz algumas análises e acaba adicionando um conjunto de dados 100 vezes maior e obtém a velocidade esperada, mas agora sabe que o tempo de configuração para usar a versão CUDA é significativo e que, com pequenos conjuntos de dados, você deve usar um algoritmo sem esse custo de instalação.

Em cada um desses casos, você está implementando o mesmo algoritmo, todos devem se comportar da mesma maneira, mas serão executados com diferentes eficiências e velocidades em arquiteturas diferentes (se for que serão executadas). Porém, do ponto de vista do código, você pode comparar qualquer par de arquivos de origem para descobrir por que a mesma interface é implementada de maneiras diferentes e, geralmente, a maneira mais fácil será consultar a versão original não otimizada.

O mesmo vale para uma implementação OOP em que uma classe base que implementa o algoritmo não otimizado e classes derivadas implementam otimizações diferentes.

O importante é manter as mesmas coisas que são as mesmas , de modo que as diferenças são óbvias .

Mark Booth
fonte
7

Especificamente, como você tomou o exemplo do processamento de vídeo e imagem, é possível manter o código como parte da mesma versão, mas ativo ou inativo, dependendo do contexto.

Enquanto você não mencionou, estou assumindo Caqui.

A maneira mais simples no Ccódigo, a otimização (e também se aplica ao tentar tornar as coisas portáteis) é manter

 
#ifdef OPTIMIZATION_XYZ_ENABLE 
   // your optimzied code here... 
#else  
   // your basic code here...

Quando você ativa #define OPTIMIZATION_XYZ_ENABLEdurante a compilação no Makefile, tudo funciona de acordo.

Geralmente, cortar algumas linhas de código no meio de funções pode ficar confuso quando há muitas funções otimizadas. Portanto, nesse caso, define-se ponteiros de funções diferentes para executar uma função específica.

o código principal sempre executa através de um ponteiro de função como


   codec->computed_idct(blocks); 

Mas os ponteiros de função são definidos dependendo do tipo de exemplo (por exemplo, aqui a função idct é otimizada para diferentes arquiteturas de CPU).



if(OPTIMIZE_X86) {
  codec->computed_idct = compute_idct_x86; 
}
else if(OPTIMZE_ARM) {
  codec->computed_idct = compute_idct_ARM;
}
else {
  codec->computed_idct = compute_idct_C; 
}

você deve ver os códigos libjpeg e libmpeg2 e pode ser ffmpeg para essas técnicas.

Dipan Mehta
fonte
6

Como pesquisador, acabei escrevendo bastante do código de "gargalo". No entanto, uma vez colocado em produção, o ônus de integrá-lo ao produto e fornecer suporte subseqüente recai sobre os desenvolvedores. Como você pode imaginar, comunicar com clareza o que e como o programa deve operar é da maior importância.

Descobri que existem três ingredientes essenciais para concluir esta etapa com sucesso

  1. O algoritmo usado deve ser absolutamente claro.
  2. O objetivo de toda linha de implementação deve ser claro.
  3. Os desvios dos resultados esperados devem ser identificados o mais rápido possível.

Para a primeira etapa, eu sempre escrevo um pequeno white paper que documenta o algoritmo. O objetivo aqui é realmente escrevê-lo para que outra pessoa possa implementá-lo do zero usando apenas o whitepaper. Se é um algoritmo bem conhecido, publicado, basta fornecer as referências e repetir as equações principais. Se for um trabalho original, você precisará ser um pouco mais explícito. Isso informará o que o código deve fazer .

A implementação real que é entregue ao desenvolvimento deve ser documentada de tal maneira que todas as sutilezas sejam explicitadas. Se você adquirir bloqueios em uma ordem específica para evitar conflitos, adicione um comentário. Se você iterar sobre as colunas em vez de sobre as linhas de uma matriz devido a problemas de coerência de cache, adicione um comentário. Se você fizer algo até um pouco inteligente, comente. Se você pode garantir o whitepaper e o código nunca será separado (por meio do VCS ou sistema similar), consulte o whitepaper. O resultado pode facilmente ter mais de 50% de comentários. Tudo bem. Isso mostrará por que o código faz o que faz.

Por fim, você precisa garantir a correção diante das alterações. Felizmente, temos uma ferramenta útil em testes automatizados e plataformas de integração contínua . Isso informará o que o código está realmente fazendo .

Minha recomendação mais calorosa seria não economizar em nenhuma das etapas. Você precisará deles mais tarde;)

drxzcl
fonte
Obrigado pela sua resposta abrangente. Eu concordo com todos os seus pontos. Em termos de teste automatizado, acho que cobrir adequadamente o intervalo numérico da aritmética de ponto fixo e do código SIMD é difícil, algo que eu já queimei duas vezes. As condições prévias declaradas apenas nos comentários (sem código para reforçar) nem sempre foram atendidas.
rwong
A razão pela qual ainda não aceitei sua resposta é porque preciso de mais orientação sobre o que "um pequeno white paper" significa e que esforço deve ser feito para produzi-lo. Para alguns setores, isso faz parte da linha principal de negócios, mas em outros setores o custo deve ser considerado e os atalhos disponíveis legalmente devem ter sido adotados.
rwong
Antes de tudo, sinto sua dor em relação a testes automatizados, aritmética de ponto flutuante e código paralelo. Receio que não exista uma solução válida para todos os casos. Geralmente trabalho com tolerâncias bastante liberais, mas no seu setor isso pode não ser possível.
drxzcl
2
Na prática, o whitepaper freqüentemente se parece com o primeiro rascunho de um artigo científico, sem as partes "leves" (sem introdução significativa, sem resumo, conclusões / discussões mínimas e apenas as referências necessárias para entendê-lo). Vejo escrever o artigo como um relatório e parte integrante do desenvolvimento de algoritmos e / ou seleção de algoritmos. Você optou por implementar esse algoritmo (por exemplo, FFT espectral). O que é isso exatamente? Por que você escolheu este sobre os outros? Quais são as suas características de paralelização? O esforço deve ser proporcional ao trabalho de seleção / desenvolvimento.
drxzcl
5

Acredito que isso seja melhor resolvido através de comentários abrangentes do código, a ponto de cada bloco significativo de código ter comentários explicativos de antemão.

Os comentários devem incluir citações das especificações ou material de referência de hardware.

Use nomes de terminologia e algoritmos em todo o setor, quando apropriado - por exemplo, 'a arquitetura X gera traps de CPU para leituras desalinhadas, para que este dispositivo da Duff preencha o próximo limite de alinhamento'.

Eu usaria nomes de variáveis ​​no seu rosto para garantir que não houvesse mal-entendido sobre o que está acontecendo. Não é húngaro, mas coisas como 'stride' para descrever a distância em bytes entre dois pixels verticais.

Eu também complementaria isso com um documento breve e humanamente legível, com diagramas de alto nível e design de blocos.

JBRWilkinson
fonte
1
Usar uma terminologia consistente para uma única coisa (por exemplo, usar "passada" em termos de significados semelhantes, por exemplo, "passo", "alinhamento") no mesmo projeto ajudaria. Isso é um pouco difícil ao integrar a base de código de vários projetos em um projeto.
Rwong