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.
Respostas:
Eu não escrevi muito código SIMD para mim, mas muito código assembler algumas décadas atrás. O AFAIK usando intrínsecas SIMD é essencialmente programação de assembler, e toda a sua pergunta pode ser reformulada substituindo "SIMD" pela palavra "assembly". Por exemplo, os pontos que você já mencionou, como
o código leva de 10 a 100 vezes para ser desenvolvido em vez de "código de alto nível"
está ligado a uma arquitetura específica
o código nunca é "limpo" nem fácil de refatorar
você precisa de especialistas para escrevê-lo e mantê-lo
depurar e manter é difícil, evoluindo muito difícil
não são de forma alguma "especiais" para o SIMD - esses pontos são verdadeiros para qualquer tipo de linguagem assembly e são todos "consenso da indústria". E a conclusão na indústria de software também é praticamente a mesma da montadora:
não escreva se não precisar - use uma linguagem de alto nível sempre que possível e permita que os compiladores façam o trabalho duro
se os compiladores não forem suficientes, pelo menos encapsule as partes de "baixo nível" em algumas bibliotecas, mas evite espalhar o código por todo o programa
como é quase impossível escrever código SIMD ou montador "auto-documentado", tente equilibrar isso com muita documentação.
Certamente, há realmente uma diferença na situação do assembly "clássico" ou do código de máquina: atualmente, os compiladores modernos geralmente produzem código de máquina de alta qualidade a partir de uma linguagem de alto nível, que geralmente é melhor otimizada do que o código de assembler escrito manualmente. Para as arquiteturas SIMD que são populares hoje em dia, a qualidade dos compiladores disponíveis é AFAIK muito abaixo disso - e talvez nunca chegue a isso, pois a vetorização automática ainda é um tópico de pesquisa científica. Veja, por exemplo, este artigo que descreve as diferenças de opimização entre um compilador e um humano, fornecendo uma noção de que pode ser muito difícil criar bons compiladores SIMD.
Como você já descreveu na sua pergunta, também existe um problema de qualidade nas bibliotecas atuais de ponta. Portanto, o melhor que podemos esperar do IMHO é que, nos próximos anos, a qualidade dos compiladores e bibliotecas aumente, talvez o hardware SIMD precise mudar para se tornar mais "amigável ao compilador", talvez linguagens de programação especializadas que suportem uma vetorização mais fácil (como o Halide, que você mencionou duas vezes) se tornará mais popular (isso já não era uma força do Fortran?). Segundo a Wikipedia , o SIMD se tornou "um produto em massa" cerca de 15 a 20 anos atrás (e Halide tem menos de 3 anos de idade, quando interpreto os documentos corretamente). Compare isso com os compiladores de tempo para a linguagem assembly "clássica" necessária para amadurecer. De acordo com este artigo da WikipediaLevou quase 30 anos (de ~ 1970 até o final dos anos 90) até os compiladores excederem o desempenho de especialistas humanos (na produção de código de máquina não paralelo). Portanto, talvez tenhamos que esperar mais 10 a 15 anos até que o mesmo aconteça com os compiladores habilitados para SIMD.
fonte
Minha organização lidou com esse problema exato. Nossos produtos estão no espaço de vídeo, mas grande parte do código que escrevemos é de processamento de imagem que funcionaria também para imagens estáticas.
Nós "resolvemos" (ou talvez "resolvemos") o problema escrevendo nosso próprio compilador. Isso não é tão louco quanto parece à primeira vista. Tem um conjunto restrito de entradas. Sabemos que todo o código está funcionando em imagens, principalmente imagens RGBA. Definimos algumas restrições, como que os buffers de entrada e saída nunca podem se sobrepor, portanto não há alias de ponteiro. Coisas assim.
Em seguida, escrevemos nosso código na OpenGL Shading Language (glsl). Ele é compilado para código escalar, SSE, SSE2, SSE3, AVX, Neon e, claro, glsl real. Quando precisamos oferecer suporte a uma nova plataforma, atualizamos o compilador para gerar o código para essa plataforma.
Também fazemos o mosaico das imagens para melhorar a coerência do cache e coisas assim. Mas, mantendo o processamento da imagem em um pequeno kernel e usando o glsl (que nem suporta ponteiros) reduzimos bastante a complexidade de compilar o código.
Essa abordagem não é para todos e tem seus próprios problemas (você precisa garantir a correção do compilador, por exemplo). Mas funcionou bastante bem para nós.
fonte
Parece não adicionar muita sobrecarga de manutenção se você considerar usar um idioma de nível superior:
vs
É claro que você terá que enfrentar as limitações da biblioteca, mas não a manterá por conta própria. Pode ser um bom equilíbrio entre o custo de manutenção e o ganho de desempenho.
http://blogs.msdn.com/b/dotnet/archive/2014/04/07/the-jit-finally-proposed-jit-and-simd-are-getting-married.aspx
http://blogs.msdn.com/b/dotnet/archive/2014/05/13/update-to-simd-support.aspx
fonte
Eu fiz programação de montagem no passado, não programação SIMD recentemente.
Você já pensou em usar um compilador compatível com SIMD como o da Intel? É um guia para Vectorization com Intel® C ++ compiladores interessante?
Vários de seus comentários, como "estourar balões", sugerem o uso de um compilador (para obter benefícios, se você não tiver um único ponto de acesso).
fonte