Highway는 이식 가능한 SIMD/벡터 내장 함수를 제공하는 C++ 라이브러리입니다.
선적 서류 비치
이전에는 Apache 2에 따라 라이센스가 부여되었지만 이제는 Apache 2/BSD-3으로 이중 라이센스가 부여됩니다.
우리는 고성능 소프트웨어에 열정을 갖고 있습니다. 우리는 CPU(서버, 모바일, 데스크톱)에서 아직 개발되지 않은 주요 잠재력을 확인했습니다. Highway는 소프트웨어에서 가능한 것의 경계를 안정적이고 경제적으로 확장하려는 엔지니어를 위한 것입니다.
CPU는 여러 데이터 항목에 동일한 작업을 적용하는 SIMD/벡터 명령을 제공합니다. 이는 더 적은 수의 명령이 실행되기 때문에 에너지 사용량을 5배로 줄일 수 있습니다. 또한 속도가 5~10배 향상되는 경우도 종종 있습니다.
Highway는 다음 지침 원칙에 따라 SIMD/벡터 프로그래밍을 실용적이고 실행 가능하게 만듭니다.
예상대로 작동합니다 . Highway는 광범위한 컴파일러 변환 없이 CPU 명령어에 잘 매핑되는 신중하게 선택된 기능을 갖춘 C++ 라이브러리입니다. 결과 코드는 자동 벡터화보다 코드 변경/컴파일러 업데이트에 더 예측 가능하고 강력합니다.
널리 사용되는 플랫폼에서 작동 : Highway는 5가지 아키텍처를 지원합니다. 동일한 애플리케이션 코드는 '확장 가능한' 벡터(컴파일 시 알 수 없는 크기)를 포함하여 다양한 명령어 세트를 대상으로 할 수 있습니다. Highway에는 C++11만 필요하며 4가지 컴파일러 제품군을 지원합니다. 다른 플랫폼에서 고속도로를 사용하고 싶다면 문제를 제기해 주세요.
유연한 배포 : Highway를 사용하는 애플리케이션은 런타임에 사용 가능한 최상의 명령 세트를 선택하여 이기종 클라우드 또는 클라이언트 장치에서 실행될 수 있습니다. 또는 개발자는 런타임 오버헤드 없이 단일 명령 세트를 대상으로 하도록 선택할 수 있습니다. 두 경우 모두 HWY_STATIC_DISPATCH
HWY_DYNAMIC_DISPATCH
로 바꾸는 것과 한 줄의 코드를 제외하고 애플리케이션 코드는 동일합니다. @kfjahnke의 파견 소개도 참조하세요.
다양한 도메인에 적합 : Highway는 이미지 처리(부동 소수점), 압축, 비디오 분석, 선형 대수, 암호화, 정렬 및 무작위 생성에 사용되는 광범위한 작업 세트를 제공합니다. 우리는 새로운 사용 사례에 추가 작업이 필요할 수 있다는 점을 인식하고 합리적인 곳에 이를 추가하게 되어 기쁘게 생각합니다(예: 일부 아키텍처에서는 성능 절벽이 없음). 논의하고 싶다면 이슈를 제출해 주세요.
데이터 병렬 설계 보상 : Highway는 레거시 데이터 구조의 속도 향상을 가능하게 하는 Gather, MaskedLoad 및 FixTag와 같은 도구를 제공합니다. 그러나 확장 가능한 벡터에 대한 알고리즘과 데이터 구조를 설계하면 가장 큰 이점을 얻을 수 있습니다. 유용한 기술에는 일괄 처리, 배열 구조 레이아웃, 정렬/패딩 할당이 포함됩니다.
시작하려면 다음 리소스를 권장합니다.
컴파일러 탐색기를 사용한 온라인 데모:
우리는 sourcegraph.com을 통해 찾은 다음 오픈 소스 프로젝트에서 Highway가 참조되는 것을 확인했습니다. 대부분은 GitHub 저장소입니다. 프로젝트를 추가하거나 직접 링크하고 싶다면 언제든지 문제를 제기하거나 아래 이메일을 통해 문의해 주세요.
다른
Highway를 얻으려면 이 GitHub 저장소에서 복제하거나 Git 하위 모듈로 사용하는 것 외에도 다음 패키지 관리자 또는 저장소에서 찾을 수도 있습니다.
https://repology.org/project/highway-simd-library/versions의 목록도 참조하세요.
고속도로는 플랫폼의 알파벳 순서로 나열된 24개의 대상을 지원합니다.
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, 컴파일러 버그로 인해 아직 지원되지 않음, #1207 참조, QEMU 7.2 필요)RVV
(1.0);WASM
, WASM_EMU256
(wasm128의 2배 전개 버전, HWY_WANT_WASM2
정의된 경우 활성화됨. 이는 잠재적으로 WASM의 향후 버전으로 대체될 때까지 계속 지원됩니다.)SSE2
SSSE3
(~인텔 코어)SSE4
(~Nehalem, AES + CLMUL도 포함).AVX2
(~Haswell, BMI2 + F16 + FMA도 포함)AVX3
(~스카이레이크, AVX-512F/BW/CD/DQ/VL)AVX3_DL
(~Icelake, BitAlg + CLMUL + GFNI + VAES + VBMI + VBMI2 + VNNI + VPOPCNT 포함, 정적 디스패치를 위해 컴파일하지 않는 한 HWY_WANT_AVX3_DL
정의하여 옵트인 필요),AVX3_ZEN4
(AVX3_DL과 유사하지만 AMD Zen4에 최적화됨. 정적 디스패치를 위해 컴파일하는 경우 HWY_WANT_AVX3_ZEN4
정의하여 옵트인이 필요하지만 런타임 디스패치의 경우 기본적으로 활성화됨)AVX3_SPR
(~Sapphire Rapids, AVX-512FP16 포함)우리의 정책은 달리 지정하지 않는 한 대상이 현재 지원되는 Clang 또는 GCC로 (교차) 컴파일되고 QEMU를 사용하여 테스트될 수 있는 한 계속 지원된다는 것입니다. 대상을 LLVM 트렁크로 컴파일하고 추가 플래그 없이 QEMU 버전을 사용하여 테스트할 수 있는 경우 지속적인 테스트 인프라에 포함될 수 있습니다. 그렇지 않으면 Clang 및 GCC의 선택된 버전/구성이 포함된 릴리스가 출시되기 전에 대상이 수동으로 테스트됩니다.
SVE는 처음에 farm_sve를 사용하여 테스트되었습니다(감사의 글 참조).
Highway 릴리스는 semver.org 시스템(MAJOR.MINOR.PATCH)을 따르는 것을 목표로 하며, 이전 버전과 호환되는 추가 후에는 MINOR를 증가시키고 이전 버전과 호환되는 수정 후에는 PATCH를 증가시킵니다. Git 팁 대신 릴리스를 사용하는 것이 좋습니다. 릴리스는 더 광범위하게 테스트되었기 때문입니다. 아래를 참조하세요.
현재 버전 1.0은 이전 버전과의 호환성에 대한 관심이 높아졌음을 나타냅니다. 문서화된 기능을 사용하는 애플리케이션은 동일한 주요 버전 번호를 갖는 향후 업데이트와 계속 호환됩니다.
지속적인 통합 테스트는 최신 버전의 Clang(기본 x86에서 실행 또는 RISC-V 및 Arm의 경우 QEMU)과 MSVC 2019(v19.28, 기본 x86에서 실행)를 사용하여 구축됩니다.
릴리스 전에는 Clang 및 GCC를 사용하여 x86에서, GCC 크로스 컴파일을 통해 Armv7/8에서도 테스트했습니다. 자세한 내용은 테스트 프로세스를 참조하세요.
contrib
디렉토리에는 정렬된 행이 있는 이미지 클래스, 수학 라이브러리(대부분 삼각법이 이미 구현된 16개 함수), 내적 계산 및 정렬을 위한 함수 등 SIMD 관련 유틸리티가 포함되어 있습니다.
x86 지원만 필요한 경우 Agner Fog의 VCL 벡터 클래스 라이브러리를 사용할 수도 있습니다. 여기에는 완전한 수학 라이브러리를 포함한 많은 기능이 포함되어 있습니다.
x86/NEON 내장 함수를 사용하는 기존 코드가 있는 경우 다른 플랫폼의 내장 함수 또는 자동 벡터화를 사용하여 해당 내장 함수를 에뮬레이트하는 SIMDe에 관심이 있을 수 있습니다.
이 프로젝트는 CMake를 사용하여 생성하고 빌드합니다. Debian 기반 시스템에서는 다음을 통해 설치할 수 있습니다.
sudo apt install cmake
Highway의 단위 테스트는 googletest를 사용합니다. 기본적으로 Highway의 CMake는 구성 시 이 종속성을 다운로드합니다. HWY_SYSTEM_GTEST
CMake 변수를 ON으로 설정하고 gtest를 별도로 설치하면 이를 방지할 수 있습니다.
sudo apt install libgtest-dev
또는 HWY_TEST_STANDALONE=1
정의하고 각 BUILD 파일에서 gtest_main
을 모두 제거한 다음 테스트에서 GUnit에 대한 종속성을 피할 수 있습니다.
크로스 컴파일된 테스트를 실행하려면 OS의 지원이 필요하며 Debian에서는 qemu-user-binfmt
패키지에서 제공됩니다.
Highway를 공유 또는 정적 라이브러리(BUILD_SHARED_LIBS에 따라 다름)로 빌드하려면 표준 CMake 워크플로를 사용할 수 있습니다.
mkdir -p build && cd build
cmake ..
make -j && make test
또는 run_tests.sh
(Windows의 경우 run_tests.bat
)를 실행할 수 있습니다.
Bazel은 빌드에도 지원되지만 널리 사용/테스트되지는 않습니다.
Armv7용으로 빌드할 때 현재 컴파일러의 제한 사항으로 인해 CMake 명령줄에 -DHWY_CMAKE_ARM7:BOOL=ON
추가해야 합니다. #834 및 #1032를 참조하세요. 우리는 이러한 제한을 없애기 위한 작업이 진행 중이라는 것을 알고 있습니다.
32비트 x86에서의 빌드는 공식적으로 지원되지 않으며 AVX2/3은 기본적으로 비활성화되어 있습니다. johnplatts는 GCC 7/8 및 Clang 8/11/12에서 AVX2/3을 포함하여 32비트 x86에서 고속도로 테스트를 성공적으로 구축하고 실행했습니다. Ubuntu 22.04에서 Clang 11 및 12(이후 버전 제외)에는 추가 컴파일러 플래그 -m32 -isystem /usr/i686-linux-gnu/include
필요합니다. Clang 10 이하에는 위와 -isystem /usr/i686-linux-gnu/include/c++/12/i686-linux-gnu
필요합니다. #1279를 참조하세요.
이제 vcpkg에서 고속도로를 사용할 수 있습니다.
vcpkg install highway
vcpkg의 고속도로 포트는 Microsoft 팀 구성원과 커뮤니티 기여자가 최신 상태로 유지됩니다. 버전이 오래된 경우 vcpkg 저장소에서 이슈 또는 풀 요청을 생성하세요.
예제/ 내부의 benchmark
시작점으로 사용할 수 있습니다.
빠른 참조 페이지에는 모든 작업과 해당 매개변수가 간략하게 나열되어 있으며, Instruction_matrix는 작업당 명령 수를 나타냅니다.
FAQ는 이식성, API 디자인 및 추가 정보를 찾을 수 있는 위치에 대한 질문에 답변합니다.
최대 성능 이식성을 위해 가능할 때마다 전체 SIMD 벡터를 사용하는 것이 좋습니다. 이를 얻으려면 ScalableTag
(또는 HWY_FULL(float)
와 동일) 태그를 Zero/Set/Load
와 같은 함수에 전달하세요. 레인의 상한이 필요한 사용 사례에는 두 가지 대안이 있습니다.
최대 N
개 레인의 경우 CappedTag
또는 이에 상응하는 HWY_CAPPED(T, N)
지정하세요. 실제 레인 수는 N
가장 가까운 2의 거듭제곱으로 내림됩니다(예: N
이 5인 경우 4, N
이 8인 경우 8). 이는 좁은 행렬과 같은 데이터 구조에 유용합니다. 벡터에는 실제로 N
개보다 적은 레인이 있을 수 있으므로 루프가 여전히 필요합니다.
정확히 2개 N
레인의 거듭제곱을 얻으려면 FixedTag
지정하세요. 지원되는 가장 큰 N
대상에 따라 다르지만 최소 16/sizeof(T)
가 보장됩니다.
ADL 제한으로 인해 고속도로 작전을 호출하는 사용자 코드는 다음 중 하나를 수행해야 합니다.
namespace hwy { namespace HWY_NAMESPACE {
; 내부에 상주합니다. 또는namespace hn = hwy::HWY_NAMESPACE; hn::Add()
; 또는using hwy::HWY_NAMESPACE::Add;
. 또한 고속도로 작업(예: Load
)을 호출하는 각 함수는 HWY_ATTR
접두사가 있거나 HWY_BEFORE_NAMESPACE()
및 HWY_AFTER_NAMESPACE()
사이에 있어야 합니다. Lambda 함수는 현재 여는 중괄호 앞에 HWY_ATTR
필요합니다.
SIMD 벡터에는 네임스페이스 범위나 static
초기화 프로그램을 사용하지 마세요. 런타임 디스패치를 사용할 때 SIGILL이 발생할 수 있고 컴파일러가 현재 CPU에서 지원하지 않는 대상에 대해 컴파일된 초기화 프로그램을 선택하기 때문입니다. 대신 Set
통해 초기화된 상수는 일반적으로 로컬(const) 변수여야 합니다.
Highway를 사용하는 코드의 진입점은 정적 또는 동적 디스패치를 사용하는지 여부에 따라 약간 다릅니다. 두 경우 모두 최상위 함수가 대상별 벡터 유형이 아닌 배열에 대한 하나 이상의 포인터를 받는 것이 좋습니다.
정적 디스패치의 경우 HWY_TARGET
HWY_BASELINE_TARGETS
중에서 사용 가능한 가장 좋은 대상, 즉 컴파일러에서 사용하도록 허용된 대상이 됩니다(빠른 참조 참조). HWY_NAMESPACE
내부의 함수는 정의된 동일한 모듈 내에서 HWY_STATIC_DISPATCH(func)(args)
사용하여 호출할 수 있습니다. 일반 함수로 래핑하고 헤더에서 일반 함수를 선언하여 다른 모듈에서 함수를 호출할 수 있습니다.
동적 디스패치의 경우 HWY_DYNAMIC_DISPATCH(func)(args)
에서 사용되는 HWY_EXPORT
매크로를 통해 함수 포인터 테이블이 생성되어 현재 CPU가 지원하는 대상에 가장 적합한 함수 포인터를 호출합니다. HWY_TARGET_INCLUDE
정의되고 foreach_target.h
가 포함된 경우 HWY_TARGETS
(빠른 참조 참조)의 각 대상에 대해 모듈이 자동으로 컴파일됩니다. HWY_DYNAMIC_DISPATCH
의 첫 번째 호출 또는 HWY_DYNAMIC_POINTER
의 첫 번째 호출에서 반환된 포인터에 대한 각 호출에는 약간의 CPU 감지 오버헤드가 포함됩니다. HWY_DYNAMIC_*
를 호출하기 전에 다음을 호출하여 이를 방지할 수 있습니다. hwy::GetChosenTarget().Update(hwy::SupportedTargets());
.
@kfjahnke의 동적 디스패치에 대한 별도의 소개도 참조하세요.
동적 디스패치를 사용하는 경우 foreach_target.h
헤더가 아닌 번역 단위(.cc 파일)에서 포함됩니다. 여러 번역 단위 간에 공유되는 벡터 코드를 포함하는 헤더에는 특별한 포함 가드가 필요합니다. 예를 들어 다음은 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
일반적으로 해당 헤더의 내용(종종 함수 템플릿)이 인라인되기 때문에 이러한 헤더의 이름을 -inl.h
로 지정합니다.
애플리케이션은 최적화가 활성화된 상태로 컴파일되어야 합니다. SIMD 코드를 인라인하지 않으면 속도가 10~100배 정도 느려질 수 있습니다. clang 및 GCC의 경우 일반적으로 -O2
이면 충분합니다.
MSVC의 경우 인라인되지 않은 함수가 레지스터의 벡터 인수를 전달할 수 있도록 /Gv
사용하여 컴파일하는 것이 좋습니다. 반폭 벡터(예: PromoteTo
)와 함께 AVX2 대상을 사용하려는 경우 /arch:AVX2
로 컴파일하는 것도 중요합니다. 이는 MSVC에서 VEX로 인코딩된 SSE 명령어를 안정적으로 생성하는 유일한 방법인 것 같습니다. AVX와 혼합된 경우 MSVC가 VEX로 인코딩된 SSE 명령어를 생성하는 경우도 있지만 항상 그런 것은 아닙니다. DevCom-10618264를 참조하세요. 그렇지 않은 경우 VEX로 인코딩된 AVX2 명령어와 비 VEX SSE를 혼합하면 심각한 성능 저하가 발생할 수 있습니다. 안타깝게도 /arch:AVX2
옵션을 사용하면 결과 바이너리에 AVX2가 필요합니다. clang 및 GCC에는 AVX2 대상에 대한 적절한 VEX 코드 생성을 보장하는 데 사용되는 대상별 속성을 지원하기 때문에 이러한 플래그가 필요하지 않습니다.
루프를 벡터화할 때 중요한 질문은 벡터 크기 N = Lanes(d)
균등하게 나누지 않는 반복 횟수('여행 횟수', count
로 표시)를 처리할지 여부와 처리 방법입니다. 예를 들어, 배열의 끝을 지나서 쓰는 것을 피해야 할 수도 있습니다.
이 섹션에서는 T
요소 유형을 나타내고 d = ScalableTag
라고 가정합니다. 루프 본문이 template
함수로 제공된다고 가정합니다.
스트립마이닝(Strip-mining)은 내부 루프의 반복 횟수가 벡터 너비와 일치하도록 루프를 외부 루프와 내부 루프로 변환하여 벡터화하는 기술입니다. 그런 다음 내부 루프가 벡터 연산으로 대체됩니다.
Highway는 루프 벡터화를 위한 몇 가지 전략을 제공합니다.
모든 입력/출력이 채워졌는지 확인하세요. 그런 다음 (외부) 루프는 간단합니다.
for (size_t i = 0; i < count; i += N) LoopBody(d, i, 0);
여기서는 템플릿 매개변수와 두 번째 함수 인수가 필요하지 않습니다.
N
이 수천 개가 아니고 벡터 작업이 긴 대기 시간으로 파이프라인되지 않는 한 이는 선호되는 옵션입니다. 90년대 슈퍼컴퓨터의 경우도 마찬가지였지만 요즘 ALU는 가격이 저렴하고 대부분의 구현에서 벡터를 1, 2 또는 4개의 부분으로 분할하므로 모든 레인이 필요하지 않더라도 전체 벡터를 처리하는 데 드는 비용이 거의 없습니다. 실제로 이는 이전 대상에 대한 예측 또는 부분 로드/저장의 (잠재적으로 큰) 비용을 방지하고 코드를 복제하지 않습니다.
전체 벡터를 처리하고 이전에 처리된 요소를 마지막 벡터에 포함합니다.
for (size_t i = 0; i < count; i += N) LoopBody(d, HWY_MIN(i, count - N), 0);
이는 count >= N
이고 LoopBody
가 멱등성을 갖는 경우 두 번째로 선호되는 옵션입니다. 일부 요소는 두 번 처리될 수 있지만 단일 코드 경로와 전체 벡터화는 일반적으로 그만한 가치가 있습니다. count < N
인 경우에도 일반적으로 최대 N
까지 입력/출력을 채우는 것이 합리적입니다.
hwy/contrib/algo/transform-inl.h에서 Transform*
기능을 사용하세요. 이는 루프 및 나머지 처리를 처리하고 입력/출력 배열에서 현재 벡터와 선택적으로 최대 두 개의 추가 입력 배열에서 벡터를 수신하는 일반 람다 함수(C++14) 또는 펑터를 정의하고 반환합니다. 입력/출력 배열에 쓸 값입니다.
다음은 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);
});
위와 같이 전체 벡터를 처리한 후 스칼라 루프를 처리합니다.
size_t i = 0;
for (; i + N <= count; i += N) LoopBody(d, i, 0);
for (; i < count; ++i) LoopBody(CappedTag(), i, 0);
템플릿 매개변수와 두 번째 함수 인수는 다시 필요하지 않습니다.
이는 코드 중복을 방지하고 count
클 경우 합리적입니다. count
작으면 두 번째 루프가 다음 옵션보다 느릴 수 있습니다.
위와 같이 전체 벡터를 처리한 후 마스킹을 사용하여 수정된 LoopBody
를 단일 호출합니다.
size_t i = 0;
for (; i + N <= count; i += N) {
LoopBody(d, i, 0);
}
if (i < count) {
LoopBody(d, i, count - i);
}
이제 템플릿 매개변수와 세 번째 함수 인수를 LoopBody
내에서 사용하여 v
의 첫 번째 num_remaining
레인을 후속 위치의 이전 메모리 내용과 비원자적으로 '혼합'할 수 있습니다: BlendedStore(v, FirstN(d, num_remaining), d, pointer);
. 마찬가지로 MaskedLoad(FirstN(d, num_remaining), d, pointer)
첫 번째 num_remaining
요소를 로드하고 다른 레인에서는 0을 반환합니다.
이는 벡터가 채워지는 것을 보장하는 것이 불가능할 때 좋은 기본값이지만 #if !HWY_MEM_OPS_MIGHT_FAULT
에만 안전합니다! 스칼라 루프와 달리 최종 반복은 단 한 번만 필요합니다. 두 개의 루프 본문에서 증가된 코드 크기는 최종 반복을 제외한 모든 부분에서 마스킹 비용을 방지하므로 가치가 있을 것으로 예상됩니다.
우리는 Berenger Bramas의 farm-sve를 사용했습니다. x86 개발 시스템에서 SVE 포트를 확인하는 데 유용한 것으로 입증되었습니다.
이 제품은 공식적으로 지원되는 Google 제품이 아닙니다. 연락처: [email protected]