Highway es una biblioteca de C++ que proporciona elementos intrínsecos SIMD/vectoriales portátiles.
Documentación
Anteriormente con licencia Apache 2, ahora con doble licencia como Apache 2/BSD-3.
Nos apasiona el software de alto rendimiento. Vemos un gran potencial sin explotar en las CPU (servidores, dispositivos móviles, computadoras de escritorio). Highway es para ingenieros que desean superar de manera confiable y económica los límites de lo que es posible en software.
Las CPU proporcionan instrucciones SIMD/vectoriales que aplican la misma operación a múltiples elementos de datos. Esto puede reducir el uso de energía, por ejemplo cinco veces , porque se ejecutan menos instrucciones. También vemos a menudo aceleraciones de 5 a 10 veces .
Highway hace que la programación SIMD/vectorial sea práctica y viable de acuerdo con estos principios rectores:
Hace lo que espera : Highway es una biblioteca de C++ con funciones cuidadosamente elegidas que se asignan bien a las instrucciones de la CPU sin grandes transformaciones del compilador. El código resultante es más predecible y robusto a los cambios de código/actualizaciones del compilador que la autovectorización.
Funciona en plataformas ampliamente utilizadas : Highway admite cinco arquitecturas; el mismo código de aplicación puede apuntar a varios conjuntos de instrucciones, incluidos aquellos con vectores "escalables" (tamaño desconocido en el momento de la compilación). Highway sólo requiere C++11 y admite cuatro familias de compiladores. Si desea utilizar Highway en otras plataformas, plantee un problema.
Flexible de implementar : las aplicaciones que utilizan Highway pueden ejecutarse en nubes o dispositivos cliente heterogéneos, eligiendo el mejor conjunto de instrucciones disponible en tiempo de ejecución. Alternativamente, los desarrolladores pueden optar por apuntar a un único conjunto de instrucciones sin ninguna sobrecarga de tiempo de ejecución. En ambos casos, el código de la aplicación es el mismo excepto por el intercambio de HWY_STATIC_DISPATCH
con HWY_DYNAMIC_DISPATCH
más una línea de código. Consulte también la introducción de @kfjahnke al despacho.
Adecuado para una variedad de dominios : Highway proporciona un amplio conjunto de operaciones, utilizadas para procesamiento de imágenes (punto flotante), compresión, análisis de video, álgebra lineal, criptografía, clasificación y generación aleatoria. Reconocemos que los nuevos casos de uso pueden requerir operaciones adicionales y nos complace agregarlas cuando tenga sentido (por ejemplo, sin límites de rendimiento en algunas arquitecturas). Si desea conversar, presente un problema.
Recompensa el diseño de datos paralelos : Highway proporciona herramientas como Gather, MaskedLoad y FixedTag para permitir aceleraciones para estructuras de datos heredadas. Sin embargo, las mayores ganancias se obtienen mediante el diseño de algoritmos y estructuras de datos para vectores escalables. Las técnicas útiles incluyen procesamiento por lotes, diseños de estructura de matriz y asignaciones alineadas/rellenadas.
Recomendamos estos recursos para comenzar:
Demostraciones en línea usando Compiler Explorer:
Observamos que se hace referencia a Highway en los siguientes proyectos de código abierto, que se encuentran a través de sourcegraph.com. La mayoría son repositorios de GitHub. Si desea agregar su proyecto o vincularlo directamente, no dude en plantear un problema o contactarnos a través del siguiente correo electrónico.
Otro
Si desea obtener Highway, además de clonarlo desde este repositorio de GitHub o usarlo como un submódulo de Git, también puede encontrarlo en los siguientes administradores de paquetes o repositorios:
Consulte también la lista en https://repology.org/project/highway-simd-library/versions.
Highway admite 24 objetivos, enumerados en orden alfabético 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, aún no compatible debido a errores del compilador, consulte el n.º 1207; también requiere QEMU 7.2);RVV
(1,0);WASM
, WASM_EMU256
(una versión 2x desenrollada de wasm128, habilitada si se define HWY_WANT_WASM2
. Seguirá siendo compatible hasta que sea potencialmente reemplazada por una versión futura de WASM);SSE2
SSSE3
(~Intel Core)SSE4
(~Nehalem, también incluye AES + CLMUL).AVX2
(~Haswell, también incluye BMI2 + F16 + FMA)AVX3
(~Skylake, AVX-512F/B/N/CD/DQ/VL)AVX3_DL
(~Icelake, incluye BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT; requiere participación definiendo HWY_WANT_AVX3_DL
a menos que se compile para envío estático),AVX3_ZEN4
(como AVX3_DL pero optimizado para AMD Zen4; requiere autorización definiendo HWY_WANT_AVX3_ZEN4
si se compila para envío estático, pero habilitado de forma predeterminada para envío en tiempo de ejecución),AVX3_SPR
(~Sapphire Rapids, incluye AVX-512FP16)Nuestra política es que, a menos que se especifique lo contrario, los objetivos seguirán siendo compatibles siempre que puedan compilarse (de forma cruzada) con Clang o GCC actualmente compatibles y probarse con QEMU. Si el objetivo se puede compilar con el troncal LLVM y probar usando nuestra versión de QEMU sin indicadores adicionales, entonces es elegible para su inclusión en nuestra infraestructura de prueba continua. De lo contrario, el objetivo se probará manualmente antes de los lanzamientos con versiones/configuraciones seleccionadas de Clang y GCC.
SVE se probó inicialmente usando farm_sve (ver agradecimientos).
Los lanzamientos de autopista pretenden seguir el sistema semver.org (MAJOR.MINOR.PATCH), incrementando MINOR después de adiciones compatibles con versiones anteriores y PATCH después de correcciones compatibles con versiones anteriores. Recomendamos utilizar versiones (en lugar del consejo de Git) porque se prueban más exhaustivamente, consulte a continuación.
La versión actual 1.0 indica un mayor enfoque en la compatibilidad con versiones anteriores. Las aplicaciones que utilizan funciones documentadas seguirán siendo compatibles con futuras actualizaciones que tengan el mismo número de versión principal.
Las pruebas de integración continua se crean con una versión reciente de Clang (que se ejecuta en x86 nativo o QEMU para RISC-V y Arm) y MSVC 2019 (v19.28, que se ejecuta en x86 nativo).
Antes de los lanzamientos, también probamos en x86 con Clang y GCC, y Armv7/8 mediante compilación cruzada de GCC. Consulte el proceso de prueba para obtener más detalles.
El directorio contrib
contiene utilidades relacionadas con SIMD: una clase de imagen con filas alineadas, una biblioteca matemática (16 funciones ya implementadas, en su mayoría trigonometría) y funciones para calcular productos escalares y ordenar.
Si solo necesita compatibilidad con x86, también puede utilizar la biblioteca de clases vectoriales VCL de Agner Fog. Incluye muchas funciones, incluida una biblioteca matemática completa.
Si tiene código existente que utiliza intrínsecos x86/NEON, puede que le interese SIMDe, que emula esos intrínsecos utilizando los intrínsecos o la autovectorización de otras plataformas.
Este proyecto utiliza CMake para generar y construir. En un sistema basado en Debian puedes instalarlo mediante:
sudo apt install cmake
Las pruebas unitarias de Highway utilizan googletest. De forma predeterminada, CMake de Highway descarga esta dependencia en el momento de la configuración. Puede evitar esto configurando la variable HWY_SYSTEM_GTEST
CMake en ON e instalando gtest por separado:
sudo apt install libgtest-dev
Alternativamente, puede definir HWY_TEST_STANDALONE=1
y eliminar todas las apariciones de gtest_main
en cada archivo BUILD, luego las pruebas evitan la dependencia de GUnit.
La ejecución de pruebas con compilación cruzada requiere soporte del sistema operativo, que en Debian lo proporciona el paquete qemu-user-binfmt
.
Para crear Highway como una biblioteca compartida o estática (según BUILD_SHARED_LIBS), se puede utilizar el flujo de trabajo estándar de CMake:
mkdir -p build && cd build
cmake ..
make -j && make test
O puede ejecutar run_tests.sh
( run_tests.bat
en Windows).
Bazel también es compatible con la construcción, pero no se utiliza ni se prueba con tanta frecuencia.
Al compilar para Armv7, una limitación de los compiladores actuales requiere que agregue -DHWY_CMAKE_ARM7:BOOL=ON
a la línea de comando de CMake; consulte los n.° 834 y n.° 1032. Entendemos que se está trabajando para eliminar esta limitación.
La compilación en x86 de 32 bits no es oficialmente compatible y AVX2/3 están deshabilitados de forma predeterminada allí. Tenga en cuenta que johnplatts ha creado y ejecutado con éxito las pruebas de Highway en x86 de 32 bits, incluido AVX2/3, en GCC 7/8 y Clang 8/11/12. En Ubuntu 22.04, Clang 11 y 12, pero no en versiones posteriores, requieren indicadores de compilación adicionales -m32 -isystem /usr/i686-linux-gnu/include
. Clang 10 y versiones anteriores requieren lo anterior más -isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
. Ver n.° 1279.
La autopista ahora está disponible en vcpkg.
vcpkg install highway
Los miembros del equipo de Microsoft y los contribuyentes de la comunidad mantienen actualizado el puerto de autopista en vcpkg. Si la versión no está actualizada, cree un problema o una solicitud de extracción en el repositorio de vcpkg.
Puede utilizar el benchmark
dentro de los ejemplos/ como punto de partida.
Una página de referencia rápida enumera brevemente todas las operaciones y sus parámetros, y la matriz de instrucciones indica el número de instrucciones por operación.
Las preguntas frecuentes responden preguntas sobre portabilidad, diseño de API y dónde encontrar más información.
Recomendamos utilizar vectores SIMD completos siempre que sea posible para obtener la máxima portabilidad del rendimiento. Para obtenerlos, pase una etiqueta ScalableTag
(o equivalentemente HWY_FULL(float)
) a funciones como Zero/Set/Load
. Hay dos alternativas para casos de uso que requieren un límite superior en los carriles:
Para hasta N
carriles, especifique CappedTag
o el equivalente HWY_CAPPED(T, N)
. El número real de carriles se redondeará N
a la potencia de dos más cercana, como 4 si N
es 5 u 8 si N
es 8. Esto es útil para estructuras de datos como una matriz estrecha. Todavía se requiere un bucle porque los vectores pueden tener menos de N
carriles.
Para exactamente una potencia de dos N
carriles, especifique FixedTag
. El N
más grande admitido depende del objetivo, pero se garantiza que será al menos 16/sizeof(T)
.
Debido a las restricciones de ADL, el código de usuario que llama a operaciones de autopista debe:
namespace hwy { namespace HWY_NAMESPACE {
; onamespace hn = hwy::HWY_NAMESPACE; hn::Add()
; ousing hwy::HWY_NAMESPACE::Add;
. Además, cada función que llama a operaciones de autopista (como Load
) debe tener el prefijo HWY_ATTR
O residir entre HWY_BEFORE_NAMESPACE()
y HWY_AFTER_NAMESPACE()
. Las funciones Lambda actualmente requieren HWY_ATTR
antes de su llave de apertura.
No use el alcance del espacio de nombres ni inicializadores static
para los vectores SIMD porque esto puede causar SIGILL cuando se usa el envío en tiempo de ejecución y el compilador elige un inicializador compilado para un objetivo no admitido por la CPU actual. En cambio, las constantes inicializadas mediante Set
generalmente deberían ser variables locales (constantes).
Los puntos de entrada al código mediante Highway difieren ligeramente dependiendo de si utilizan despacho estático o dinámico. En ambos casos, recomendamos que la función de nivel superior reciba uno o más punteros a matrices, en lugar de tipos de vectores específicos de destino.
Para el envío estático, HWY_TARGET
será el mejor objetivo disponible entre HWY_BASELINE_TARGETS
, es decir, aquellos permitidos para su uso por el compilador (consulte la referencia rápida). Las funciones dentro de HWY_NAMESPACE
se pueden llamar usando HWY_STATIC_DISPATCH(func)(args)
dentro del mismo módulo en el que están definidas. Puede llamar a la función desde otros módulos envolviéndola en una función regular y declarando la función regular en un encabezado.
Para el envío dinámico, se genera una tabla de punteros de función a través de la macro HWY_EXPORT
que utiliza HWY_DYNAMIC_DISPATCH(func)(args)
para llamar al mejor puntero de función para los objetivos admitidos de la CPU actual. Se compila automáticamente un módulo para cada objetivo en HWY_TARGETS
(ver referencia rápida) si se define HWY_TARGET_INCLUDE
y se incluye foreach_target.h
. Tenga en cuenta que la primera invocación de HWY_DYNAMIC_DISPATCH
, o cada llamada al puntero devuelto por la primera invocación de HWY_DYNAMIC_POINTER
, implica cierta sobrecarga de detección de CPU. Puede evitar esto llamando a lo siguiente antes de cualquier invocación de HWY_DYNAMIC_*
: hwy::GetChosenTarget().Update(hwy::SupportedTargets());
.
Consulte también una introducción separada al envío dinámico de @kfjahnke.
Cuando se utiliza el envío dinámico, foreach_target.h
se incluye en las unidades de traducción (archivos .cc), no en los encabezados. Los encabezados que contienen código vectorial compartido entre varias unidades de traducción requieren una protección de inclusión especial, por ejemplo, el siguiente tomado 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 convención, denominamos a dichos encabezados -inl.h
porque su contenido (a menudo plantillas de funciones) suele estar integrado.
Las aplicaciones deben compilarse con las optimizaciones habilitadas. Sin insertar el código SIMD, la velocidad puede disminuir en factores de 10 a 100. Para clang y GCC, -O2
generalmente es suficiente.
Para MSVC, recomendamos compilar con /Gv
para permitir que funciones no integradas pasen argumentos vectoriales en registros. Si pretende utilizar el destino AVX2 junto con vectores de ancho medio (por ejemplo, para PromoteTo
), también es importante compilar con /arch:AVX2
. Esta parece ser la única forma de generar de manera confiable instrucciones SSE codificadas con VEX en MSVC. A veces, MSVC genera instrucciones SSE codificadas en VEX, si se mezclan con AVX, pero no siempre, consulte DevCom-10618264. De lo contrario, mezclar instrucciones AVX2 codificadas con VEX y SSE que no sean VEX puede provocar una degradación grave del rendimiento. Desafortunadamente, con la opción /arch:AVX2
, el binario resultante requerirá AVX2. Tenga en cuenta que no se necesita dicho indicador para clang y GCC porque admiten atributos específicos del objetivo, que utilizamos para garantizar la generación adecuada de código VEX para objetivos AVX2.
Al vectorizar un bucle, una pregunta importante es si se debe lidiar con una cantidad de iteraciones ('recuento de viajes', denotado count
) que no divide uniformemente el tamaño del vector N = Lanes(d)
y cómo hacerlo. Por ejemplo, puede que sea necesario evitar escribir más allá del final de una matriz.
En esta sección, sea T
el tipo de elemento y d = ScalableTag
. Supongamos que el cuerpo del bucle se proporciona como una template
.
La "minería a cielo abierto" es una técnica para vectorizar un bucle transformándolo en un bucle externo y un bucle interno, de modo que el número de iteraciones en el bucle interno coincida con el ancho del vector. Luego, el bucle interno se reemplaza con operaciones vectoriales.
Highway ofrece varias estrategias para la vectorización de bucles:
Asegúrese de que todas las entradas/salidas estén rellenas. Entonces el bucle (exterior) es simplemente
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
Aquí, el parámetro de plantilla y el segundo argumento de función no son necesarios.
Esta es la opción preferida, a menos que N
sea de miles y las operaciones vectoriales se canalicen con latencias largas. Este fue el caso de las supercomputadoras en los años 90, pero hoy en día las ALU son baratas y vemos que la mayoría de las implementaciones dividen los vectores en 1, 2 o 4 partes, por lo que procesar vectores completos tiene poco costo incluso si no necesitamos todos sus carriles. De hecho, esto evita el costo (potencialmente alto) de la predicación o las cargas/almacenamiento parciales en objetivos más antiguos y no duplica el código.
Procese vectores completos e incluya elementos previamente procesados en el último vector:
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
Esta es la segunda opción preferida siempre que count >= N
y LoopBody
sea idempotente. Algunos elementos pueden procesarse dos veces, pero normalmente vale la pena una única ruta de código y una vectorización completa. Incluso si count < N
, normalmente tiene sentido rellenar las entradas/salidas hasta N
.
Utilice las funciones Transform*
en hwy/contrib/algo/transform-inl.h. Esto se encarga del manejo del bucle y del resto y usted simplemente define una función lambda genérica (C++14) o funtor que recibe el vector actual de la matriz de entrada/salida, además de, opcionalmente, vectores de hasta dos matrices de entrada adicionales, y devuelve el valor a escribir en la matriz de entrada/salida.
Aquí hay un ejemplo que implementa la función 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);
});
Procese vectores completos como se indica arriba, seguido de un bucle escalar:
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
El parámetro de plantilla y los argumentos de la segunda función tampoco son necesarios.
Esto evita la duplicación de código y es razonable si count
es grande. Si count
es pequeño, el segundo ciclo puede ser más lento que la siguiente opción.
Procese vectores completos como se indica arriba, seguido de una única llamada a un LoopBody
modificado con enmascaramiento:
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
Ahora el parámetro de plantilla y el tercer argumento de función se pueden usar dentro de LoopBody
para 'mezclar' de forma no atómica los primeros carriles num_remaining
de v
con el contenido anterior de la memoria en ubicaciones posteriores: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
. De manera similar, MaskedLoad(FirstN(d, num_remaining), d, pointer)
carga los primeros num_remaining
elementos y devuelve cero en otros carriles.
Este es un buen valor predeterminado cuando no es factible garantizar que los vectores estén rellenos, pero solo es seguro #if !HWY_MEM_OPS_MIGHT_FAULT
! A diferencia del bucle escalar, sólo se necesita una iteración final. Se espera que valga la pena aumentar el tamaño del código de dos cuerpos de bucle porque evita el costo del enmascaramiento en todas las iteraciones excepto en la final.
Hemos utilizado farm-sve de Berenger Bramas; Ha resultado útil para comprobar el puerto SVE en una máquina de desarrollo x86.
Este no es un producto de Google con soporte oficial. Contacto: [email protected]