Questão:
O consenso da indústria de software é que o código limpo e simples é fundamental para a viabilidade a longo prazo da base de código e da organização que a possui. Essas propriedades levam a custos de manutenção mais baixos e maior probabilidade de continuidade da base de códigos.
No entanto, o código SIMD é diferente do código geral do aplicativo, e eu gostaria de saber se existe um consenso semelhante em relação ao código limpo e simples que se aplica especificamente ao código SIMD.
Antecedentes da minha pergunta.
Escrevo bastante código SIMD (instrução única, vários dados) para várias tarefas de processamento e análise de imagem. Recentemente, também tive que portar um pequeno número dessas funções de uma arquitetura (SSE2) para outra (ARM NEON).
O código foi escrito para software compactado, portanto, não pode depender de idiomas proprietários sem direitos de redistribuição irrestritos, como o MATLAB.
Um exemplo de estrutura de código típica:
- Usando o tipo de matriz do OpenCV (
Mat
) para todo o gerenciamento de memória, buffer e tempo de vida. - Após verificar o tamanho (dimensões) dos argumentos de entrada, os ponteiros para o endereço inicial de cada linha de pixels são obtidos.
- A contagem de pixels e os endereços iniciais de cada linha de pixels de cada matriz de entrada são passados para algumas funções C ++ de baixo nível.
- Essas funções C ++ de baixo nível usam intrínsecas SIMD (para Intel Architecture e ARM NEON ), carregando e salvando em endereços de ponteiro não processados.
- Características dessas funções C ++ de baixo nível:
- Exclusivamente unidimensional (consecutivo na memória)
- Não lida com alocações de memória.
(Toda alocação, incluindo temporários, é tratada pelo código externo usando os recursos do OpenCV.) - O intervalo de comprimentos dos nomes dos símbolos (intrínsecas, nomes de variáveis etc.) tem aproximadamente 10 a 20 caracteres, o que é bastante excessivo.
(Lê como tagarelar techno.) - A reutilização de variáveis SIMD é desencorajada porque os compiladores são bastante problemáticos ao analisar corretamente o código que não está gravado no estilo de codificação "atribuição única".
(Arquivei vários relatórios de erros do compilador.)
Quais aspectos da programação do SIMD poderiam fazer com que a discussão diferisse do caso geral? Ou, por que o SIMD é diferente?
Em termos de custo inicial de desenvolvimento
- É sabido que o custo inicial de desenvolvimento do código C ++ SIMD com bom desempenho é de cerca de 10x - 100x (com uma ampla margem) em comparação com o código C ++ gravado casualmente .
- Conforme observado nas respostas para a escolha entre desempenho versus código legível / limpador? , a maioria dos códigos (incluindo código escrito casualmente e código SIMD) não é nem limpa nem rápida .
- As melhorias evolutivas no desempenho do código (no código escalar e no SIMD) são desencorajadas (porque são vistas como uma espécie de retrabalho de software ), e o custo e o benefício não são rastreados.
Em termos de propensão
(por exemplo, o princípio de Pareto, também conhecido como a regra 80-20 )
- Mesmo que o processamento de imagens compreenda apenas 20% de um sistema de software (em tamanho e funcionalidade do código), o processamento de imagens é comparativamente lento (quando visto como uma porcentagem do tempo gasto na CPU), levando mais de 80% do tempo.
- Isso se deve ao efeito do tamanho dos dados: Um tamanho típico de imagem é medido em megabytes, enquanto o tamanho típico de dados sem imagem é medido em kilobytes.
- Dentro do código de processamento de imagem, um programador SIMD é treinado para reconhecer automaticamente o código de 20% que compreende os pontos de acesso, identificando a estrutura do loop no código C ++. Assim, da perspectiva de um programador SIMD, 100% do "código que importa" é um gargalo de desempenho.
- Geralmente, em um sistema de processamento de imagens, existem vários pontos de acesso e ocupam proporções comparáveis de tempo. Por exemplo, pode haver 5 pontos de acesso cada um ocupando (20%, 18%, 16%, 14%, 12%) do tempo total. Para obter um ganho de alto desempenho, todos os pontos de acesso precisam ser reescritos no SIMD.
- Isso é resumido como a regra de estourar balões: um balão não pode ser estourado duas vezes.
- Suponha que haja alguns balões, digamos 5 deles. A única maneira de dizimá-los é estourá-los um por um.
- Depois que o primeiro balão é acionado, os 4 balões restantes agora representam uma porcentagem maior do tempo total de execução.
- Para obter mais ganhos, é preciso então estourar outro balão.
(Isto é em desafio para a 80-20 regra de otimização: um bom resultado econômico pode ser alcançado após os 20% de frutas de menor suspensão foi escolhido.)
Em termos de legibilidade e manutenção
O código SIMD é claramente difícil de ler.
- Isso é verdade mesmo que se siga todas as práticas recomendadas de engenharia de software, por exemplo, nomeação, encapsulamento, correção de const (e tornando óbvios os efeitos colaterais), decomposição de funções etc.
- Isso é verdade mesmo para programadores SIMD experientes.
O código SIMD ideal é muito distorcido (consulte a observação) em comparação com seu código de protótipo C ++ equivalente.
- Existem várias maneiras de contornar o código SIMD, mas apenas 1 em cada 10 tentativas alcançará resultados aceitáveis rapidamente.
- (Ou seja, na faixa dos ganhos de desempenho de 4x-10x, a fim de justificar um alto custo de desenvolvimento. Na prática, ganhos ainda maiores foram observados.)
(Observação)
Esta é a principal tese do projeto MIT Halide - citando o título do artigo literalmente:
"dissociar algoritmos de agendas para facilitar a otimização de pipelines de processamento de imagem"
Em termos de aplicabilidade futura
- O código SIMD está estritamente vinculado a uma única arquitetura. Cada nova arquitetura (ou cada ampliação de registros SIMD) requer uma reescrita.
- Ao contrário da maioria do desenvolvimento de software, cada parte do código SIMD é tipicamente escrita para um único objetivo que nunca muda.
(Com exceção de portar para outras arquiteturas.) - Algumas arquiteturas mantêm perfeita compatibilidade com versões anteriores (Intel); alguns ficam aquém de uma quantidade trivial (ARM AArch64, substituindo
vtbl
porvtblq
), mas que é suficiente para causar a falha na compilação de algum código.
Em termos de habilidades e treinamento
- Não está claro quais pré-requisitos de conhecimento são necessários para treinar adequadamente um novo programador para escrever e manter o código SIMD.
- Os graduados que aprenderam programação SIMD na escola parecem desprezá-la e descartá-la como uma carreira impraticável.
- A leitura de desmontagem e o perfil de desempenho de baixo nível são citados como duas habilidades fundamentais para escrever código SIMD de alto desempenho. No entanto, não está claro como treinar sistematicamente programadores nessas duas habilidades.
- A arquitetura moderna da CPU (que diverge significativamente do que é ensinado nos livros didáticos) torna o treinamento ainda mais difícil.
Em termos de correção e custos relacionados a defeitos
- Uma única função de processamento SIMD é realmente coesa o suficiente para que se possa estabelecer a correção por:
- Aplicação de métodos formais (com caneta e papel) e
- Verificando intervalos inteiros de saída (com código de protótipo e executados fora do tempo de execução) .
- O processo de verificação é, no entanto, muito caro (gasta 100% do tempo em revisão de código e 100% em verificação de modelo de protótipo), o que triplica o já caro custo de desenvolvimento do código SIMD.
- Se, de alguma forma, um bug conseguir passar por esse processo de verificação, é quase impossível "reparar" (consertar), exceto substituir (reescrever) a função defeituosa suspeita.
- O código SIMD sofre com a falta de defeitos no compilador C ++ (otimizando o gerador de código).
- O código SIMD gerado usando modelos de expressão C ++ também sofre muito com defeitos do compilador.
Em termos de inovações disruptivas
Muitas soluções foram propostas pela academia, mas poucas estão vendo uso comercial generalizado.
- MIT Halide
- Stanford Darkroom
- NT2 (Numerical Template Toolbox) e o Boost.SIMD relacionado
Bibliotecas com amplo uso comercial não parecem ser fortemente habilitadas para SIMD.
- As bibliotecas de código aberto parecem mornas para o SIMD.
- Recentemente, tenho essa observação em primeira mão depois de criar um perfil de um grande número de funções da API OpenCV, a partir da versão 2.4.9.
- Muitas outras bibliotecas de processamento de imagem que eu criei perfil também não fazem uso pesado de SIMD, ou elas perdem os verdadeiros hotspots.
- Bibliotecas comerciais parecem evitar completamente o SIMD.
- Em alguns casos, eu já vi bibliotecas de processamento de imagem revertendo código otimizado para SIMD em uma versão anterior para código não SIMD em uma versão posterior, resultando em severas regressões de desempenho.
(A resposta do fornecedor é que era necessário evitar erros do compilador.)
- Em alguns casos, eu já vi bibliotecas de processamento de imagem revertendo código otimizado para SIMD em uma versão anterior para código não SIMD em uma versão posterior, resultando em severas regressões de desempenho.
- As bibliotecas de código aberto parecem mornas para o SIMD.
A pergunta deste programador: o código de baixa latência às vezes precisa ser "feio"? está relacionado, e eu escrevi anteriormente uma resposta a essa pergunta para explicar meus pontos de vista há alguns anos atrás.
No entanto, essa resposta é basicamente "apaziguamento" ao ponto de vista "otimização prematura", ou seja, ao ponto de vista que:
- Todas as otimizações são prematuras por definição (ou a curto prazo por natureza ) e
- A única otimização que traz benefícios a longo prazo é a simplicidade.
Mas esses pontos de vista são contestados neste artigo do ACM .
Tudo isso me leva a perguntar: o
código SIMD é diferente do código geral do aplicativo, e eu gostaria de saber se existe um consenso semelhante no setor em relação ao valor do código limpo e simples para o código SIMD.