Highway é uma biblioteca C++ que fornece intrínsecos SIMD/vetoriais portáteis.
Documentação
Anteriormente licenciado sob Apache 2, agora com licença dupla como Apache 2/BSD-3.
Somos apaixonados por software de alto desempenho. Vemos um grande potencial inexplorado em CPUs (servidores, dispositivos móveis, desktops). Highway é para engenheiros que desejam ampliar de maneira confiável e econômica os limites do que é possível em software.
As CPUs fornecem instruções SIMD/vetoriais que aplicam a mesma operação a vários itens de dados. Isto pode reduzir o uso de energia, por exemplo, cinco vezes porque menos instruções são executadas. Também vemos frequentemente acelerações de 5 a 10x .
Highway torna a programação SIMD/vetorial prática e viável de acordo com estes princípios orientadores:
Faz o que você espera : Highway é uma biblioteca C++ com funções cuidadosamente escolhidas que mapeiam bem as instruções da CPU sem extensas transformações do compilador. O código resultante é mais previsível e robusto para alterações de código/atualizações do compilador do que a autovetorização.
Funciona em plataformas amplamente utilizadas : Highway suporta cinco arquiteturas; o mesmo código de aplicação pode ter como alvo vários conjuntos de instruções, incluindo aqueles com vetores 'escaláveis' (tamanho desconhecido em tempo de compilação). Highway requer apenas C++ 11 e oferece suporte a quatro famílias de compiladores. Se você gostaria de usar o Highway em outras plataformas, levante um problema.
Flexível para implantação : aplicativos que usam Highway podem ser executados em nuvens heterogêneas ou dispositivos clientes, escolhendo o melhor conjunto de instruções disponível em tempo de execução. Alternativamente, os desenvolvedores podem optar por direcionar um único conjunto de instruções sem qualquer sobrecarga de tempo de execução. Em ambos os casos, o código do aplicativo é o mesmo, exceto pela troca de HWY_STATIC_DISPATCH
por HWY_DYNAMIC_DISPATCH
mais uma linha de código. Veja também a introdução de @kfjahnke ao despacho.
Adequado para uma variedade de domínios : Highway fornece um extenso conjunto de operações, usado para processamento de imagens (ponto flutuante), compressão, análise de vídeo, álgebra linear, criptografia, classificação e geração aleatória. Reconhecemos que novos casos de uso podem exigir operações adicionais e estamos felizes em adicioná-los onde fizer sentido (por exemplo, sem quedas de desempenho em algumas arquiteturas). Se você quiser discutir, registre um problema.
Design paralelo de dados de recompensas : Highway fornece ferramentas como Gather, MaskedLoad e FixedTag para permitir acelerações para estruturas de dados legadas. No entanto, os maiores ganhos são obtidos através da concepção de algoritmos e estruturas de dados para vectores escaláveis. Técnicas úteis incluem lotes, layouts de estrutura de array e alocações alinhadas/preenchidas.
Recomendamos estes recursos para começar:
Demonstrações online usando o Compiler Explorer:
Observamos que Highway é referenciado nos seguintes projetos de código aberto, encontrados em sourcegraph.com. A maioria são repositórios GitHub. Se você gostaria de adicionar seu projeto ou criar um link diretamente para ele, sinta-se à vontade para levantar um problema ou entrar em contato conosco através do e-mail abaixo.
Outro
Se desejar obter o Highway, além de clonar este repositório GitHub ou usá-lo como um submódulo Git, você também pode encontrá-lo nos seguintes gerenciadores de pacotes ou repositórios:
Veja também a lista em https://repology.org/project/highway-simd-library/versions .
A rodovia suporta 24 alvos, listados em ordem alfabética de plataforma:
EMU128
, SCALAR
;NEON_WITHOUT_AES
, NEON
, NEON_BF16
, SVE
, SVE2
, SVE_256
, SVE2_128
;Z14
, Z15
;PPC8
(v2.07), PPC9
(v3.0), PPC10
(v3.1B, ainda não suportado devido a bugs do compilador, consulte #1207; também requer QEMU 7.2);RVV
(1,0);WASM
, WASM_EMU256
(uma versão 2x desenrolada do wasm128, habilitada se HWY_WANT_WASM2
estiver definido. Isso permanecerá com suporte até que seja potencialmente substituído por uma versão futura do WASM.);SSE2
SSSE3
(~Intel Core)SSE4
(~Nehalem, também inclui AES + CLMUL).AVX2
(~Haswell, também inclui IMC2 + F16 + FMA)AVX3
(~Skylake, AVX-512F/BW/CD/DQ/VL)AVX3_DL
(~Icelake, inclui BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT; requer aceitação definindo HWY_WANT_AVX3_DL
a menos que seja compilado para envio estático),AVX3_ZEN4
(como AVX3_DL, mas otimizado para AMD Zen4; requer aceitação definindo HWY_WANT_AVX3_ZEN4
se estiver compilando para envio estático, mas habilitado por padrão para envio em tempo de execução),AVX3_SPR
(~Sapphire Rapids, inclui AVX-512FP16)Nossa política é que, a menos que especificado de outra forma, os alvos permanecerão suportados desde que possam ser compilados (cruzados) com Clang ou GCC atualmente suportados e testados usando QEMU. Se o destino puder ser compilado com o tronco LLVM e testado usando nossa versão do QEMU sem sinalizadores extras, ele será elegível para inclusão em nossa infraestrutura de testes contínuos. Caso contrário, o alvo será testado manualmente antes dos lançamentos com versões/configurações selecionadas de Clang e GCC.
SVE foi inicialmente testado usando farm_sve (ver agradecimentos).
Os lançamentos da Highway visam seguir o sistema semver.org (MAJOR.MINOR.PATCH), incrementando MINOR após adições compatíveis com versões anteriores e PATCH após correções compatíveis com versões anteriores. Recomendamos o uso de releases (em vez da dica do Git) porque eles são testados mais extensivamente, veja abaixo.
A versão atual 1.0 sinaliza um foco maior na compatibilidade com versões anteriores. Os aplicativos que usam funcionalidade documentada permanecerão compatíveis com atualizações futuras que tenham o mesmo número de versão principal.
Testes de integração contínua construídos com uma versão recente do Clang (rodando em x86 nativo ou QEMU para RISC-V e Arm) e MSVC 2019 (v19.28, rodando em x86 nativo).
Antes dos lançamentos, também testamos em x86 com Clang e GCC, e Armv7/8 via compilação cruzada do GCC. Consulte o processo de teste para obter detalhes.
O diretório contrib
contém utilitários relacionados ao SIMD: uma classe de imagem com linhas alinhadas, uma biblioteca matemática (16 funções já implementadas, principalmente trigonometria) e funções para calcular produtos escalares e classificação.
Se você precisar apenas de suporte x86, também poderá usar a biblioteca de classes vetoriais VCL da Agner Fog. Inclui muitas funções, incluindo uma biblioteca matemática completa.
Se você possui código existente usando intrínsecos x86/NEON, pode estar interessado no SIMDe, que emula esses intrínsecos usando intrínsecos ou autovetorização de outras plataformas.
Este projeto usa CMake para gerar e construir. Em um sistema baseado em Debian você pode instalá-lo via:
sudo apt install cmake
Os testes de unidade da Highway usam o googletest. Por padrão, o CMake do Highway baixa essa dependência no momento da configuração. Você pode evitar isso definindo a variável HWY_SYSTEM_GTEST
CMake como ON e instalando o gtest separadamente:
sudo apt install libgtest-dev
Alternativamente, você pode definir HWY_TEST_STANDALONE=1
e remover todas as ocorrências de gtest_main
em cada arquivo BUILD, então os testes evitam a dependência do GUnit.
A execução de testes compilados cruzados requer suporte do sistema operacional, que no Debian é fornecido pelo pacote qemu-user-binfmt
.
Para construir Highway como uma biblioteca compartilhada ou estática (dependendo de BUILD_SHARED_LIBS), o fluxo de trabalho padrão do CMake pode ser usado:
mkdir -p build && cd build
cmake ..
make -j && make test
Ou você pode executar run_tests.sh
( run_tests.bat
no Windows).
O Bazel também é compatível com construção, mas não é tão amplamente usado/testado.
Ao compilar para Armv7, uma limitação dos compiladores atuais exige que você adicione -DHWY_CMAKE_ARM7:BOOL=ON
à linha de comando do CMake; veja #834 e #1032. Entendemos que está em andamento um trabalho para remover essa limitação.
A construção em x86 de 32 bits não é oficialmente suportada e o AVX2/3 está desabilitado por padrão. Observe que johnplatts construiu e executou com sucesso os testes Highway em x86 de 32 bits, incluindo AVX2/3, em GCC 7/8 e Clang 8/11/12. No Ubuntu 22.04, Clang 11 e 12, mas não em versões posteriores, exigem sinalizadores extras do compilador -m32 -isystem /usr/i686-linux-gnu/include
. Clang 10 e anteriores requerem o plus acima -isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
. Consulte #1279.
rodovia agora está disponível em vcpkg
vcpkg install highway
A porta rodoviária no vcpkg é mantida atualizada pelos membros da equipe da Microsoft e colaboradores da comunidade. Se a versão estiver desatualizada, crie um problema ou solicitação pull no repositório vcpkg.
Você pode usar o benchmark
dentro de samples/ como ponto de partida.
Uma página de referência rápida lista resumidamente todas as operações e seus parâmetros, e a instrução_matrix indica o número de instruções por operação.
O FAQ responde perguntas sobre portabilidade, design de API e onde encontrar mais informações.
Recomendamos o uso de vetores SIMD completos sempre que possível para máxima portabilidade de desempenho. Para obtê-los, passe uma tag ScalableTag
(ou equivalentemente HWY_FULL(float)
) para funções como Zero/Set/Load
. Existem duas alternativas para casos de uso que exigem um limite superior nas pistas:
Para até N
pistas, especifique CappedTag
ou o equivalente HWY_CAPPED(T, N)
. O número real de pistas será N
arredondado para a potência de dois mais próxima, como 4 se N
for 5 ou 8 se N
for 8. Isso é útil para estruturas de dados como uma matriz estreita. Um loop ainda é necessário porque os vetores podem, na verdade, ter menos de N
pistas.
Para exatamente uma potência de duas N
pistas, especifique FixedTag
. O maior N
suportado depende do alvo, mas é garantido que será pelo menos 16/sizeof(T)
.
Devido às restrições da ADL, o código do usuário que chama as operações rodoviárias deve:
namespace hwy { namespace HWY_NAMESPACE {
; ounamespace hn = hwy::HWY_NAMESPACE; hn::Add()
; ouusing hwy::HWY_NAMESPACE::Add;
. Além disso, cada função que chama operações de rodovia (como Load
) deve ser prefixada com HWY_ATTR
OU residir entre HWY_BEFORE_NAMESPACE()
e HWY_AFTER_NAMESPACE()
. As funções Lambda atualmente exigem HWY_ATTR
antes da chave de abertura.
Não use escopo de namespace nem inicializadores static
para vetores SIMD porque isso pode causar SIGILL ao usar o despacho de tempo de execução e o compilador escolhe um inicializador compilado para um destino não suportado pela CPU atual. Em vez disso, as constantes inicializadas via Set
geralmente devem ser variáveis locais (const).
Os pontos de entrada no código usando Highway diferem ligeiramente dependendo se usam despacho estático ou dinâmico. Em ambos os casos, recomendamos que a função de nível superior receba um ou mais ponteiros para matrizes, em vez de tipos de vetores específicos de destino.
Para despacho estático, HWY_TARGET
será o melhor alvo disponível entre HWY_BASELINE_TARGETS
, ou seja, aqueles permitidos para uso pelo compilador (ver referência rápida). Funções dentro de HWY_NAMESPACE
podem ser chamadas usando HWY_STATIC_DISPATCH(func)(args)
dentro do mesmo módulo em que estão definidas. Você pode chamar a função de outros módulos envolvendo-a em uma função regular e declarando a função regular em um cabeçalho.
Para despacho dinâmico, uma tabela de ponteiros de função é gerada por meio da macro HWY_EXPORT
que é usada por HWY_DYNAMIC_DISPATCH(func)(args)
para chamar o melhor ponteiro de função para os destinos suportados pela CPU atual. Um módulo é compilado automaticamente para cada destino em HWY_TARGETS
(veja referência rápida) se HWY_TARGET_INCLUDE
estiver definido e foreach_target.h
estiver incluído. Observe que a primeira invocação de HWY_DYNAMIC_DISPATCH
, ou cada chamada ao ponteiro retornado pela primeira invocação de HWY_DYNAMIC_POINTER
, envolve alguma sobrecarga de detecção de CPU. Você pode evitar isso chamando o seguinte antes de qualquer invocação de HWY_DYNAMIC_*
: hwy::GetChosenTarget().Update(hwy::SupportedTargets());
.
Veja também uma introdução separada ao envio dinâmico por @kfjahnke.
Ao usar o envio dinâmico, foreach_target.h
é incluído nas unidades de tradução (arquivos .cc), não nos cabeçalhos. Cabeçalhos contendo código vetorial compartilhado entre várias unidades de tradução requerem uma proteção de inclusão especial, por exemplo, o seguinte retirado de examples/skeleton-inl.h
:
#if defined(HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_) == defined(HWY_TARGET_TOGGLE)
#ifdef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#undef HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#else
#define HIGHWAY_HWY_EXAMPLES_SKELETON_INL_H_
#endif
#include "hwy/highway.h"
// Your vector code
#endif
Por convenção, chamamos esses cabeçalhos -inl.h
porque seu conteúdo (geralmente modelos de função) geralmente está embutido.
Os aplicativos devem ser compilados com otimizações habilitadas. Sem inlining, o código SIMD pode ficar lento por fatores de 10 a 100. Para clang e GCC, -O2
geralmente é suficiente.
Para MSVC, recomendamos compilar com /Gv
para permitir que funções não embutidas passem argumentos vetoriais em registros. Se pretender usar o alvo AVX2 junto com vetores de meia largura (por exemplo, para PromoteTo
), também é importante compilar com /arch:AVX2
. Esta parece ser a única maneira de gerar instruções SSE codificadas em VEX de maneira confiável no MSVC. Às vezes, o MSVC gera instruções SSE codificadas em VEX, se forem misturadas com AVX, mas nem sempre, consulte DevCom-10618264. Caso contrário, misturar instruções AVX2 codificadas em VEX e SSE não VEX pode causar grave degradação de desempenho. Infelizmente, com a opção /arch:AVX2
, o binário resultante exigirá AVX2. Observe que esse sinalizador não é necessário para clang e GCC porque eles suportam atributos específicos de destino, que usamos para garantir a geração adequada de código VEX para destinos AVX2.
Ao vetorizar um loop, uma questão importante é se e como lidar com uma série de iterações ('contagem de viagens', denotada por count
) que não dividem uniformemente o tamanho do vetor N = Lanes(d)
. Por exemplo, pode ser necessário evitar escrever além do final de um array.
Nesta seção, deixe T
denotar o tipo de elemento e d = ScalableTag
. Suponha que o corpo do loop seja fornecido como uma função template
.
"Strip-mining" é uma técnica para vetorizar um loop, transformando-o em um loop externo e um loop interno, de modo que o número de iterações no loop interno corresponda à largura do vetor. Então, o loop interno é substituído por operações vetoriais.
Highway oferece diversas estratégias para vetorização de loop:
Certifique-se de que todas as entradas/saídas estejam preenchidas. Então o loop (externo) é simplesmente
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
Aqui, o parâmetro do modelo e o segundo argumento da função não são necessários.
Esta é a opção preferida, a menos que N
esteja na casa dos milhares e as operações vetoriais sejam canalizadas com longas latências. Este foi o caso dos supercomputadores nos anos 90, mas hoje em dia as ALUs são baratas e vemos a maioria das implementações dividir vetores em 1, 2 ou 4 partes, portanto há pouco custo para processar vetores inteiros, mesmo que não precisemos de todas as suas pistas. Na verdade, isso evita o custo (potencialmente grande) de predicação ou carregamentos/armazenamentos parciais em alvos mais antigos e não duplica o código.
Processe vetores inteiros e inclua elementos processados anteriormente no último vetor:
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
Esta é a segunda opção preferida, desde que count >= N
e LoopBody
seja idempotente. Alguns elementos podem ser processados duas vezes, mas um único caminho de código e uma vetorização completa geralmente valem a pena. Mesmo que count < N
, geralmente faz sentido preencher entradas/saídas até N
.
Use as funções Transform*
em hwy/contrib/algo/transform-inl.h. Isso cuida do manuseio do loop e do restante e você simplesmente define uma função lambda genérica (C++ 14) ou functor que recebe o vetor atual da matriz de entrada/saída, além de, opcionalmente, vetores de até duas matrizes de entrada extras e retorna o valor a ser gravado na matriz de entrada/saída.
Aqui está um exemplo de implementação da função BLAS SAXPY ( alpha * x + y
):
Transform1(d, x, n, y, [](auto d, const auto v, const auto v1) HWY_ATTR {
return MulAdd(Set(d, alpha), v, v1);
});
Processe vetores inteiros como acima, seguido por um loop escalar:
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
O parâmetro do modelo e os argumentos da segunda função novamente não são necessários.
Isso evita a duplicação de código e é razoável se count
for grande. Se count
for pequena, o segundo loop poderá ser mais lento que a próxima opção.
Processe vetores inteiros como acima, seguido por uma única chamada para um LoopBody
modificado com mascaramento:
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
Agora, o parâmetro do modelo e o terceiro argumento da função podem ser usados dentro de LoopBody
para 'combinar' de forma não atômica as primeiras pistas num_remaining
de v
com o conteúdo anterior da memória em locais subsequentes: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
. Da mesma forma, MaskedLoad(FirstN(d, num_remaining), d, pointer)
carrega os primeiros num_remaining
elementos e retorna zero em outras pistas.
Este é um bom padrão quando é inviável garantir que os vetores sejam preenchidos, mas só é seguro #if !HWY_MEM_OPS_MIGHT_FAULT
! Em contraste com o loop escalar, apenas uma única iteração final é necessária. Espera-se que o tamanho aumentado do código de dois corpos de loop valha a pena porque evita o custo de mascaramento em tudo, exceto na iteração final.
Usamos farm-sve de Berenger Bramas; provou ser útil para verificar a porta SVE em uma máquina de desenvolvimento x86.
Este não é um produto do Google com suporte oficial. Contato: [email protected]