- 문자열을 복사하며 ASCII 대문자를 소문자로 바꾸는 작업을 AVX-512-BW로 64바이트씩 처리해, 작은 문자열에서도 SIMD 성능을 끌어내는 실험임
- 구현의 핵심은 각 바이트가
'A'이상'Z'이하인지 비교한 뒤, 해당 위치에만'a' - 'A'를 더하는 마스크 연산임 - 짧은 문자열과 긴 문자열의 남은 꼬리는 마스크드 load/store로 처리해, SIMD 코드가 흔히 겪는 작은 조각 처리 비용을 줄임
- Clang 16, Debian 11, AMD Ryzen 9 7950X에서 약 1MiB 복사를 1바이트~1KiB 청크로 측정한 결과,
tolower64는 비교 대상 중 꾸준히 빠른 축에 속함 - Zen 4에서는 AVX-512-BW가 문자열 처리에 잘 맞는 모습을 보였지만, ARM SVE와 RISC-V Vector 확장은 직접 자세히 검증하지 못함
AVX-512-BW로 64바이트 tolower() 만들기
- 목표는 문자열을 복사하면서 대문자 ASCII 문자를 소문자로 바꾸는
tolower()커널을 SIMD로 구현하는 것임 - AVX-512-BW는 바이트와 워드 단위 연산을 지원하는 확장으로, 최근 AMD Zen 프로세서에서 사용할 수 있음
- AVX-512는 여러 확장으로 나뉘어 지원 여부가 복잡함
- Intel 쪽 지원은 특히 일정하지 않다고 평가함
- ARM SVE도 문자열 처리에 적합한 바이트 단위 마스크드 load/store를 제공함
- 최근 big-ARM Neoverse 코어, 예를 들어 Amazon Graviton에서 사용 가능함
- Apple Silicon에서는 사용할 수 없음
- RISC-V Vector extension도 ARM SVE와 비슷한 스타일이며, 여러 소형 싱글보드 컴퓨터에서 사용할 수 있음
tolower64()의 동작 방식
tolower64()는 한 번에 64바이트를 처리하는 AVX-512 기반 커널임- 먼저 64개 바이트가 들어 있는 벡터 레지스터에 기준값을 채움
'A''Z''a' - 'A'
- 입력 문자 벡터
c를'A','Z'와 비교해 각각 64비트 마스크를 만듦c >= 'A'인 위치c <= 'Z'인 위치
- 두 마스크를
_kand_mask64()로 결합해 대문자 위치만 표시하는is_upper마스크를 만듦 - 마지막으로
_mm512_mask_add_epi8()를 적용함is_upper가 false인 바이트는 원래c를 유지함is_upper가 true인 바이트는c + ('a' - 'A')가 됨
긴 문자열과 짧은 문자열 처리
- 긴 문자열의 대부분은 일반적인 비정렬 벡터 load/store로 처리함
_mm512_loadu_epi8()tolower64()_mm512_storeu_epi8()
- 짧은 문자열과 긴 문자열의 마지막 남은 조각에는 마스크드 비정렬 load/store를 사용함
- 마스크는 낮은 쪽
len비트만 켜진 형태로 만듦uint64_t len_bits = (~0ULL) >> (64 - len)_cvtu64_mask64(len_bits)로 SIMD 마스크 레지스터에 올림
_mm512_maskz_loadu_epi8()는 마스크가 꺼진 위치의 목적지 레지스터를 0으로 채움_mm512_mask_storeu_epi8()는 마스크가 켜진 위치만 저장함- 이 방식이 작은 문자열 조각을 빠르게 처리하는 핵심임
벤치마크 조건과 비교 대상
- 벤치마크는 Clang 16, Debian 11, AMD Ryzen 9 7950X에서 실행함
- 측정 대상은 약 1MiB 복사이며, 청크 길이는 1바이트부터 1KiB까지 바꿈
- 소스와 목적지 문자열의 정렬 차이를 반영하기 위해 각 문자열 사이에 몇 바이트를 두었고, 이 바이트들은 1MiB 측정량에 포함하지 않음
- Ryzen 9 7950X의 L2 캐시는 코어당 1MiB라서, 각 테스트 실행은 L3 캐시까지 넘어갈 것으로 예상함
- 각 함수는 인라이닝과 코드 이동의 간섭을 피하려고 별도로 컴파일함
- 실제 코드에서는 인라이닝을 막기보다 장려하는 편이 더 가능성이 높음
결과: tolower64의 매끄러운 성능
- 분홍색
tolower64는 전반적으로 테스트 함수들 중 가장 빠른 축에 꾸준히 가까움- 길이가 65바이트일 때 두 번째 벡터로 넘어가면서 약간 떨어짐
- 빠르게 상승하고 깊은 성능 골이 없어, 마스크드 load/store가 짧은 문자열 조각 처리에 효과적임을 보여줌
- 초록색
copybytes64는 AVX-512를 비슷한 방식으로 쓰는memcpy버전임tolower64보다 많이 빠르지는 않음- 최신 Clang은 이 함수의 의미를 인식해 완전히 다시 작성하므로 Clang 11로 컴파일함
- 주황색
copybytes1는 바이트 단위memcpy버전임- Clang 11로 컴파일함
- 256바이트보다 작은 문자열 조각에서 Clang 11의 자동 벡터화 휴리스틱이 상대적으로 좋지 않음을 보여줌
- 빨간색
tolower는<ctype.h>의 표준tolower()를 호출하는 기준선이며 매우 느림 - 보라색
tolower1은 Clang 16으로 컴파일한 바이트 단위tolower()임- Clang 16의 자동 벡터화는 Clang 11보다 훨씬 좋아짐
- 손으로 작성한 버전보다 느리고 훨씬 복잡한 코드를 생성함
- 짧은 문자열 조각 처리가
tolower64만큼 좋지 않아 성능 그래프가 뾰족하게 흔들림
- 갈색
tolower8은 이전 글의 SWARtolower()임- Clang이 자동 벡터화를 시도하지만 함수가 복잡해 결과가 좋지 않음
- Clang 16으로 컴파일했지만 Clang 11 스타일의 256바이트 성능 절벽이 나타남
- 파란색
memcpy는 glibc의memcpy를 호출함- 처음에는 빠르지만
copybytes64속도의 절반 정도로 떨어지는 구간이 있음 - 원인은 확인하지 못함
- 처음에는 빠르지만
결론과 코드
- AVX-512-BW는 문자열, 특히 짧은 문자열을 다루는 데 매우 적합함
- Zen 4에서는 매우 빠르고, intrinsic 함수도 비교적 사용하기 쉬움
- 가장 눈에 띄는 특징은 매끄러운 성능임
- 자동 벡터화가 작은 문자열 조각에서 스칼라 코드로 전환하며 겪는 성능 골이 거의 보이지 않음
- ARM SVE 지원 장비나 RISC-V Vector extension 장비에 편리하게 접근할 수 없어 두 확장은 자세히 조사하지 못함
- 코드는 웹 사이트의 git 저장소에서 볼 수 있음