SIMD 명령과 커널 개발

커널 공간(Kernel Space) SIMD 활용을 성능 향상과 문맥 전환(Context Switch) 비용 관리 관점에서 심층 분석합니다. x86 SSE/AVX/AVX-512/AMX와 ARM NEON/SVE 레지스터(Register) 모델, kernel_fpu_begin/end 사용 규칙, preemption/irq 컨텍스트에서의 금지 패턴, FPU 상태 저장·복원 오버헤드(Overhead), 대체 경로(fallback scalar) 설계, 컴파일 플래그와 ISA 기능 게이팅, crypto·checksum·memcpy 가속 사례, 성능 회귀를 피하기 위한 벤치마크·프로파일링(Profiling) 절차까지 실전 커널 최적화 포인트를 다룹니다.

전제 조건: 어셈블리(Assembly) 종합과 커널 아키텍처를 먼저 읽으세요. SIMD는 FPU 컨텍스트 관리와 커널/유저 전환 이해가 필요합니다.
일상 비유: SIMD는 공장 조립 라인의 병렬 공정과 비슷합니다. 혼자서 박스 4개를 하나씩 포장하는 대신, 4명이 동시에 1개씩 잡아 한 번에 4개를 처리하는 방식입니다. CPU에서 이것이 실제로 일어나는 장소가 바로 넓은 SIMD 레지스터(XMM/YMM/ZMM)입니다. SSE는 4개, AVX2는 8개, AVX-512는 16개의 32비트 정수를 단 한 명령어로 처리합니다.

핵심 요약

  • 레지스터 계층 — XMM(128-bit, SSE) ⊂ YMM(256-bit, AVX/AVX2) ⊂ ZMM(512-bit, AVX-512) 순서로 확장됩니다. 상위 레지스터는 하위 레지스터를 포함하므로, ZMM0의 하위 128비트가 바로 XMM0입니다. 더 넓은 레지스터일수록 한 번에 처리하는 데이터 개수가 2배씩 늘어납니다.
  • 커널 SIMD 제한 — XMM/YMM/ZMM은 유저 프로세스(Process)도 사용하는 실제 CPU 레지스터입니다. 커널이 이 레지스터를 덮어쓰면 유저 프로세스가 잃어버린 값으로 계산을 이어 가게 됩니다. 그래서 kernel_fpu_begin()으로 기존 값을 먼저 저장하고, 사용 후 kernel_fpu_end()로 복원해야 합니다. 이 절차를 생략하면 유저 프로세스의 부동소수점 연산 결과가 조용히 오염됩니다.
  • FPU 상태 저장 — 컨텍스트 스위치(Context Switch) 시 XSAVE/XRSTOR 명령이 FPU/SIMD 레지스터 전체를 task 구조체의 fpu 필드에 저장합니다. AVX-512까지 사용하면 저장해야 할 상태가 2.5 KB를 넘으므로, 커널은 성능을 위해 FPU 상태를 지연(lazy) 복원합니다.
  • ARM NEON/SVE — ARM64 커널에서도 동일 원칙이 적용됩니다. kernel_neon_begin()/kernel_neon_end()가 x86의 kernel_fpu_begin/end 역할을 맡으며, 내부적으로 fpsimd_save()/fpsimd_load()를 사용합니다.
  • CPUID 확인 — 특정 SIMD 확장이 없는 CPU에서 해당 명령어를 실행하면 Illegal Instruction 예외가 발생합니다. 반드시 런타임에 static_cpu_has(X86_FEATURE_AVX2) 등으로 기능 존재 여부를 확인한 뒤에 사용해야 합니다.

단계별 이해

  1. SIMD가 무엇인지 이해
    하나의 명령어로 여러 데이터를 동시 처리하는 실행 모델입니다. 4개의 정수를 더할 때 스칼라는 ADD 4번을 실행하지만, SSE2는 PADDD 1번으로 끝납니다. 아래 "SIMD 기본 개념" 섹션에서 다이어그램과 코드 예시로 확인하세요.
  2. 레지스터 구조 파악
    XMM/YMM/ZMM 계층 구조를 이해합니다. 레지스터가 넓어질수록 담을 수 있는 "요소(Element)"가 늘어납니다. 예를 들어 128비트 XMM에는 32비트 정수 4개, 256비트 YMM에는 8개, 512비트 ZMM에는 16개가 들어갑니다.
  3. 커널 사용 규칙 확인
    kernel_fpu_begin()/kernel_fpu_end() 쌍은 단순 관례가 아니라 필수 안전 장치입니다. 이 API가 선점(Preemption)을 막는다는 점도 중요합니다. 따라서 이 구간에서는 sleep, blocking I/O, mutex_lock 등을 절대 호출할 수 없으며, 구간을 가능한 한 짧게 유지해야 합니다.
  4. ARM·RISC-V 대응 확인
    x86 SIMD가 ARM에서 NEON/SVE, RISC-V에서 RVV(Vector Extension)로 어떻게 매핑(Mapping)되는지 비교합니다. 아키텍처마다 레지스터 폭·벡터 모델이 다르므로, 크로스 아키텍처 코드는 추상화 레이어가 필요합니다.
  5. 실제 커널 활용 사례
    crypto 서브시스템(AES-NI, SHA-NI), CRC32 체크섬, memcpy/memset 최적화에서 SIMD 사용 패턴을 분석합니다. 이들은 모두 짧은 kernel_fpu_begin/end 구간 안에서 실행되며, SIMD를 지원하지 않는 CPU를 위한 스칼라 폴백(Fallback)을 함께 제공합니다.

SIMD 기본 개념 — Single Instruction, Multiple Data

SIMD는 하나의 명령어(Single Instruction)로 여러 데이터(Multiple Data)를 동시에 처리하는 CPU 실행 모델입니다. 일반적인 스칼라(Scalar) 명령어는 한 번에 값 하나만 다루지만, SIMD 명령어는 CPU 내부의 넓은 레지스터에 여러 값을 나란히 채운 뒤 한꺼번에 같은 연산을 수행합니다. 결과적으로 동일한 클럭(Clock) 수에 더 많은 데이터를 처리하므로 처리량(Throughput)이 크게 향상됩니다.

Flynn 분류 — SIMD의 위치

Flynn 분류(Flynn Taxonomy)는 명령어와 데이터 스트림의 수에 따라 CPU 실행 모델을 네 가지로 구분합니다. 현대 CPU는 단일 코어 내부에서는 SIMD를, 멀티코어 전체로는 MIMD를 사용합니다.

분류명령어 스트림데이터 스트림예시커널 관련성
SISD단일(Single)단일(Single)일반 스칼라 ADD, MOV일반 커널 코드의 기본 동작 방식
SIMD단일(Single)복수(Multiple)SSE PADDD, AVX VADDPScrypto·memcpy·checksum 가속 경로
MISD복수(Multiple)단일(Single)이론적 모델 (거의 미구현)—
MIMD복수(Multiple)복수(Multiple)멀티코어 SMP커널 SMP 병렬 처리 전체

스칼라 vs SIMD — 코드와 다이어그램으로 비교

4개의 32비트 정수를 더하는 단순한 예시로 스칼라와 SIMD의 차이를 비교합니다. 스칼라는 루프를 4회 반복하지만, SSE2 PADDD 명령어는 128비트 레지스터에 정수 4개를 담아 명령어 하나로 처리합니다.

/* 스칼라: 루프를 4회 반복, ADD 명령어 4번 실행 */
int a[4] = {1, 2, 3, 4};
int b[4] = {5, 6, 7, 8};
int c[4];
for (int i = 0; i < 4; i++)
    c[i] = a[i] + b[i];   /* c = {6, 8, 10, 12} — ADD × 4번 */

/* SSE2 SIMD: PADDD 1번으로 4개를 동시 처리 */
#include <immintrin.h>
__m128i va = _mm_loadu_si128((const __m128i *)a);  /* XMM에 a[3]|a[2]|a[1]|a[0] 로드 */
__m128i vb = _mm_loadu_si128((const __m128i *)b);  /* XMM에 b[3]|b[2]|b[1]|b[0] 로드 */
__m128i vc = _mm_add_epi32(va, vb);                 /* PADDD: 4쌍을 1사이클에 덧셈 */
_mm_storeu_si128((__m128i *)c, vc);                 /* 결과를 메모리에 저장 */
/* c = {6, 8, 10, 12} — 스칼라와 동일한 결과, 명령어 1개 */

/* AVX2로 확장: YMM(256-bit)으로 int 8개를 한 번에 처리 */
__m256i va8 = _mm256_loadu_si256((const __m256i *)a);
__m256i vb8 = _mm256_loadu_si256((const __m256i *)b);
__m256i vc8 = _mm256_add_epi32(va8, vb8);  /* VPADDD ymm: 8개 동시 처리 */
① 스칼라 — a[i] + b[i]를 4회 반복 a[0]=1 a[1]=2 a[2]=3 a[3]=4 b[0]=5 b[1]=6 b[2]=7 b[3]=8 ADD ADD ADD ADD c[0]=6 c[1]=8 c[2]=10 c[3]=12 ADD ×4 순차 실행 (4회 반복) ② SIMD — 4쌍을 XMM에 담아 1번에 처리 XMM_a a[3]=4 a[2]=3 a[1]=2 a[0]=1 XMM_b b[3]=8 b[2]=7 b[1]=6 b[0]=5 PADDD — 4쌍 동시 덧셈 XMM_c c[3]=12 c[2]=10 c[1]=8 c[0]=6 PADDD ×1 4레인 병렬 실행

처리량 vs 지연 시간 — SIMD 이득의 정확한 이해

SIMD는 처리량을 높이지만, 개별 연산의 지연 시간(Latency)은 줄이지 않습니다. PADDD의 지연 시간은 스칼라 ADD와 비슷한 수준이지만(마이크로아키텍처(Microarchitecture)에 따라 다소 차이가 있음), 데이터 4개(SSE2)·8개(AVX2)·16개(AVX-512)를 동시에 처리하므로 같은 시간에 더 많은 작업을 끝냅니다. 이 특성 때문에 SIMD는 동일한 연산을 대량의 데이터에 반복 적용하는 루프에서 가장 큰 이득을 얻습니다. 반면 데이터 의존성이 깊거나 분기가 많은 코드는 SIMD 이점이 줄어들 수 있습니다.

명령어 집합(Instruction Set)레지스터int32 요소 수float 요소 수이론 처리량 향상(스칼라 대비)
SSE2XMM (128-bit)4개4개최대 4×
AVX2YMM (256-bit)8개8개최대 8×
AVX-512FZMM (512-bit)16개16개최대 16×
ARM NEONQ 레지스터 (128-bit)4개4개최대 4×
ARM SVE (최대)Z 레지스터 (가변, 최대 2048-bit)최대 64개최대 64개최대 64×

Load → Compute → Store — SIMD 기본 프로그래밍 흐름

SIMD 코드는 항상 세 단계로 구성됩니다. 첫째, 메모리에서 SIMD 레지스터로 데이터를 로드(Load)합니다. 둘째, 레지스터 안의 데이터에 벡터 연산(Compute)을 적용합니다. 셋째, 결과를 메모리에 저장(Store)합니다. 이 흐름을 지키면 컴파일러가 레지스터 할당과 명령 스케줄링을 자동으로 최적화합니다.

/* Load → Compute → Store 기본 패턴 */
#include <immintrin.h>

void add_arrays(float *dst, const float *a, const float *b, size_t n)
{
    size_t i = 0;
    for (; i + 8 <= n; i += 8) {          /* 8개씩 처리 (AVX2, 256-bit) */
        __m256 va = _mm256_loadu_ps(a + i); /* ① Load: 메모리 → YMM 레지스터 */
        __m256 vb = _mm256_loadu_ps(b + i);
        __m256 vc = _mm256_add_ps(va, vb);  /* ② Compute: 8쌍 덧셈 */
        _mm256_storeu_ps(dst + i, vc);      /* ③ Store: YMM 레지스터 → 메모리 */
    }
    for (; i < n; i++)                     /* 나머지 스칼라 처리 (tail loop) */
        dst[i] = a[i] + b[i];
}
/* VZEROUPPER: AVX→SSE 전환 시 상위 비트 초기화 (AVX 사용 함수 끝에 권장) */
_mm256_zeroupper();
Tail Loop(꼬리 루프): 배열 길이가 SIMD 폭의 배수가 아닐 때, SIMD로 처리하지 못한 나머지 요소를 스칼라 루프로 처리합니다. 실제 코드에서는 AVX-512 마스크 레지스터(k1~k7)를 사용해 tail loop 없이 처리할 수도 있지만, 스칼라 tail loop가 더 이식성(Portability)이 높고 디버깅이 쉽습니다.

x86 SIMD 확장 역사

확장레지스터비트 폭도입주요 기능
MMXMM0-MM764-bitPentium MMX (1997)정수 SIMD, FPU 레지스터 공유
SSEXMM0-XMM7128-bitPentium III (1999)단정밀도 부동소수점 4개 병렬
SSE2XMM0-XMM15 (64-bit)128-bitPentium 4 (2001)배정밀도, 정수 128-bit. x86-64 기본 지원
SSE3/SSSE3/SSE4XMM128-bit2004-2008수평 연산, 문자열 비교, CRC32
AVXYMM0-YMM15256-bitSandy Bridge (2011)256-bit 부동소수점, VEX 인코딩
AVX2YMM0-YMM15256-bitHaswell (2013)256-bit 정수, Gather, FMA3
AVX-512ZMM0-ZMM31, k0-k7512-bitXeon Phi / Skylake-X (2016)512-bit, 마스크 레지스터, scatter/gather
AMXTMM0-TMM7타일 (최대 1KB)Sapphire Rapids (2023)행렬 곱셈 가속 (INT8/BF16, FP16)
AVX10.1ZMM/YMM/XMM128~512-bitGranite Rapids (2024, Xeon) / Panther Lake (2026, 클라이언트)AVX-512 통합 후속, 버전 기반 열거, 256/512비트 프로필 분리
AVX10.2ZMM/YMM/XMM128~512-bitDiamond Rapids (2026 예정, Xeon) / Nova Lake (2027 예정, 클라이언트)AVX-512 전 서브셋 통합, P코어·E코어 양측 512비트 지원으로 하이브리드 비대칭 해소

CPU 플래그(Flags) 매핑 — /proc/cpuinfo · lscpu

lscpu 또는 /proc/cpuinfo의 flags 필드에 나타나는 플래그(Flag) 이름과 실제 SIMD 확장·기능의 대응 관계를 정리합니다. 커널은 부팅 시 CPUID를 통해 이 플래그들을 탐지하고, arch/x86/include/asm/cpufeatures.h에 X86_FEATURE_* 매크로(Macro)로 정의합니다.

# SIMD 관련 플래그만 추출
grep -oP 'flags\s*:\s*\K.*' /proc/cpuinfo | head -1 | tr ' ' '\n' | \
  grep -iE 'mmx|sse|avx|fma|f16c|amx|vnni|gfni|vaes|vpclmul|sha|aes|pclmul|xsave|bmi|popcnt|movbe|fsrm' | sort -u

# lscpu로 확인
lscpu | grep -i flags

x86 기본 SIMD 플래그

cpuinfo 플래그X86_FEATURE_*SIMD 확장설명
fpuX86_FEATURE_FPUx87 FPUx87 부동소수점 유닛(Floating Point Unit) 내장
mmxX86_FEATURE_MMXMMX64-bit 정수 SIMD (MM0-MM7, FPU 레지스터 공유)
sseX86_FEATURE_XMMSSE128-bit 단정밀도(Single Precision) 부동소수점 4개 병렬
sse2X86_FEATURE_XMM2SSE2128-bit 배정밀도(Double Precision) + 정수(Integer) 128-bit. x86-64 필수
pniX86_FEATURE_XMM3SSE3수평 덧셈(HADDPS), LDDQU, MONITOR/MWAIT. pni = Prescott New Instructions
ssse3X86_FEATURE_SSSE3SSSE3PSHUFB(바이트 셔플), PMADDUBSW, PABS* 정수 연산 강화
sse4_1X86_FEATURE_XMM4_1SSE4.1PBLENDVB, PMOVSX*, ROUNDPS, DPPS(내적), EXTRACTPS
sse4_2X86_FEATURE_XMM4_2SSE4.2PCMPESTRI/PCMPISTRM(문자열 비교), CRC32 명령어
avxX86_FEATURE_AVXAVX256-bit 부동소수점(YMM), VEX 접두사(Prefix) 인코딩 도입
avx2X86_FEATURE_AVX2AVX2256-bit 정수 연산, VGATHERD/Q(Gather), FMA 통합
fmaX86_FEATURE_FMAFMA3Fused Multiply-Add 3-operand (VFMADD132/213/231PS 등)
f16cX86_FEATURE_F16CF16C반정밀도(Half Precision) ↔ 단정밀도 변환 (VCVTPH2PS/VCVTPS2PH)

AVX-512 서브셋(Subset) 플래그

AVX-512는 단일 확장이 아니라 Foundation + 다수 서브셋으로 구성됩니다. CPU 세대에 따라 지원 서브셋 조합이 다릅니다.

cpuinfo 플래그X86_FEATURE_*서브셋설명
avx512fX86_FEATURE_AVX512FFoundation512-bit 기본 연산, 마스크 레지스터(k0-k7), ZMM0-ZMM31
avx512dqX86_FEATURE_AVX512DQDoubleword/Quadword64-bit 정수 곱셈, 부동소수점 ↔ 정수 변환 확장
avx512ifmaX86_FEATURE_AVX512IFMAInteger FMA52-bit 정수 FMA (VPMADD52LUQ/HUQ) — 암호 연산 가속
avx512cdX86_FEATURE_AVX512CDConflict DetectionVPCONFLICTD/Q — 히스토그램(Histogram), scatter 충돌 감지
avx512bwX86_FEATURE_AVX512BWByte/Word512-bit 바이트(8-bit)/워드(16-bit) 연산, k 마스크 64-bit 확장
avx512vlX86_FEATURE_AVX512VLVector LengthAVX-512 명령을 128/256-bit(XMM/YMM)에도 적용 — 마스크+VL 조합
avx512vbmiX86_FEATURE_AVX512VBMIVBMI바이트 단위 퍼뮤트(Permute) — VPERMB, VPERMI2B
avx512_vbmi2X86_FEATURE_AVX512_VBMI2VBMI2VPCOMPRESSB/W, VPEXPANDB/W — 바이트/워드 압축·확장
avx512_vnniX86_FEATURE_AVX512_VNNIVNNIVPDPBUSD/VPDPWSSD — INT8/INT16 내적 누적 (딥러닝 추론)
avx512_bitalgX86_FEATURE_AVX512_BITALGBit AlgorithmsVPOPCNTB/W, VPSHUFBITQMB — 비트 연산 확장
avx512_vpopcntdqX86_FEATURE_AVX512_VPOPCNTDQVPOPCNTDQVPOPCNTD/Q — 32/64-bit 요소별 popcount
avx512_bf16X86_FEATURE_AVX512_BF16BF16BFloat16 변환·내적 (VCVTNE2PS2BF16, VDPBF16PS)
avx512_fp16X86_FEATURE_AVX512_FP16FP16IEEE 754 반정밀도(FP16) 직접 연산 — 산술·변환·비교 전체
avx512_4vnniwX86_FEATURE_AVX512_4VNNIW4-reg VNNI Word4-레지스터 INT16 내적 (Xeon Phi 전용, 단종)
avx512_4fmapsX86_FEATURE_AVX512_4FMAPS4-reg FMA SP4-레지스터 단정밀도(Single Precision) FMA (Xeon Phi 전용, 단종)
avx512_vp2intersectX86_FEATURE_AVX512_VP2INTERSECTVP2INTERSECT두 벡터 교집합 마스크 생성 (Tiger Lake)
avx512erX86_FEATURE_AVX512ERExponential/ReciprocalVRCP28/VRSQRT28/VEXP2 — 고정밀 근사 (Xeon Phi 전용, 단종)
avx512pfX86_FEATURE_AVX512PFPrefetchVGATHERPF/VSCATTERPF — Gather/Scatter 프리페치(Prefetch) (Xeon Phi 전용, 단종)

AMX · AVX10 · VEX-VNNI 플래그

cpuinfo 플래그X86_FEATURE_*확장설명
amx_tileX86_FEATURE_AMX_TILEAMX Tile타일 레지스터(TMM0-TMM7) 구성·로드·저장 기반 명령
amx_bf16X86_FEATURE_AMX_BF16AMX BF16BFloat16 타일 행렬 곱셈 (TDPBF16PS)
amx_int8X86_FEATURE_AMX_INT8AMX INT8INT8 타일 행렬 곱셈 (TDPBSSD/TDPBSUD/TDPBUSD/TDPBUUD)
amx_fp16X86_FEATURE_AMX_FP16AMX FP16FP16 타일 행렬 곱셈 (TDPFP16PS)
avx_vnniX86_FEATURE_AVX_VNNIAVX-VNNIVEX 인코딩 VNNI — AVX-512 없이 VPDPBUSD 등 사용 (Alder Lake+)
avx_ifmaX86_FEATURE_AVX_IFMAAVX-IFMAVEX 인코딩 52-bit 정수 FMA — AVX-512 없이 사용
avx_vnni_int8X86_FEATURE_AVX_VNNI_INT8AVX-VNNI-INT8VEX 인코딩 INT8 내적 (Lunar Lake+)
avx_vnni_int16X86_FEATURE_AVX_VNNI_INT16AVX-VNNI-INT16VEX 인코딩 INT16 내적 (Arrow Lake+)
avx_ne_convertX86_FEATURE_AVX_NE_CONVERTAVX-NE-CONVERT비예외(Non-Exception) FP16/BF16 ↔ FP32 변환
avx10_versionX86_FEATURE_AVX10AVX10AVX-512 후속 통합 ISA — 벡터 길이(VL) 프로필 기반

암호화(Encryption)(Crypto) · 비트 조작(Bit Manipulation) 관련 플래그

SIMD 레지스터를 사용하거나 SIMD 코드와 함께 쓰이는 특수 명령어 플래그입니다.

cpuinfo 플래그X86_FEATURE_*기능설명
aesX86_FEATURE_AESAES-NIAES 라운드 명령 (AESENC/AESDEC) — XMM 레지스터 사용
vaesX86_FEATURE_VAESVector AESAES-NI를 256/512-bit YMM/ZMM으로 확장
pclmulqdqX86_FEATURE_PCLMULQDQPCLMULQDQ캐리 없는 곱셈(Carry-Less Multiplication) — GCM, CRC 가속
vpclmulqdqX86_FEATURE_VPCLMULQDQVector CLMULPCLMULQDQ를 256/512-bit로 확장
sha_niX86_FEATURE_SHA_NISHA ExtensionsSHA-1/SHA-256 해시(Hash) 하드웨어 가속 명령
gfniX86_FEATURE_GFNIGFNI갈루아 필드(Galois Field) 연산 — GF2P8AFFINEQB 등
popcntX86_FEATURE_POPCNTPOPCNT인구 수 세기(Population Count) — 비트 1 개수
abmX86_FEATURE_ABMABM (LZCNT)선행 0 비트 카운트(Leading Zero Count). AMD 플래그명
bmi1X86_FEATURE_BMI1BMI1ANDN, BEXTR, BLSI, BLSMSK, BLSR, TZCNT
bmi2X86_FEATURE_BMI2BMI2BZHI, MULX, PDEP, PEXT, RORX, SARX, SHRX, SHLX
movbeX86_FEATURE_MOVBEMOVBE빅 엔디안(Endianness)(Big-Endian) 로드/스토어 — 바이트 스왑(Byte Swap) + 이동

XSAVE 상태 관리 플래그

SIMD 레지스터 상태의 저장·복원 메커니즘(Mechanism)을 결정하는 플래그입니다. 커널 FPU 컨텍스트 스위치(Context Switch)의 핵심입니다.

cpuinfo 플래그X86_FEATURE_*기능설명
xsaveX86_FEATURE_XSAVEXSAVE확장 프로세서 상태(Extended Processor State) 저장·복원 기본 명령
xsaveoptX86_FEATURE_XSAVEOPTXSAVEOPT수정된 상태만 선택적으로 저장 — 성능 최적화
xsavecX86_FEATURE_XSAVECXSAVEC컴팩트(Compact) 형식으로 저장 — 사용하지 않는 구성 요소 건너뜀
xsavesX86_FEATURE_XSAVESXSAVES수퍼바이저(Supervisor) 상태 지원 — 커널 전용 상태 컴포넌트 포함

기타 SIMD 연관 플래그

cpuinfo 플래그X86_FEATURE_*기능설명
fsrmX86_FEATURE_FSRMFast Short REP MOV짧은 REP MOVSB/STOSB 고속화 — 커널 memcpy가 SIMD 대신 이를 선호
ermsX86_FEATURE_ERMSEnhanced REP MOVSB향상된 REP MOVSB — 대용량 memcpy에서 SIMD에 준하는 성능
movdiriX86_FEATURE_MOVDIRIMOVDIRI32/64-bit 직접 저장(Direct Store) — 비시간적(Non-Temporal) 쓰기
movdir64bX86_FEATURE_MOVDIR64BMOVDIR64B64-byte 직접 저장 — 캐시(Cache) 바이패스(Bypass) 대량 쓰기
serializeX86_FEATURE_SERIALIZESERIALIZE모든 이전 명령 실행·메모리 접근 완료 보장 — 직렬화(Serialization) 배리어
prefetchiX86_FEATURE_PREFETCHIPREFETCHI명령 캐시 프리페치(Prefetch)
cmpxchg16bX86_FEATURE_CX16CMPXCHG16B128-bit 비교 교환 — 잠금(Lock)-프리(Lock-Free) 알고리즘에서 SIMD 폭 데이터 원자 갱신

ARM64 HWCAP 플래그 — /proc/cpuinfo · HWCAP

ARM64에서는 /proc/cpuinfo의 Features 필드 또는 getauxval(AT_HWCAP)/getauxval(AT_HWCAP2)로 확인합니다. 커널 헤더는 arch/arm64/include/asm/hwcap.h에 정의됩니다.

# ARM64 SIMD 관련 feature 확인
grep Features /proc/cpuinfo | head -1

# 프로그래밍 방식 확인 (C 코드)
unsigned long hwcap = getauxval(AT_HWCAP);
if (hwcap & HWCAP_ASIMD)  /* NEON/Advanced SIMD 지원 */
if (hwcap & HWCAP_SVE)    /* SVE 지원 */
Features 플래그HWCAP 매크로기능설명
fpHWCAP_FPFP부동소수점(Floating Point) 기본 지원
asimdHWCAP_ASIMDAdvanced SIMD (NEON)128-bit NEON SIMD — ARM64에서 필수(AArch64 기본 포함)
fphpHWCAP_FPHPFP Half Precision반정밀도(FP16) 부동소수점 연산
asimdhpHWCAP_ASIMDHPASIMD Half PrecisionNEON 반정밀도(FP16) SIMD 연산
asimddpHWCAP_ASIMDDPASIMD Dot ProductINT8 내적 명령 (SDOT/UDOT) — 머신러닝(Machine Learning) 추론 가속
asimdfhmHWCAP_ASIMDFHMASIMD FP16 FMLFP16 Fused Multiply-Long (FMLAL/FMLSL)
sveHWCAP_SVESVEScalable Vector Extension — 128~2048-bit 가변 폭(Variable Length) 벡터
sve2HWCAP2_SVE2SVE2SVE2 — 정수·암호·비트 조작 확장
sveaesHWCAP2_SVEAESSVE AESSVE 벡터 AES 암호화 명령
svebitpermHWCAP2_SVEBITPERMSVE Bit PermuteSVE 비트 퍼뮤트(Bit Permutation) 명령
svesha3HWCAP2_SVESHA3SVE SHA3SVE SHA3 해시 명령
svesm4HWCAP2_SVESM4SVE SM4SVE SM4 블록 암호(Block Cipher) 명령
svei8mmHWCAP2_SVEI8MMSVE I8MMSVE INT8 행렬 곱셈(Matrix Multiply)
svef32mmHWCAP2_SVEF32MMSVE F32MMSVE FP32 행렬 곱셈
svef64mmHWCAP2_SVEF64MMSVE F64MMSVE FP64 행렬 곱셈
svebf16HWCAP2_SVEBF16SVE BF16SVE BFloat16 연산
smeHWCAP2_SMESMEScalable Matrix Extension — 행렬 타일(Tile) 연산 (x86 AMX 대응)
sme2HWCAP2_SME2SME2SME2 — 다중 벡터 명령, ZA 타일 확장
aesHWCAP_AESAESNEON AES 암호화 명령 (AESE/AESD/AESMC/AESIMC)
sha1HWCAP_SHA1SHA1NEON SHA-1 해시 명령
sha2HWCAP_SHA2SHA2NEON SHA-256 해시 명령
sha3HWCAP2_SHA3SHA3SHA3 해시 명령
sha512HWCAP2_SHA512SHA512SHA-512 해시 명령
crc32HWCAP_CRC32CRC32CRC32/CRC32C 하드웨어 명령
atomicsHWCAP_ATOMICSLSE AtomicsLarge System Extensions 원자 연산 — SIMD 데이터의 원자적(Atomic) 갱신 지원
bf16HWCAP2_BF16BF16NEON BFloat16 변환·내적
i8mmHWCAP2_I8MMI8MMNEON INT8 행렬 곱셈 (SMMLA/UMMLA/USMMLA)

커널에서의 플래그 확인 방법

/* x86: 커널 코드에서 SIMD 기능 확인 */
#include <asm/cpufeature.h>

/* 정적 분기(Static Branch) — 런타임 패칭으로 분기 비용 제로 */
if (static_cpu_has(X86_FEATURE_AVX2)) {
    kernel_fpu_begin();
    avx2_optimized_path(dst, src, len);
    kernel_fpu_end();
} else if (static_cpu_has(X86_FEATURE_XMM4_2)) {
    /* SSE4.2 — cpuinfo 플래그명 sse4_2 */
    kernel_fpu_begin();
    sse42_path(dst, src, len);
    kernel_fpu_end();
} else {
    generic_scalar_path(dst, src, len);
}

/* 동적 확인 — 모듈 초기화 등에서 사용 */
if (boot_cpu_has(X86_FEATURE_AVX512F) &&
    boot_cpu_has(X86_FEATURE_AVX512BW)) {
    pr_info("AVX-512F + BW detected, using 512-bit path\n");
}

/* ARM64: 커널 코드에서 확인 */
#include <asm/hwcap.h>
#include <asm/neon.h>

if (elf_hwcap & HWCAP_SVE) {
    kernel_neon_begin();
    sve_optimized_path(dst, src, len);
    kernel_neon_end();
}
플래그명 ≠ 매크로명 주의: /proc/cpuinfo에 표시되는 플래그명과 커널 매크로명이 다른 경우가 있습니다. 대표적으로 pni(cpuinfo) = X86_FEATURE_XMM3(SSE3), sse4_1(cpuinfo) = X86_FEATURE_XMM4_1 등입니다. 정확한 매핑은 arch/x86/include/asm/cpufeatures.h와 arch/x86/kernel/cpu/capflags.c에서 확인할 수 있습니다.

레인(Lane)과 요소(Element) — SIMD 데이터 구조의 핵심

SIMD 레지스터는 넓은 비트 공간을 고정 크기의 요소(Element)로 나눠 사용합니다. 각 요소가 차지하는 슬롯을 레인(Lane)이라 부릅니다. 예를 들어 128비트 XMM 레지스터를 32비트 단위로 나누면 레인 4개가 생기고, 각 레인에 정수 하나가 들어갑니다. SIMD 연산은 모든 레인에서 독립적으로 동시에 수행됩니다. 레인 간 통신(수평 연산)은 별도의 셔플(Shuffle)/퍼뮤트(Permute) 명령어가 필요합니다.

같은 레지스터도 해석 방식에 따라 요소 유형과 레인 수가 달라집니다. PADDB는 XMM을 8비트 정수 16개 레인으로, PADDW는 16비트 정수 8개 레인으로, PADDD는 32비트 정수 4개 레인으로 처리합니다. 레지스터 자체는 하나이지만, 명령어가 해석 방식(요소 타입)을 결정합니다. Intrinsic 함수 이름의 접미사(_epi8, _epi16, _epi32, _ps, _pd 등)가 바로 이 해석 방식을 나타냅니다.

요소 타입비트 폭XMM 레인 수YMM 레인 수ZMM 레인 수접미사(Suffix)
int8 (byte)8-bit16개32개64개_epi8 / _epu8
int16 (word)16-bit8개16개32개_epi16 / _epu16
int32 (dword)32-bit4개8개16개_epi32 / _epu32
int64 (qword)64-bit2개4개8개_epi64 / _epu64
float32 (single)32-bit4개8개16개_ps
float64 (double)64-bit2개4개8개_pd
Packed vs Scalar 명령어: SSE/AVX 명령어 이름에서 P(Packed)는 레지스터의 모든 레인에 동시 적용하는 SIMD 연산이고, S(Scalar)는 최하위 레인 하나에만 연산하고 나머지 레인은 유지하거나 0으로 만듭니다. 예를 들어 ADDPS는 4개 float 레인 전부 더하고, ADDSS는 레인 0만 더합니다. 커널 SIMD 코드는 대부분 Packed 명령어를 사용합니다.

x86 SIMD 레지스터 레이아웃

① 레지스터 계층 구조 (하위 비트 방향으로 중첩) 511 255 127 0 ZMM0 512비트 AVX-512 전용 [511:256] AVX 상위 [255:128] XMM / SSE [127:0] YMM0 256비트 AVX 상위 [255:128] XMM / SSE [127:0] XMM0 128비트 XMM / SSE [127:0] * 하위 비트만 공유합니다 — YMM0 = ZMM0[255:0], XMM0 = ZMM0[127:0] (같은 물리 레지스터) ② 데이터 요소 해석 — XMM0 128비트를 요소 타입별 레인으로 나눈 모습 127 63 0 16×i8 (byte) 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0 8×i16 (word) 7 6 5 4 3 2 1 0 4×i32 (dword) 3 2 1 0 2×i64 (qword) 1 0 4×f32 (single) 3 2 1 0 2×f64 (double) 1 0 ③ 마스크 레지스터 k0–k7 (AVX-512, 각 64비트) k0 마스크 지정 불가 k1 조건부 마스크 k2 조건부 마스크 k3 조건부 마스크 k4 조건부 마스크 k5 조건부 마스크 k6 조건부 마스크 k7 조건부 마스크 * k0: 마스크 자리에 지정하면 마스킹이 적용되지 않습니다(전체 레인 활성). k0 자체는 쓸 수 있습니다. * k1–k7: 조건부 마스크 — {z}를 붙이면 zeroing, 생략하면 merging(목적지의 기존 값을 유지합니다).

VEX vs Legacy 인코딩: SSE 명령어(legacy)로 XMM 레지스터를 수정하면 YMM/ZMM의 상위 비트가 보존됩니다. 반면 VEX 인코딩(AVX) 명령어는 YMM 상위 128비트를 자동으로 0으로 클리어합니다. legacy SSE와 VEX/EVEX 명령어를 혼용하면 SSE-AVX 전환 페널티가 발생하므로, 커널 SIMD 코드에서는 한 가지 인코딩만 일관되게 사용해야 합니다.

x86 SIMD 핵심 명령어 패턴

로드/스토어 명령어

/* === SSE/AVX 로드/스토어 명령어 === */

/* 정렬된 로드/스토어 (16/32/64바이트 정렬 필수) */
movaps  (%rdi), %xmm0           /* 128-bit aligned load (packed single) */
movapd  (%rdi), %xmm0           /* 128-bit aligned load (packed double) */
movdqa  (%rdi), %xmm0           /* 128-bit aligned load (integer) */
vmovaps (%rdi), %ymm0           /* 256-bit aligned load (VEX) */
vmovaps (%rdi), %zmm0           /* 512-bit aligned load (EVEX) */

/* 비정렬 로드/스토어 (정렬 불필요, 약간 느릴 수 있음) */
movups  (%rdi), %xmm0           /* 128-bit unaligned (packed single) */
movdqu  (%rdi), %xmm0           /* 128-bit unaligned (integer) */
vmovdqu (%rdi), %ymm0           /* 256-bit unaligned */
vmovdqu32 (%rdi), %zmm0         /* 512-bit unaligned (EVEX, 32-bit elem) */

/* Scalar FP 이동 (low lane만 이동, upper lane merge 규칙 주의) */
movss   (%rdi), %xmm0           /* low32 로드, xmm0[127:32] = 0 */
movss   %xmm1, %xmm0            /* low32만 복사, xmm0[127:32] 유지 */
movsd   (%rdi), %xmm0           /* low64 로드, xmm0[127:64] = 0 */
movsd   %xmm1, %xmm0            /* low64만 복사, xmm0[127:64] 유지 */
vmovss  (%rdi), %xmm0           /* low32 로드, xmm0[127:32] = 0, YMM/ZMM 상위도 0 */
vmovss  %xmm2, %xmm1, %xmm0     /* xmm0[31:0]=xmm2[31:0], xmm0[127:32]=xmm1[127:32] */
vmovsd  (%rdi), %xmm0           /* low64 로드, xmm0[127:64] = 0, YMM/ZMM 상위도 0 */
vmovsd  %xmm2, %xmm1, %xmm0     /* xmm0[63:0]=xmm2[63:0], xmm0[127:64]=xmm1[127:64] */
vmovss  %xmm0, (%rdi)           /* low32 스토어 */
vmovsd  %xmm0, (%rdi)           /* low64 스토어 */

/* Non-Temporal 스토어 (캐시 오염 방지, Write-Combining) */
movntps %xmm0, (%rdi)           /* 캐시를 거치지 않고 메모리에 직접 기록 */
movntdq %xmm0, (%rdi)           /* 정수 non-temporal store */
vmovntps %ymm0, (%rdi)          /* 256-bit non-temporal */
sfence                           /* non-temporal store 후 반드시 sfence */

/* 마스크 로드/스토어 (AVX-512) */
vmovdqu32 (%rdi), %zmm0{%k1}    /* k1 마스크 비트가 1인 요소만 로드 */
vmovdqu32 (%rdi), %zmm0{%k1}{z} /* zero-masking: 마스크 0인 요소는 0 */

산술/논리 연산

/* === SSE2 정수 연산 (128-bit) === */
paddb   %xmm1, %xmm0           /* 바이트 단위 덧셈 (16개 병렬) */
paddw   %xmm1, %xmm0           /* 워드 단위 덧셈 (8개 병렬) */
paddd   %xmm1, %xmm0           /* 더블워드 단위 덧셈 (4개 병렬) */
paddq   %xmm1, %xmm0           /* 쿼드워드 단위 덧셈 (2개 병렬) */
psubb   %xmm1, %xmm0           /* 바이트 단위 뺄셈 */

/* 논리 연산 (비트 단위, 데이터 타입 무관) */
pxor    %xmm1, %xmm0           /* 128-bit XOR (RAID, 암호에 핵심) */
pand    %xmm1, %xmm0           /* 128-bit AND */
por     %xmm1, %xmm0           /* 128-bit OR */
pandn   %xmm1, %xmm0           /* ~xmm0 AND xmm1 */

/* === AVX2 정수 연산 (256-bit, VEX 3-operand) === */
vpaddd  %ymm2, %ymm1, %ymm0    /* ymm0 = ymm1 + ymm2 (비파괴적) */
vpxor   %ymm2, %ymm1, %ymm0    /* ymm0 = ymm1 XOR ymm2 */

/* === AVX-512 연산 (512-bit, EVEX) === */
vpaddd  %zmm2, %zmm1, %zmm0            /* 16개 int32 병렬 덧셈 */
vpaddd  %zmm2, %zmm1, %zmm0{%k1}       /* 마스크 적용: k1=0인 요소 유지 */
vpaddd  %zmm2, %zmm1, %zmm0{%k1}{z}    /* 마스크 적용: k1=0인 요소 = 0 */

/* === 부동소수점 연산 === */
addps   %xmm1, %xmm0           /* 4 × float 병렬 덧셈 */
mulpd   %xmm1, %xmm0           /* 2 × double 병렬 곱셈 */
vfmadd231ps %ymm2, %ymm1, %ymm0 /* FMA: ymm0 = ymm1*ymm2 + ymm0 */

셔플/퍼뮤트/비트 조작

/* 데이터 재배열 명령어 — SIMD 프로그래밍의 핵심 */

/* PSHUFB (SSSE3): 바이트 단위 임의 셔플 (look-up table 패턴) */
/* xmm0의 각 바이트를 xmm1의 인덱스에 따라 재배열 */
pshufb  %xmm1, %xmm0           /* xmm0[i] = xmm0[xmm1[i] & 0xF] */
                                /* xmm1[i] bit7=1이면 xmm0[i]=0 */

/* PSHUFD: 32-bit 요소 셔플 (즉시값으로 순서 지정) */
pshufd  $0x39, %xmm1, %xmm0   /* xmm0 = {xmm1[0],xmm1[3],xmm1[2],xmm1[1]} */

/* PUNPCKLBW/PUNPCKHBW: 인터리브 (바이트 단위) */
punpcklbw %xmm1, %xmm0         /* 하위 8바이트를 인터리브 */

/* AVX2 퍼뮤트: 128-bit 레인 간 교차 가능 */
vperm2i128 $0x31, %ymm1, %ymm0, %ymm2  /* 128-bit 레인 교환 */
vpermd  %ymm1, %ymm0, %ymm2    /* 32-bit 단위 임의 퍼뮤트 */

/* AVX-512 퍼뮤트 */
vpermb  %zmm1, %zmm0, %zmm2    /* 바이트 단위 64-way 퍼뮤트 */
vpermt2d %zmm2, %zmm1, %zmm0   /* 2-source 퍼뮤트 (merge) */

/* 비트 시프트 (요소별) */
pslld   $4, %xmm0              /* 각 32-bit 요소를 4비트 좌측 시프트 */
psrld   $4, %xmm0              /* 각 32-bit 요소를 4비트 우측 시프트 */
vpsllvd %ymm1, %ymm0, %ymm2    /* AVX2: 요소별 가변 시프트 */

비교/블렌드/변환

/* === 비교 명령어 === */
pcmpeqb %xmm1, %xmm0           /* 바이트 비교: 같으면 0xFF, 다르면 0x00 */
pcmpgtd %xmm1, %xmm0           /* 부호있는 32-bit 비교: xmm0>xmm1이면 0xFFFFFFFF */

/* SSE4.2 문자열 비교 (커널에서 문자열 처리에 활용) */
pcmpistri $0x08, %xmm1, %xmm0  /* Equal Each: 바이트 일치 인덱스 → ECX */
pcmpestri $0x00, %xmm1, %xmm0  /* Equal Any: 문자 집합 검색 */

/* AVX-512 비교 → 마스크 레지스터 */
vpcmpeqd %zmm1, %zmm0, %k1     /* 16개 int32 비교 → k1 마스크 (16-bit) */
vpcmpud $1, %zmm1, %zmm0, %k2  /* 부호없는 less-than 비교 */
kmovw   %k1, %eax               /* 마스크 → 범용 레지스터 전송 */
popcnt  %eax, %eax              /* 일치 개수 세기 */

/* === 블렌드: 조건부 선택 === */
blendvps %xmm0, %xmm1, %xmm2   /* SSE4.1: xmm0 MSB로 xmm1/xmm2 선택 */
vpblendvb %ymm3, %ymm2, %ymm1, %ymm0 /* AVX2: 바이트별 조건부 선택 */
vpblendmd %zmm2, %zmm1, %zmm0{%k1}   /* AVX-512: 마스크 기반 블렌드 */

/* === 데이터 타입 변환 === */
cvtdq2ps  %xmm0, %xmm1         /* 4 × int32 → 4 × float */
cvtps2dq  %xmm0, %xmm1         /* 4 × float → 4 × int32 (반올림) */
cvttps2dq %xmm0, %xmm1         /* 4 × float → 4 × int32 (truncate) */
vpmovzxbw %xmm0, %ymm1         /* 16 × uint8 → 16 × uint16 (zero-extend) */
vpmovsxwd %xmm0, %ymm1         /* 8 × int16 → 8 × int32 (sign-extend) */

암호화 전용 SIMD 명령어

/* === AES-NI 명령어 (커널 crypto 핵심) === */
aesenc    %xmm1, %xmm0         /* AES 한 라운드 암호화 */
aesenclast %xmm1, %xmm0        /* AES 마지막 라운드 암호화 */
aesdec    %xmm1, %xmm0         /* AES 한 라운드 복호화(Decryption) */
aesdeclast %xmm1, %xmm0        /* AES 마지막 라운드 복호화 */
aeskeygenassist $1, %xmm0, %xmm1 /* AES 라운드 키 생성 보조 */
aesimc    %xmm0, %xmm1         /* InvMixColumns (복호화 키 변환) */

/* VAES: 256/512-bit AES (AVX-512 + VAES) */
vaesenc   %ymm2, %ymm1, %ymm0  /* 2블록 병렬 AES 암호화 */
vaesenc   %zmm2, %zmm1, %zmm0  /* 4블록 병렬 AES 암호화 */

/* === PCLMULQDQ: 갈루아 필드 곱셈 (GCM, CRC) === */
pclmulqdq $0x00, %xmm1, %xmm0  /* Carry-less multiply: xmm0[63:0] × xmm1[63:0] */
pclmulqdq $0x11, %xmm1, %xmm0  /* xmm0[127:64] × xmm1[127:64] */
vpclmulqdq $0x00, %zmm1, %zmm0, %zmm2 /* 512-bit VPCLMULQDQ (4 병렬) */

/* === SHA-NI 명령어 === */
sha256rnds2 %xmm0, %xmm1       /* SHA-256 2라운드 처리 */
sha256msg1  %xmm1, %xmm0       /* SHA-256 메시지 스케줄 1 */
sha256msg2  %xmm1, %xmm0       /* SHA-256 메시지 스케줄 2 */
sha1rnds4   $0, %xmm1, %xmm0   /* SHA-1 4라운드 처리 */
sha1nexte   %xmm1, %xmm0       /* SHA-1 다음 E값 계산 */

/* === CRC32 (SSE4.2) === */
crc32b  (%rdi), %eax            /* CRC32C: 1바이트 누적 */
crc32q  (%rdi), %rax            /* CRC32C: 8바이트 누적 */

Gather/Scatter (AVX2/AVX-512)

/* Gather: 불연속 메모리 주소에서 벡터로 모아 읽기 */
/* 기존 스칼라 코드:
 *   for (i = 0; i < 8; i++) result[i] = base[index[i]];
 * → 단일 Gather 명령어로 대체 */

/* AVX2 Gather */
vgatherdps %ymm2, (%rdi,%ymm1,4), %ymm0
/* ymm0[i] = MEM[rdi + ymm1[i]*4] (ymm2 마스크 기반)
 * ymm2는 마스크 겸 오류 추적, 실행 후 0으로 클리어됨 */

vgatherdpd %xmm2, (%rdi,%xmm1,8), %ymm0
/* 4개 인덱스(xmm1, 32-bit) → 4개 double 로드 → ymm0 */

/* AVX-512 Gather (마스크 레지스터 사용, 더 효율적) */
vgatherdps (%rdi,%zmm1,4), %zmm0{%k1}
/* k1 마스크가 1인 요소만 로드, 완료된 요소의 k1 비트 클리어 */

/* AVX-512 Scatter: 벡터에서 불연속 메모리 주소로 흩뿌려 쓰기 */
vscatterdps %zmm0, (%rdi,%zmm1,4){%k1}
/* MEM[rdi + zmm1[i]*4] = zmm0[i] (k1 마스크 적용)
 * Gather의 역연산. AVX-512 이전에는 없었음 */

Gather 성능 주의: Gather 명령어는 편리하지만, 내부적으로 여러 개의 스칼라 로드를 수행합니다. 캐시 라인(Cache Line)이 연속적이지 않으면 성능 이점이 제한됩니다. Intel Skylake-X 이후부터 성능이 크게 개선되었으며, 커널에서는 주로 테이블 룩업이 빈번한 암호 알고리즘에서 활용됩니다.

ARM SIMD 확장

확장레지스터비트 폭도입주요 기능
NEON (Advanced SIMD)V0-V31 (AArch64)128-bitARMv7 / ARMv8정수+부동소수점 SIMD, 암호 확장(CE)
SVE (Scalable Vector)Z0-Z31, P0-P15128~2048-bitARMv8.2-A가변 벡터 길이, 프레디케이트 레지스터
SVE2Z0-Z31128~2048-bitARMv9-ANEON 명령어 세트 포괄, 암호 확장
SMEZA 타일SVE VL × SVE VLARMv9.2-A행렬 연산, Streaming SVE 모드

ARM NEON 핵심 명령어

/* ARM NEON (Advanced SIMD) 레지스터 구조
 *
 * V0-V31: 128-bit 벡터 레지스터
 * 접근 방식:
 *   Vn.16B  = 16 × byte    Vn.8B  = 하위 8 × byte (64-bit)
 *   Vn.8H   = 8 × halfword Vn.4H  = 하위 4 × halfword
 *   Vn.4S   = 4 × single   Vn.2S  = 하위 2 × single
 *   Vn.2D   = 2 × double   Vn.1D  = 하위 1 × double
 *   Bn/Hn/Sn/Dn = 스칼라 접근 (byte/half/single/double)
 */

/* === 로드/스토어 === */
ld1     {v0.16b}, [x0]          /* 128-bit 연속 로드 */
ld1     {v0.16b, v1.16b}, [x0]  /* 256-bit 연속 로드 (2 레지스터) */
ld1     {v0.4s-v3.4s}, [x0]     /* 512-bit 연속 로드 (4 레지스터) */
st1     {v0.16b}, [x0]          /* 128-bit 스토어 */

/* Structure 로드: 인터리브 해제 (SoA 변환) */
ld2     {v0.4s, v1.4s}, [x0]    /* 2-way 디인터리브: ABABAB → A,B */
ld3     {v0.4s, v1.4s, v2.4s}, [x0] /* RGB → R,G,B 분리 */
ld4     {v0.4s-v3.4s}, [x0]     /* 4-way 디인터리브 */
st2     {v0.4s, v1.4s}, [x0]    /* 2-way 인터리브 저장 */

/* === 산술 연산 === */
add     v0.4s, v1.4s, v2.4s     /* 4 × int32 덧셈 */
sub     v0.8h, v1.8h, v2.8h     /* 8 × int16 뺄셈 */
mul     v0.4s, v1.4s, v2.4s     /* 4 × int32 곱셈 */
mla     v0.4s, v1.4s, v2.4s     /* v0 += v1 * v2 (multiply-accumulate) */
fmul    v0.4s, v1.4s, v2.4s     /* 4 × float 곱셈 */
fmla    v0.4s, v1.4s, v2.4s     /* fused multiply-add (FP) */

/* === 논리/비트 연산 === */
eor     v0.16b, v1.16b, v2.16b  /* 128-bit XOR */
and     v0.16b, v1.16b, v2.16b  /* 128-bit AND */
bsl     v0.16b, v1.16b, v2.16b  /* Bitwise Select: bit=1 → v1, bit=0 → v2 */

/* === 비교 === */
cmeq    v0.4s, v1.4s, v2.4s     /* 같으면 0xFFFFFFFF, 다르면 0 */
cmgt    v0.4s, v1.4s, v2.4s     /* 부호있는 greater-than */
cmhi    v0.4s, v1.4s, v2.4s     /* 부호없는 greater-than (higher) */

/* === 셔플/퍼뮤트 === */
tbl     v0.16b, {v1.16b}, v2.16b /* 테이블 룩업 (x86 PSHUFB와 유사) */
tbl     v0.16b, {v1.16b, v2.16b}, v3.16b /* 32바이트 테이블 룩업 */
trn1    v0.4s, v1.4s, v2.4s     /* Transpose (짝수 요소 인터리브) */
trn2    v0.4s, v1.4s, v2.4s     /* Transpose (홀수 요소 인터리브) */
zip1    v0.4s, v1.4s, v2.4s     /* 하위 절반 인터리브 */
uzp1    v0.4s, v1.4s, v2.4s     /* 짝수 요소 추출 */
rev64   v0.16b, v1.16b          /* 64-bit 단위 내 바이트 역순 */
ext     v0.16b, v1.16b, v2.16b, #4 /* 연결 후 4바이트 시프트 추출 */

/* === 암호 확장 (ARMv8 Crypto Extension) === */
aese    v0.16b, v1.16b          /* AES 단일 라운드 암호화 */
aesd    v0.16b, v1.16b          /* AES 단일 라운드 복호화 */
aesmc   v0.16b, v1.16b          /* AES MixColumns */
aesimc  v0.16b, v1.16b          /* AES InvMixColumns */
pmull   v0.1q, v1.1d, v2.1d     /* 다항식 곱셈 (GHASH) */
sha256h  q0, q1, v2.4s          /* SHA-256 해시(Hash) 업데이트 */
sha256su0 v0.4s, v1.4s          /* SHA-256 스케줄 업데이트 */

ARM SVE 프로그래밍 모델

SVE(Scalable Vector Extension)는 벡터 길이를 하드웨어 구현에 의존하는 벡터 길이 비종속 (Vector Length Agnostic, VLA) 프로그래밍 모델입니다. 동일한 바이너리가 128비트~2048비트 구현에서 동작합니다.

/* SVE 레지스터 구조
 *
 * Z0-Z31: 스케일러블 벡터 (128~2048-bit, 하드웨어 정의)
 *   Zn의 하위 128비트 = NEON Vn과 공유 (아키텍처 보장)
 * P0-P15: 프레디케이트 레지스터 (VL/8 비트)
 *   각 비트가 벡터의 한 바이트에 대응
 *   P0-P7: governing predicate로 사용 가능
 * FFR: First Fault Register (투기적 로드용)
 * VL: Vector Length (cntb로 바이트 단위 조회)
 */

/* === 벡터 길이 조회 === */
cntb    x0                      /* x0 = VL (바이트 단위). 예: 512-bit → 64 */
cnth    x0                      /* x0 = VL / 2 (halfword 개수) */
cntw    x0                      /* x0 = VL / 4 (word 개수) */
cntd    x0                      /* x0 = VL / 8 (doubleword 개수) */

/* === 프레디케이트 생성 === */
ptrue   p0.b                    /* 모든 바이트 활성 (all-true) */
ptrue   p0.s                    /* 모든 word 활성 */
whilelt p0.s, x0, x1            /* x0 < x1인 요소만 활성 (루프 테일 처리) */
pfalse  p0.b                    /* 모든 비트 비활성 (all-false) */

/* === VLA 루프 패턴 (커널에서 가장 중요) === */
/* memcpy를 SVE로 구현하는 예시 */
/*   x0 = dst, x1 = src, x2 = len (바이트) */
    mov     x3, #0              /* 오프셋 초기화 */
    whilelt p0.b, x3, x2        /* 프레디케이트 설정 */
.loop:
    ld1b    z0.b, p0/z, [x1, x3] /* 프레디케이트 기반 로드 */
    st1b    z0.b, p0, [x0, x3]   /* 프레디케이트 기반 스토어 */
    incb    x3                    /* x3 += VL (바이트 수만큼 증가) */
    whilelt p0.b, x3, x2         /* 잔여 요소 프레디케이트 갱신 */
    b.first .loop                 /* 활성 요소 있으면 계속 */

/* === Scatter/Gather (SVE 기본 지원) === */
ld1w    z0.s, p0/z, [x0, z1.s, uxtw #2] /* Gather: x0 + z1[i]*4 */
st1w    z0.s, p0, [x0, z1.s, uxtw #2]   /* Scatter */

/* === First Fault 로드 (투기적 로드, 커널 문자열 처리) === */
ldff1b  z0.b, p0/z, [x0, x1]    /* First Fault 로드 */
rdffr   p1.b                     /* FFR 읽기: 성공한 요소만 p1=1 */

SVE의 벡터 길이 비종속 설계: SVE 코드는 벡터 길이를 상수로 가정하지 않습니다. cntb로 런타임에 VL을 조회하고, whilelt 프레디케이트로 루프 테일을 자동 처리합니다. 이 덕분에 Fujitsu A64FX(512-bit)와 AWS Graviton3(256-bit) 등 다른 VL 구현에서 동일 바이너리가 동작합니다. 커널에서 SVE를 사용하는 코드는 arch/arm64/lib/의 문자열/메모리 함수에서 확인할 수 있습니다.

ARM SME — 행렬 연산 엔진

SME(Scalable Matrix Extension)는 ARMv9.2-A에 도입된 행렬 연산 전용 확장으로, 2D 타일 구조의 ZA 레지스터와 Streaming SVE 모드(SSVE)를 제공합니다. SVE2가 1차원 벡터를 다루는 데 반해, SME는 2차원 행렬 외적(Outer Product)을 하드웨어에서 직접 수행합니다.

구분SVE2SME / Streaming SVE
레지스터Z0-Z31 (벡터)ZA 타일 (2D 행렬, SVL×SVL 비트)
데이터 형태1차원 벡터2차원 행렬 (타일 단위 조작)
모드 전환항상 활성smstart/smstop으로 Streaming 모드 진입/탈출
벡터 길이VL (SVE Vector Length)SVL (Streaming Vector Length, 별도 독립)
주요 연산벡터 산술/비교/셔플외적(FMOPA), 행렬-벡터 누적
활용 사례memcpy, 암호, DSP행렬 곱셈, 신경망 추론(INT8/BF16/FP16)
/* Streaming SVE 모드 제어 */
smstart                          /* Streaming SVE 모드 + ZA 동시 활성화 */
smstart sm                       /* Streaming SVE 모드만 활성화 (ZA 제외) */
smstart za                       /* ZA 레지스터만 활성화 (SM 모드 제외) */
smstop                           /* Streaming SVE 모드 + ZA 비활성화 */

/* SVL(Streaming Vector Length) 조회 — VL과 독립 */
rdsvl   x0, #1                  /* x0 = SVL (바이트 단위) */

/* ZA 타일 로드/스토어 (가로/세로 슬라이스 단위) */
ld1w    za0h.s[w12, #0], p0/z, [x0, x1, lsl #2]
/* za0의 가로(h) 슬라이스를 로드. w12: 슬라이스 인덱스 */
st1w    za0v.s[w12, #0], p0, [x0, x1, lsl #2]
/* za0의 세로(v) 슬라이스를 스토어 */

/* 외적 누적 (Outer Product Accumulate) — SME 핵심 연산 */
fmopa   za0.s, p0/m, p1/m, z0.s, z1.s
/* za0 += z0 × z1^T  (부동소수점 외적, 마스크 p0/p1 적용) */
smopa   za0.s, p0/m, p1/m, z0.b, z1.b
/* za0 += z0 × z1^T  (INT8→INT32 누적, SME2 행렬 곱) */
bfmopa  za0.s, p0/m, p1/m, z0.h, z1.h
/* za0 += z0 × z1^T  (BF16→FP32 누적) */
/* 커널 SME 감지 — arch/arm64/include/asm/cpufeature.h */
#include <asm/cpufeature.h>

if (system_supports_sme()) {
    /* SME 사용 가능: ZA 타일, FMOPA, SMOPA 등 */
    unsigned int svl = task_get_svl(current);
    /* SVL 예: 512-bit → ZA 크기 = 64×64/8 = 512바이트 */
}

if (system_supports_sme2()) {
    /* SME2: 다중 벡터 ZA 연산, INT8/FP8 지원 */
}

/* ZA 상태 저장 비용 — arch/arm64/include/asm/fpsimd.h
 *
 * 스케줄러(Scheduler)가 컨텍스트 스위치 시 자동 처리:
 *   fpsimd_save_state() → thread.uw.fpsimd_state에 NEON 저장
 *   za_state는 thread.za_state 별도 영역에 저장
 *
 * 크기 예시 (SVL 기준):
 *   SVL =  512-bit → ZA =  64× 64/8 =   512 bytes
 *   SVL = 1024-bit → ZA = 128×128/8 = 2,048 bytes
 *   SVL = 2048-bit → ZA = 256×256/8 = 8,192 bytes
 * → SME 활성 프로세스는 context switch 비용이 현저히 증가
 */

/* 커널에서 SME를 직접 사용하지 않는 이유:
 * 1. ZA 상태 저장 비용이 크고, 커널 경로 지연 증가
 * 2. Streaming SVE 모드에서 일부 SVE 명령어 동작 변경
 * 3. smstart/smstop이 모드 전환 비용(마이크로초 단위) 수반
 * → SME는 유저 공간 라이브러리(BLAS, NN 추론)에서 사용
 * → 커널 역할: SMAN 상태 저장/복원, cpufeature 노출만 담당
 */

Intrinsic 함수 실전 가이드

Intrinsic 함수는 C/C++ 코드에서 특정 CPU 명령어를 직접 호출할 수 있는 컴파일러 내장 함수(Compiler Built-in Function)입니다. 인라인 어셈블리와 달리 C 함수 호출 문법을 사용하면서도, 컴파일러가 1:1에 가깝게 기계 명령어로 변환합니다. 레지스터 할당, 명령 스케줄링(Instruction Scheduling), 루프 언롤링(Loop Unrolling) 등의 최적화를 컴파일러에 위임할 수 있어 인라인 어셈블리보다 유지보수가 쉽습니다.

Intrinsic 컴파일 파이프라인(Pipeline)

Intrinsic 함수가 최종 기계 코드로 변환되는 과정을 이해하면, 최적화 결과를 예측하고 디버깅(Debugging)하는 데 도움이 됩니다.

① C 소스 코드 __m256i sum = _mm256_add_epi32(a, b); Intrinsic 호출 문법 타입 검사 적용 ② 컴파일러 IR vector(8) unsigned int _3 = _1 + _2; GIMPLE / LLVM IR 벡터 타입으로 표현 ③ 어셈블리 vpaddd %ymm1, %ymm0, %ymm0 레지스터 할당 완료 명령 스케줄링 적용 ④ 기계어 C5 FD FE C1 VEX 접두사 + opcode int32 레인 8개 처리 gcc -mavx2 ISel / RA as 컴파일러에 위임되는 최적화 레지스터 할당 · 명령 스케줄링 · 루프 언롤링 인라인 어셈블리와 달리 개발자는 벡터 연산의 의미만 기술하고, 레지스터·명령 순서는 컴파일러가 결정합니다.

Intrinsic 데이터 타입 체계

Intrinsic 함수는 고유한 벡터 데이터 타입을 사용합니다. 이 타입은 실제 CPU 레지스터에 1:1 대응하며, 컴파일러가 타입 불일치(Type Mismatch)를 검사할 수 있게 합니다.

데이터 타입레지스터비트 폭내용물선언 예
__m128XMM1284 × float (단정밀도)__m128 v = _mm_set_ps(3,2,1,0);
__m128dXMM1282 × double (배정밀도)__m128d v = _mm_set_pd(1.0, 0.0);
__m128iXMM128정수 (8/16/32/64-bit)__m128i v = _mm_set_epi32(3,2,1,0);
__m256YMM2568 × float__m256 v = _mm256_setzero_ps();
__m256dYMM2564 × double__m256d v = _mm256_set1_pd(1.0);
__m256iYMM256정수 (8/16/32/64-bit)__m256i v = _mm256_set1_epi32(42);
__m512ZMM51216 × float__m512 v = _mm512_setzero_ps();
__m512iZMM512정수 (8/16/32/64-bit)__m512i v = _mm512_set1_epi64(0);
__mmask8k0-k788-bit 마스크__mmask8 k = 0xFF;
__mmask16k0-k71616-bit 마스크__mmask16 k = _cvtu32_mask16(m);
__mmask32k0-k73232-bit 마스크 (BW)__mmask32 k = 0xFFFFFFFF;
__mmask64k0-k76464-bit 마스크 (BW)__mmask64 k = _cvtu64_mask64(m);
타입 접미사의미요소 크기128-bit 레인당 개수예시
psPacked Single32-bit float4_mm256_add_ps → VADDPS
pdPacked Double64-bit double2_mm256_mul_pd → VMULPD
ssScalar Single32-bit float (1개)—_mm_add_ss → VADDSS
sdScalar Double64-bit double (1개)—_mm_mul_sd → VMULSD
epi8Signed int88-bit16_mm_add_epi8 → VPADDB
epi16Signed int1616-bit8_mm256_mullo_epi16 → VPMULLW
epi32Signed int3232-bit4_mm256_add_epi32 → VPADDD
epi64Signed int6464-bit2_mm256_add_epi64 → VPADDQ
epu8Unsigned int88-bit16_mm_max_epu8 → VPMAXUB
epu16Unsigned int1616-bit8_mm256_avg_epu16 → VPAVGW
epu32Unsigned int3232-bit4_mm_min_epu32 → VPMINUD
si128 / si256Whole register전체 레지스터—_mm256_xor_si256 → VPXOR

__m128i의 요소 크기 결정: __m128i 자체는 "128-bit 정수 벡터"일 뿐, 내부 요소 크기를 타입에 담고 있지 않습니다. 실제 요소 크기는 사용하는 Intrinsic 함수의 접미사가 결정합니다. 같은 __m128i 변수를 _mm_add_epi8로 처리하면 16개의 8-bit 덧셈이, _mm_add_epi32로 처리하면 4개의 32-bit 덧셈이 수행됩니다. 이 점이 타입 체계의 유연성이자, 타입만으로는 의도를 알 수 없는 한계입니다.

헤더 체계와 네이밍 규칙

x86 Intrinsic 헤더 계층과 레지스터 폭 대응 <immintrin.h> ISA별 헤더를 모두 포함 <xmmintrin.h> · <emmintrin.h> SSE / SSE2 · _mm_* (x86-64 기본) <avxintrin.h> · <avx2intrin.h> AVX / AVX2 · _mm256_* <avx512fintrin.h> AVX-512F · _mm512_* XMM · 128-bit __m128 · __m128i · __m128d YMM · 256-bit __m256 · __m256i · __m256d ZMM · 512-bit __m512 · __m512i · __mmask16 네이밍 규칙: _mm[폭]_[연산]_[타입] 폭 접두사: 없음 = 128-bit, 256 = 256-bit, 512 = 512-bit 타입 접미사: ps = float32, pd = float64, epi32 = 부호 있는 int32, epu8 = 부호 없는 int8, si128 = 128-bit 정수
헤더제공 Intrinsic 접두사레지스터ISA
<xmmintrin.h>_mm_* (float)XMM0-XMM7SSE
<emmintrin.h>_mm_* (int/double)XMM0-XMM15SSE2
<tmmintrin.h>_mm_shuffle_*XMMSSSE3
<smmintrin.h>_mm_blend_*XMMSSE4.1
<wmmintrin.h>_mm_aes*XMMAES-NI
<immintrin.h>_mm256_*, _mm512_*YMM / ZMMAVX+

네이밍 규칙 예시:

초기화 패턴

벡터 레지스터의 초기값 설정은 SIMD 코드의 시작점입니다. 초기화 Intrinsic 종류에 따라 생성되는 명령어와 성능이 크게 달라집니다.

패턴Intrinsic생성 명령어설명
제로 초기화_mm256_setzero_si256()vpxor ymm, ymm, ymmXOR 자기 자신. 의존성(Dependency) 끊기 관용구(Idiom)
브로드캐스트_mm256_set1_epi32(42)vpbroadcastd ymm, mem/r32하나의 값을 모든 요소에 복제. 상수 마스크 생성에 활용
개별 지정_mm_set_epi32(d,c,b,a)다양 (상수면 메모리 로드)요소를 개별 지정. 역순 주의: 인자 순서가 [3],[2],[1],[0]
역순 지정_mm_setr_epi32(a,b,c,d)다양메모리 순서대로 지정 (직관적). set의 역순
정렬 로드_mm256_load_si256(p)vmovdqa ymm, [mem]32-byte 정렬 필수. 미정렬 시 #GP 예외
비정렬 로드_mm256_loadu_si256(p)vmovdqu ymm, [mem]정렬 제약 없음. Haswell 이후 정렬 로드와 성능 차이 거의 없음
NT 스토어_mm256_stream_si256(p,v)vmovntdq [mem], ymm캐시 바이패스(Bypass) 쓰기. 대량 순차 기록에 유리
미정의_mm256_undefined_si256()(없음 — 컴파일러 힌트)초기값 불필요 표시. 불필요한 제로화 제거에 활용
_mm_set_epi32 인자 순서 주의: _mm_set_epi32(3, 2, 1, 0)은 메모리상 [0, 1, 2, 3]으로 저장됩니다. 첫 인자가 최상위(MSB) 요소입니다. 직관적으로 메모리 순서대로 쓰려면 _mm_setr_epi32(0, 1, 2, 3)을 사용하세요.

SSE/SSE2 기본 Intrinsic

SSE2는 x86-64에서 필수 지원되므로, 별도 CPU 기능 검사 없이 사용할 수 있는 가장 기본적인 SIMD 계층입니다.

#include <emmintrin.h>  /* SSE2 — x86-64 기본 포함 */
#include <smmintrin.h>  /* SSE4.1 — blend, extract 등 */
#include <nmmintrin.h>  /* SSE4.2 — CRC32, 문자열 비교 */

/* ── SSE2 기본 연산 ── */

/* 128-bit 정수 로드/스토어 */
__m128i a = _mm_loadu_si128((const __m128i *)src);
_mm_storeu_si128((__m128i *)dst, a);

/* 4 × int32 산술 */
__m128i sum  = _mm_add_epi32(a, b);    /* paddd  → 4개 int32 덧셈 */
__m128i diff = _mm_sub_epi32(a, b);    /* psubd  → 4개 int32 뺄셈 */

/* 16 × uint8 포화(Saturating) 연산 — 오버플로 시 255로 클램프 */
__m128i sat_sum = _mm_adds_epu8(a, b); /* paddusb → 이미지 밝기 조절 등에 활용 */
__m128i sat_sub = _mm_subs_epu8(a, b); /* psubusb → 결과가 음수면 0으로 클램프 */

/* 128-bit 논리 연산 */
__m128i x = _mm_xor_si128(a, b);     /* pxor   → 128-bit XOR */
__m128i y = _mm_and_si128(a, b);     /* pand   → 128-bit AND */
__m128i z = _mm_andnot_si128(a, b); /* pandn  → (~a) AND b */

/* 시프트 — 즉시값(Immediate) */
__m128i sl = _mm_slli_epi32(a, 4);   /* pslld  → 각 int32를 4비트 좌측 시프트 */
__m128i sr = _mm_srli_epi32(a, 4);   /* psrld  → 각 int32를 4비트 논리 우측 시프트 */
__m128i sa = _mm_srai_epi32(a, 4);   /* psrad  → 각 int32를 4비트 산술 우측 시프트 */

/* 비교 → 마스크 */
__m128i eq = _mm_cmpeq_epi8(a, b);   /* pcmpeqb → 같으면 0xFF, 다르면 0x00 */
int mask = _mm_movemask_epi8(eq);    /* pmovmskb → 각 바이트 MSB → 16-bit 마스크 */
/* mask의 각 비트가 해당 바이트의 비교 결과
 * __builtin_ctz(mask)로 첫 매치 위치 계산 가능 */

/* ── SSE2 셔플 ── */
__m128i shuf = _mm_shuffle_epi32(a, _MM_SHUFFLE(3,1,2,0));
/* pshufd → [a3, a1, a2, a0] — 즉시값으로 32-bit 요소 재배치
 * _MM_SHUFFLE(z,y,x,w) = (z<<6)|(y<<4)|(x<<2)|w */

/* 언팩(Unpack) — 인터리브 */
__m128i lo = _mm_unpacklo_epi8(a, b);
/* punpcklbw → a[0],b[0],a[1],b[1],...,a[7],b[7] 하위 8바이트 인터리브 */

/* ── SSE4.1 추가 ── */
__m128i blend = _mm_blendv_epi8(a, b, mask128);
/* pblendvb → mask의 MSB에 따라 a 또는 b 선택. 조건부 치환에 필수 */

__m128i min32 = _mm_min_epi32(a, b);   /* pminsd  → signed int32 최솟값 */
__m128i max32 = _mm_max_epi32(a, b);   /* pmaxsd  → signed int32 최댓값 */
__m128i mulo  = _mm_mullo_epi32(a, b); /* pmulld  → int32 곱셈 하위 32비트 (SSE2에는 없었음) */
int elem2 = _mm_extract_epi32(a, 2);  /* pextrd  → 벡터에서 32-bit 요소 추출 */

/* ── SSE4.2 문자열 처리 ── */
int idx = _mm_cmpestri(needle, nlen, haystack, hlen, _SIDD_CMP_EQUAL_ORDERED);
/* pcmpestri → 명시적 길이(Explicit Length) 문자열 비교, 매치 인덱스 반환
 * _SIDD_CMP_EQUAL_ORDERED: 부분 문자열(Substring) 검색 모드 */

uint32_t crc = _mm_crc32_u64(init, data);
/* crc32q → CRC32C (Castagnoli) 하드웨어 가속. iSCSI, Btrfs, ext4 체크섬 */

핵심 연산 데이터 흐름 시각화

SIMD Intrinsic의 동작을 이해하는 가장 빠른 방법은 요소별(Element-wise) 데이터 흐름을 시각화하는 것입니다.

① _mm256_add_epi32(a, b) → VPADDD 8개의 32-bit 정수를 독립적으로 덧셈. 요소 간 캐리(Carry) 전파 없음. a (YMM) 10 20 30 40 50 60 70 80 + + + + + + + + b (YMM) 1 2 3 4 5 6 7 8 결과 11 22 33 44 55 66 77 88 1 cycle ② _mm_shuffle_epi32(a, _MM_SHUFFLE(0,1,2,3)) → PSHUFD 즉시값(Immediate) 제어로 128-bit 내 4개 int32 요소를 임의 재배치. 역순 정렬 예시. a (XMM) [3] = D [2] = C [1] = B [0] = A 결과 [3] = A [2] = B [1] = C [0] = D imm8 = 00_01_10_11 = 0x1B ③ _mm_movemask_epi8(cmp_result) → PMOVMSKB 16바이트 비교 결과에서 각 바이트 MSB를 추출하여 16-bit 정수 마스크 생성. 문자열 탐색의 핵심. 비교 결과 FF 00 00 FF FF 00 00 00 00 00 FF 00 00 00 FF 00 ↓1 ↓0 ↓0 ↓1 ↓1 ↓0 ↓0 ↓0 ↓0 ↓0 ↓1 ↓0 ↓0 ↓0 ↓1 ↓0 int mask = 0b 1001_1000_0010_0010 = 0x9822 __builtin_ctz(0x9822) = 1 → 인덱스 1에서 첫 매치. __builtin_popcount(0x9822) = 5 → 총 5개 매치. ④ AVX-512 마스크: mask(merge) vs maskz(zero) 마스크 비트가 0인 레인(Lane)의 처리 방식이 다릅니다. merge는 원본 유지, zero는 0으로 클리어. k1 마스크 1 0 1 0 a+b 11 22 33 44 _mm512_mask_add (merge) src = S0 S1 S2 S3 결과: 11 S1 33 S3 _mm512_maskz_add (zero) 결과: 11 0 33 0 ⑤ _mm256_shuffle_epi8(data, ctrl) → VPSHUFB 128-bit 레인 독립 바이트 셔플. ctrl[i]의 하위 4비트가 소스 인덱스, 비트7=1이면 0. LUT(Look-Up Table) 구현에 핵심. data 레인0 [0]=A [1]=B [2]=C [3]=D ... ctrl 레인0 0x03 0x80 0x00 0x01 ... 결과 레인0 D 0 A B ... ctrl=0x03→data[3]=D, ctrl=0x80→0(비트7), ctrl=0x00→data[0]=A, ctrl=0x01→data[1]=B 주의: 레인 간 크로스 불가 — 레인0의 ctrl은 레인0의 data에서만 선택

AVX2 핵심 Intrinsic 예제

#include <immintrin.h>
#include <stdint.h>

/* 로드/스토어 */
__m256i a = _mm256_load_si256((const __m256i *)p);  /* 32B 정렬 필수 */
__m256i b = _mm256_loadu_si256((const __m256i *)p); /* 비정렬 OK */
_mm256_storeu_si256((__m256i *)p, a);
_mm256_stream_si256((__m256i *)p, a);              /* Non-Temporal */

/* 정수 산술 — int32 */
__m256i sum  = _mm256_add_epi32(a, b);   /* 8개 int32 덧셈 */
__m256i diff = _mm256_sub_epi32(a, b);   /* 8개 int32 뺄셈 */
__m256i mul  = _mm256_mullo_epi32(a, b); /* 하위 32비트 곱셈 */

/* 논리 연산 */
__m256i x = _mm256_xor_si256(a, b);     /* 256-bit XOR */
__m256i y = _mm256_and_si256(a, b);     /* 256-bit AND */

/* 부동소수점 FMA */
__m256 r = _mm256_fmadd_ps(a_ps, b_ps, c_ps);
/* r[i] = a[i]*b[i] + c[i]  (단정밀도 8개 병렬 FMA) */

/* 셔플/퍼뮤트 */
__m256i perm = _mm256_permute2x128_si256(a, b, 0x31);
/* 결과 상위 128-bit = b 하위, 결과 하위 128-bit = a 상위 */
__m256i shuf = _mm256_shuffle_epi8(a, mask); /* PSHUFB 기반 바이트 셔플 */

/* 비교 → 마스크 */
__m256i cmp   = _mm256_cmpeq_epi32(a, b);
int     mask32 = _mm256_movemask_epi8(cmp); /* 각 바이트 MSB → 비트 마스크 */

/* 실용 예: 8개 uint32 배열 XOR */
void xor_8x32(uint32_t *dst, const uint32_t *src, size_t n)
{
    for (size_t i = 0; i < n; i += 8) {
        __m256i vd = _mm256_loadu_si256((const __m256i *)(dst + i));
        __m256i vs = _mm256_loadu_si256((const __m256i *)(src + i));
        _mm256_storeu_si256((__m256i *)(dst + i), _mm256_xor_si256(vd, vs));
    }
}

AVX-512 마스크 연산 Intrinsic

/* AVX-512 고유 특성: __mmask8 / __mmask16 / __mmask32 / __mmask64 */
#include <immintrin.h>

/* 마스크 생성: 비교 결과 → 마스크 */
__mmask16 k = _mm512_cmp_epi32_mask(a, b, _MM_CMPINT_LT);
/* 16개 int32 비교: a[i] < b[i]이면 k 비트 i = 1 */

/* 마스크 적용 산술 — zeroing (k=0인 레인은 0) */
__m512i result = _mm512_maskz_add_epi32(k, a, b);

/* 마스크 적용 산술 — merging (k=0인 레인은 src 유지) */
__m512i merged = _mm512_mask_add_epi32(src, k, a, b);

/* 마스크 로드/스토어 (루프 테일 처리에 활용) */
__m512i data = _mm512_maskz_loadu_epi32(k, ptr);
_mm512_mask_storeu_epi32(ptr, k, data);

/* 마스크 비트 연산 */
__mmask16 k_and = _kand_mask16(k1, k2);
__mmask16 k_not = _knot_mask16(k1);

/* 루프 테일 처리 패턴 */
void process_avx512(float *arr, size_t n)
{
    size_t i;
    for (i = 0; i + 16 <= n; i += 16) {
        __m512 v = _mm512_loadu_ps(arr + i);
        _mm512_storeu_ps(arr + i, _mm512_mul_ps(v, v));
    }
    if (i < n) {
        __mmask16 tail = (__mmask16)((1u << (n - i)) - 1u);
        __m512 v = _mm512_maskz_loadu_ps(tail, arr + i);
        _mm512_mask_storeu_ps(arr + i, tail, _mm512_mul_ps(v, v));
    }
}

/* 커널에서 Intrinsic보다 인라인 어셈블리 선호 이유:
 * 1. 커널 기본 CFLAGS: -mno-sse, -mno-avx 등 SIMD 전면 비활성화
 *    → 개별 파일/함수에 __attribute__((target("avx2"))) 필요
 * 2. 컴파일러 버전별 VZEROUPPER 삽입 위치 불확실
 * 3. 레지스터 clobber 목록의 명시적 제어 필요
 * 4. 크리티컬 패스에서 어셈블리가 코드 검토를 더 명확히 함
 * → 커널 crypto/RAID 코드는 거의 모두 인라인 asm 또는 .S 파일 */

실전 패턴 분석

수평 합산(Horizontal Sum) — 벡터 전체를 단일 스칼라로 축소

배열의 전체 합산처럼 SIMD로 병렬 누적한 후 최종 결과를 하나의 값으로 줄여야 할 때 사용합니다. SIMD에서 수평 축소(Horizontal Reduction)는 수직 연산보다 비용이 높으므로, 루프 안이 아닌 루프 종료 후 한 번만 수행하는 것이 핵심입니다.

#include <immintrin.h>

/* int32 배열의 전체 합계를 AVX2로 계산 */
int32_t sum_array_avx2(const int32_t *arr, size_t n)
{
    __m256i acc = _mm256_setzero_si256();  /* 8×int32 누적기 초기화 */
    size_t i;

    /* ── 메인 루프: 8개씩 수직 덧셈 ── */
    for (i = 0; i + 8 <= n; i += 8) {
        __m256i v = _mm256_loadu_si256((const __m256i *)(arr + i));
        acc = _mm256_add_epi32(acc, v);  /* 8개 레인 독립 누적 */
    }

    /* ── 수평 축소: 256-bit → 128-bit → 스칼라 ──
     *
     * acc = [a7, a6, a5, a4 | a3, a2, a1, a0]  (| = 레인 경계)
     *
     * Step 1: 상위 128-bit + 하위 128-bit
     *   hi128 = [a7, a6, a5, a4]
     *   sum128 = [a7+a3, a6+a2, a5+a1, a4+a0]
     *
     * Step 2: PSHUFD로 [2,3] → [0,1] 위치로 이동 후 덧셈
     *   sum128 = [*, *, a7+a3+a5+a1, a6+a2+a4+a0]
     *
     * Step 3: PSHUFD로 [1] → [0] 위치로 이동 후 최종 덧셈
     *   sum128 = [*, *, *, 전체합]
     */
    __m128i hi128  = _mm256_extracti128_si256(acc, 1);
    __m128i sum128 = _mm_add_epi32(_mm256_castsi256_si128(acc), hi128);
    __m128i shuf   = _mm_shuffle_epi32(sum128, _MM_SHUFFLE(1,0,3,2));
    sum128 = _mm_add_epi32(sum128, shuf);
    shuf   = _mm_shuffle_epi32(sum128, _MM_SHUFFLE(0,1,0,1));
    sum128 = _mm_add_epi32(sum128, shuf);

    int32_t total = _mm_cvtsi128_si32(sum128); /* 최하위 int32 추출 */

    /* ── 테일 처리: 나머지 요소 스칼라 덧셈 ── */
    for (; i < n; i++)
        total += arr[i];

    return total;
}
/* 성능 분석:
 * - 메인 루프: vpaddd 1개/iter, 레이턴시 1 cycle, 처리량 0.5 cycle (포트 0/5)
 * - 수평 축소: 고정 비용 ~6 cycle (전체 루프 밖 1회)
 * - 1024개 int32: 스칼라 ~1024 cycle vs AVX2 ~128+6 ≈ 134 cycle (7.6배 가속) */

바이트 검색(memchr) — SIMD 문자열/메모리 탐색 패턴

특정 바이트 값을 대량 데이터에서 찾는 패턴입니다. _mm_cmpeq_epi8 + _mm_movemask_epi8 조합은 glibc memchr, strlen, strchr 등의 핵심입니다.

/* SSE2 기반 memchr 구현 — 16바이트씩 병렬 비교 */
const uint8_t *fast_memchr_sse2(const uint8_t *buf, uint8_t needle, size_t len)
{
    __m128i pattern = _mm_set1_epi8((char)needle); /* 16바이트 모두 동일 값 */
    size_t i;

    for (i = 0; i + 16 <= len; i += 16) {
        __m128i chunk = _mm_loadu_si128((const __m128i *)(buf + i));
        __m128i cmp   = _mm_cmpeq_epi8(chunk, pattern);
        int mask = _mm_movemask_epi8(cmp);
        /* mask의 각 비트: 해당 위치 바이트가 needle과 같으면 1 */

        if (mask != 0)
            return buf + i + __builtin_ctz(mask); /* 첫 1-비트 위치 = 매치 오프셋 */
    }

    /* 테일 처리 */
    for (; i < len; i++)
        if (buf[i] == needle) return buf + i;

    return NULL;
}
/* 패턴 분석:
 * pcmpeqb + pmovmskb + tzcnt → 3개 명령으로 16바이트 동시 검색
 * 스칼라 대비 약 8~12배 빠름 (캐시 상태에 따라 다름)
 * glibc, musl, 커널 lib/string.c 등에서 동일 패턴 사용 */

4×4 행렬 전치(Transpose) — 셔플 조합의 전형

4×4 float 행렬 전치는 이미지 처리, 선형 대수(Linear Algebra), AoS↔SoA 변환에서 빈번합니다. SSE의 _MM_TRANSPOSE4_PS 매크로가 이 패턴을 제공합니다.

/* 4×4 float 행렬 전치 — _MM_TRANSPOSE4_PS의 내부 동작 */
void transpose_4x4(__m128 *r0, __m128 *r1, __m128 *r2, __m128 *r3)
{
    /* 입력:
     * r0 = [a0, a1, a2, a3]
     * r1 = [b0, b1, b2, b3]
     * r2 = [c0, c1, c2, c3]
     * r3 = [d0, d1, d2, d3]
     */

    /* Step 1: 하위 절반 인터리브 */
    __m128 t0 = _mm_unpacklo_ps(*r0, *r1); /* [a0,b0,a1,b1] */
    __m128 t1 = _mm_unpackhi_ps(*r0, *r1); /* [a2,b2,a3,b3] */
    __m128 t2 = _mm_unpacklo_ps(*r2, *r3); /* [c0,d0,c1,d1] */
    __m128 t3 = _mm_unpackhi_ps(*r2, *r3); /* [c2,d2,c3,d3] */

    /* Step 2: 64-bit 블록 이동 */
    *r0 = _mm_movelh_ps(t0, t2); /* [a0,b0,c0,d0] ← 전치된 열 0 */
    *r1 = _mm_movehl_ps(t2, t0); /* [a1,b1,c1,d1] ← 전치된 열 1 */
    *r2 = _mm_movelh_ps(t1, t3); /* [a2,b2,c2,d2] ← 전치된 열 2 */
    *r3 = _mm_movehl_ps(t3, t1); /* [a3,b3,c3,d3] ← 전치된 열 3 */

    /* 총 8개 명령어, 레이턴시 약 4 cycle (포트 5 경합)
     * unpacklo/hi: 각 1 cycle 레이턴시, 포트 5
     * movelh/hl:   각 1 cycle 레이턴시, 포트 5
     * 스칼라(16회 로드+16회 스토어)보다 ~4배 빠름 */
}

값 범위 제한(Clamp) — min/max 조합

/* float 배열을 [lo, hi] 범위로 클램프 — AVX */
void clamp_avx(float *arr, size_t n, float lo, float hi)
{
    __m256 vlo = _mm256_set1_ps(lo);
    __m256 vhi = _mm256_set1_ps(hi);

    for (size_t i = 0; i + 8 <= n; i += 8) {
        __m256 v = _mm256_loadu_ps(arr + i);
        v = _mm256_max_ps(v, vlo);  /* v < lo인 요소를 lo로 올림 */
        v = _mm256_min_ps(v, vhi);  /* v > hi인 요소를 hi로 내림 */
        _mm256_storeu_ps(arr + i, v);
    }
    /* ... 테일 처리 생략 ... */
}
/* vmaxps + vminps: 각 1 cycle 레이턴시, 서로 다른 포트 사용 가능 → 파이프라인 병렬
 * 분기(branch) 없이 8개 float를 동시에 클램프 */

ARM NEON Intrinsic (arm_neon.h)

ARM NEON Intrinsic은 <arm_neon.h> 헤더를 통해 제공됩니다. x86과 달리 타입 이름에 요소 크기와 개수가 명시적으로 포함되어 있어 가독성이 높습니다.

NEON 타입 체계

타입요소개수비트 폭대응 x86 타입
uint8x16_tuint816128__m128i (epu8)
int32x4_tint324128__m128i (epi32)
float32x4_tfloat324128__m128
float64x2_tfloat642128__m128d
uint8x16x2_tuint816×2256—(구조체(Struct) 배열)
int32x4x4_tint324×4512—(LD4/ST4용)

NEON 네이밍 규칙

구성 요소설명예시
v 접두사벡터(Vector) 연산 표시vaddq_s32
연산명수행할 연산vaddq_s32
q 접미사128-bit(Quadword) 연산. 없으면 64-bitvaddq_s32
l 접미사Long — 결과 폭이 입력의 2배vmull_s16 → int16→int32
n 접미사Narrow — 결과 폭이 입력의 절반vqmovn_s32 → int32→int16
타입 접미사요소의 부호(Sign)와 비트 수vaddq_s32=signed32, u8=unsigned8, f32=float32
#include <arm_neon.h>

/* ── 기본 연산 ── */
int32x4_t a = vld1q_s32(src);              /* 128-bit 로드 → ld1 {v0.4s},[x0] */
int32x4_t b = vdupq_n_s32(42);             /* 브로드캐스트 → dup v1.4s, w0 */
int32x4_t sum = vaddq_s32(a, b);           /* 4×int32 덧셈 → add v0.4s,v1.4s,v2.4s */
vst1q_s32(dst, sum);                       /* 128-bit 스토어 → st1 {v0.4s},[x0] */

/* ── 곱셈-누적(Multiply-Accumulate) — NEON의 강점 ── */
int32x4_t acc = vmlaq_s32(acc, a, b);      /* acc += a * b → mla v0.4s,v1.4s,v2.4s */
float32x4_t facc = vfmaq_f32(facc, fa, fb); /* FMA: facc += fa * fb → fmla */

/* ── 비교와 선택 ── */
uint32x4_t mask = vcgtq_s32(a, b);         /* a > b → 0xFFFFFFFF, else 0 */
int32x4_t sel = vbslq_s32(mask, a, b);     /* mask=1→a, mask=0→b (x86 blendv) */

/* ── 내적(Dot Product) — Armv8.2+ (asimddp) ── */
int32x4_t dot = vdotq_s32(acc, a8, b8);
/* 4그룹 × 4바이트 내적 누적. 머신러닝 추론 핵심 명령.
 * 각 int32 요소 = Σ(a8[i*4+k] × b8[i*4+k]), k=0..3 */

/* ── 구조적 로드/스토어(Structure Load/Store) ── */
uint8x16x3_t rgb = vld3q_u8(pixel_data);
/* ld3 {v0.16b-v2.16b},[x0]
 * RGBRGBRGB... → R=rgb.val[0], G=rgb.val[1], B=rgb.val[2]
 * x86에서는 셔플 조합으로 구현해야 할 것을 1 명령으로 처리
 * 이미지 처리, 센서 데이터 파싱에서 NEON의 큰 강점 */

/* ── 수평 합산 ── */
int32_t hsum = vaddvq_s32(sum);             /* addv s0, v0.4s → 4개 요소 합산 스칼라 */
/* Armv8.1+ 단일 명령 수평 축소. x86은 셔플 조합이 필요하지만
 * NEON은 ADDV/SMAXV/SMINV/UMAXV/UMINV 등으로 직접 지원 */

/* ── 포화 연산(Saturating Arithmetic) ── */
uint8x16_t bright = vqaddq_u8(pixels, offset);
/* uqadd: 255 초과 시 255로 클램프. 이미지 밝기 조절.
 * x86의 _mm_adds_epu8과 동일 */

NEON vs x86 Intrinsic 핵심 차이

특성x86 (SSE/AVX)ARM (NEON)
타입 체계__m128i 하나로 모든 정수 표현uint8x16_t, int32x4_t 등 요소별 별도 타입
요소 크기 결정Intrinsic 함수 접미사변수 타입 자체
구조적 로드없음 (셔플 조합 필요)LD2/LD3/LD4 — 하드웨어 디인터리브
수평 축소셔플 + 연산 체인ADDV/MAXV 등 단일 명령
포화 연산8/16-bit만 (_mm_adds_*)모든 크기 (vqadd, vqsub, vqmovn)
마스크 레지스터AVX-512: k0-k7 전용 레지스터비교 결과가 같은 크기 벡터 (별도 마스크 레지스터 없음)
레인 크로싱AVX2+: VPERMD 등으로 가능NEON: 불가. SVE/SVE2: TBL 확장으로 가능
헤더<immintrin.h> (통합)<arm_neon.h>

Intrinsic 사용 시 흔한 실수

실수증상원인해결
정렬 미스매치 SIGBUS 또는 #GP 예외 (Segfault) _mm256_load_si256에 비정렬 주소 전달 _mm256_loadu_si256 사용하거나 __attribute__((aligned(32))) 적용
_mm_set 인자 순서 데이터 역순으로 처리 _mm_set_epi32(a,b,c,d)에서 d가 [0]번 요소 _mm_setr_epi32(a,b,c,d) 사용 (메모리 순서)
VPSHUFB 레인 경계 256-bit에서 예상과 다른 셔플 결과 AVX2 VPSHUFB는 128-bit 레인 독립 동작. 레인 간 교차 불가 레인 간 이동이 필요하면 _mm256_permutevar8x32_epi32 선 적용
VEX/레거시 혼용 성능 급감 (SSE-AVX 전환 페널티) AVX 명령(VEX) 후 레거시 SSE 명령 사용. 상위 128-bit 상태 충돌 함수 끝에 _mm256_zeroupper() 호출하거나 VEX 인코딩만 사용
AVX-512 주파수 스로틀링 전체 코어 성능 저하 512-bit 명령 사용 시 CPU가 주파수를 낮춤 (License Level 2) 짧은 SIMD 구간이면 AVX2(256-bit) 선호. 512-bit는 대량 데이터만
컴파일 플래그 누락 빌드 에러: "inlining failed" 또는 undefined intrinsic -mavx2 등 ISA 활성화 플래그 없이 Intrinsic 호출 -mavx2 -mfma 추가하거나 __attribute__((target("avx2"))) 함수 단위 적용
부호 해석 불일치 비교/시프트 결과 오류 _mm_cmpgt_epi8로 unsigned 바이트 비교 (결과 틀림) unsigned 비교: XOR 0x80으로 부호 반전 후 signed 비교, 또는 SSE4.1 min/max 활용
테일 처리 누락 배열 끝 데이터 미처리 또는 버퍼(Buffer) 오버리드(Buffer Over-Read) 배열 크기가 벡터 폭의 배수가 아닌 경우 AVX-512: 마스크 로드/스토어. SSE/AVX: 스칼라 루프 또는 오버랩 처리
커널에서 Intrinsic을 쓸 수 없는 이유: Linux 커널은 -mno-sse -mno-mmx -mno-sse2로 컴파일하여 컴파일러가 SIMD 명령어를 사용하지 못하게 합니다. 이 상태에서 Intrinsic을 호출하면 빌드 자체가 실패합니다. 커널 SIMD 코드는 별도 .S 파일(어셈블리)이나, __attribute__((target("avx2")))를 붙인 별도 .c 파일에 격리(Isolation)하고, 해당 파일의 Makefile에 CFLAGS_파일.o += -mavx2를 추가해야 합니다.

Intrinsic ↔ 어셈블리 주요 매핑 레퍼런스

자주 사용하는 Intrinsic과 대응 어셈블리 명령어, 지연(Latency), 처리량의 빠른 참조입니다. 지연/처리량은 Intel Skylake 기준입니다.

Intrinsic어셈블리동작LatTP
_mm256_add_epi32VPADDD8×int32 덧셈10.33
_mm256_mullo_epi32VPMULLD8×int32 곱셈 (하위 32-bit)101
_mm256_fmadd_psVFMADD231PS8×float FMA (a*b+c)40.5
_mm256_loadu_si256VMOVDQU256-bit 비정렬 로드5*0.5
_mm256_shuffle_epi8VPSHUFB레인 내 바이트 셔플11
_mm256_permutevar8x32_epi32VPERMD8×int32 레인 크로싱 셔플31
_mm_cmpeq_epi8PCMPEQB16×byte 동등 비교→마스크10.5
_mm_movemask_epi8PMOVMSKB16바이트 MSB→16-bit int21
_mm256_xor_si256VPXOR256-bit XOR10.33
_mm256_stream_si256VMOVNTDQ256-bit NT 스토어~400*1
_mm256_extracti128_si256VEXTRACTI128상위 128-bit 추출31
_mm_crc32_u64CRC32QCRC32C 64-bit 누적31
_mm256_setzero_si256VPXOR (self)제로 초기화0†0.25
_mm256_set1_epi32VPBROADCASTD스칼라→8개 요소 복제10.5
_mm256_blendv_epi8VPBLENDVB바이트별 조건 선택21

Lat = 지연, TP = 처리량. * = L1 캐시 히트 기준. † = 레지스터 리네이밍으로 실행 유닛 불필요.

성능 측정 팁: Intrinsic의 실제 성능은 uops.info에서 마이크로아키텍처(Microarchitecture)별 정확한 지연, 처리량, 포트 사용을 확인할 수 있습니다. Intel Intrinsics Guide는 공식 스펙을, uops.info는 실측치를 제공하므로 둘 다 참고하면 정확한 최적화가 가능합니다.

커널에서의 SIMD 사용 — 핵심 제약

왜 kernel_fpu_begin()과 kernel_fpu_end()가 꼭 필요한가

핵심은 XMM/YMM/ZMM 레지스터가 함수마다 새로 생기는 개인 메모장이 아니라, 현재 CPU가 실제로 들고 있는 공용 작업 공간이라는 점입니다. 유저 프로세스가 이미지 처리, 브라우저 엔진, 데이터베이스 계산처럼 SIMD를 많이 쓰는 작업을 하다가 시스템 콜(System Call)이나 인터럽트(Interrupt)로 커널에 들어올 수 있습니다. 그 순간 커널이 같은 레지스터를 곧바로 덮어쓰면, 유저 프로세스는 자신이 계산하던 중간값을 잃어버린 채 다시 실행됩니다.

이 문제를 더 정확히 표현하면, 커널은 FP/SIMD 레지스터를 일반 커널 문맥의 일부로 항상 저장·복원하도록 설계되지 않았습니다는 뜻입니다. 유저 태스크(Task)의 FP 상태는 필요할 때 관리되지만, 일반 커널 경로는 기본적으로 "커널은 FP를 쓰지 않습니다"는 전제를 깔고 돌아갑니다. 따라서 커널이 예외적으로 SIMD를 사용하려면 kernel_fpu_begin()으로 지금부터 FP/SIMD 레지스터를 커널이 잠깐 점유하겠다고 선언하고, 그 구간이 다른 문맥과 섞이지 않도록 보호해야 합니다. kernel_fpu_end()는 그 점유를 끝내고 원래 규칙으로 복귀시키는 호출입니다.

설계가 이렇게 된 가장 큰 이유는 비용이 너무 크기 때문입니다. 커널은 시스템 콜, 예외, 인터럽트, 선점(Preemption)처럼 매우 자주 실행되는 경로를 지날 때마다 가능한 한 적은 상태만 저장하려고 합니다. 그런데 FP/SIMD 상태는 x87 수준에서 끝나지 않고 SSE, AVX, AVX-512, AMX로 갈수록 저장해야 할 레지스터 집합이 크게 늘어납니다. 이 큰 상태를 모든 커널 진입과 모든 커널 선점에서 항상 저장·복원하면, FP를 전혀 쓰지 않는 대부분의 커널 코드까지 매번 비용을 부담하게 됩니다. 그래서 Linux는 기본 경로를 가볍게 유지하고, 정말 필요한 코드만 짧은 보호 구간 안에서 비용을 지불하는 방식을 택합니다.

여기에 구현 단순성도 있습니다. 일부 아키텍처는 성능과 단순성을 위해 FP 레지스터를 지연 저장/복원(lazy preserve/restore)하거나, 커널 모드에서는 별도 보존 메커니즘을 넉넉하게 두지 않는 방식을 사용합니다. 또한 컴파일러가 최적화 과정에서 개발자가 의도하지 않은 FP/SIMD 명령을 끼워 넣을 수 있으므로, 커널 문서는 FP 코드를 별도 파일로 분리하고 비FP 코드 밖으로 새어나가지 않게 하라고 요구합니다. 즉 이 API는 단순히 문맥 전환 경로의 한계를 메우는 보조 함수가 아니라, 커널 전체가 "FP/SIMD는 예외적으로만, 명시적으로만 사용합니다"는 정책을 지키게 만드는 경계선입니다.

설계 배경에는 성능과 단순성 이유가 있지만, 호출 규칙 자체는 정확성 보장 규칙입니다. SIMD 명령은 매우 빠르지만, 보호 절차 없이 쓰면 다른 프로세스의 부동소수점 계산 결과를 망가뜨릴 수 있습니다. 이런 버그는 즉시 커널 패닉(Kernel Panic)으로 드러나지 않고, 어떤 프로그램에서만 숫자가 틀리는 식으로 늦게 나타나기 때문에 더 위험합니다.

일상 비유: 공용 화이트보드를 떠올리면 이해가 쉽습니다. 유저 프로세스가 화이트보드에 계산 중간값을 적어 두었는데, 커널이 잠깐 들어와 그 내용을 지우고 자기 계산을 하면 원래 사용자는 이어서 계산할 수 없습니다. kernel_fpu_begin()은 화이트보드 내용을 먼저 안전하게 보관하는 단계이고, kernel_fpu_end()는 원래 내용을 다시 돌려놓는 단계입니다.
상황커널 내부에서 일어나는 일결과
그냥 SIMD 사용커널이 현재 태스크의 XMM/YMM 상태를 직접 덮어쓸 수 있습니다.유저 공간 계산 결과가 틀어질 수 있습니다.
kernel_fpu_begin() 호출기존 FPU 상태를 보호하고, 짧은 SIMD 사용 구간을 엽니다.커널이 안전하게 SIMD를 사용할 준비가 됩니다.
kernel_fpu_end() 호출보호 구간을 닫고 원래 상태와 실행 규칙으로 복귀합니다.유저 프로세스와 문맥 전환 상태가 기대대로 유지됩니다.

생략하면 실제로 무엇이 망가지는가

예를 들어 유저 공간의 동영상 인코더가 YMM 레지스터에 색 변환 계수와 누산값을 담아 둔 상태에서 파일 입출력(I/O) 시스템 콜을 호출했다고 가정하겠습니다. 이때 커널이 체크섬(Checksum) 계산이나 암호화 가속을 위해 AVX 명령을 직접 실행하면서 상태를 보존하지 않으면, 시스템 콜이 끝난 뒤 유저 프로그램은 손상된 레지스터 값으로 계산을 이어 가게 됩니다. 소스 코드 관점에서는 서로 다른 함수처럼 보여도, 실제 하드웨어 레지스터는 같은 CPU 위에서 번갈아 사용되므로 보호 절차가 필요합니다.

보호 구간이 필요한 직접적인 이유 중 하나는 선점과 스케줄링(Scheduling)입니다. 커널이 SIMD 레지스터를 잡은 채 다른 태스크로 전환되면 어떤 상태가 누구 것인지 관리가 어려워지고, 복원 시점도 복잡해집니다. 그래서 kernel_fpu_begin()은 보통 선점을 막고, 환경에 따라서는 softirq 개입도 제한하며, 개발자에게 "이 구간에서는 자지 말고 아주 짧게 끝내라"는 제약을 함께 부여합니다. 따라서 이 API는 단순 저장 함수가 아니라, 짧고 엄격한 임계 구간을 여는 장치로 이해하는 편이 정확합니다.

커널 SIMD 사용 라이프사이클 (kernel_fpu_begin / kernel_fpu_end) 임계 구간 — 선점·softirq 차단 일반 커널 코드 SIMD 미사용 기본 경로 FP 상태 저장 안 함 kernel_fpu_begin() 유저 FPU 상태 저장 선점·softirq 차단 SIMD 사용 구간 SSE / AVX / AVX-512 구간은 가능한 짧게 kernel_fpu_end() 임계 구간 종료 선점·softirq 재허용 일반 커널 코드 원래 실행 규칙으로 복귀 유저 FP 상태는 유저 복귀 시 복원 임계 구간 안에서는 sleep·블로킹 호출을 할 수 없습니다 이 절차 없이 커널에서 SIMD를 사용하면 유저 프로세스의 FPU 상태가 오염됩니다
커널 SIMD 사용 라이프사이클 — 일반 커널 코드에서 kernel_fpu_begin()으로 임계 구간에 들어가 유저 FPU 상태를 저장하고 선점·softirq를 막은 뒤, 그 구간 안에서 SSE·AVX·AVX-512를 짧게 사용하고, kernel_fpu_end()로 임계 구간을 닫아 다시 일반 커널 코드로 복귀합니다. 개념 개요 수준(호출 순서와 각 단계의 실행 규칙)이며, XSAVE·lazy restore 같은 내부 구현은 아래 섹션에서 다룹니다
/* 커널 코드에서 SIMD 레지스터를 직접 사용할 수 없습니다! */
/*
 * 이유:
 * 1. 커널은 유저 프로세스의 FPU/SIMD 상태를 보존해야 함
 * 2. 인터럽트/softirq 컨텍스트에서 FPU 상태가 정의되지 않음
 * 3. preempt 시 FPU 상태가 손상될 수 있음
 *
 * 해결: kernel_fpu_begin() / kernel_fpu_end()
 * 이 API가 유저 FPU 상태를 저장/복원하고 preempt를 비활성화
 */

#include <asm/fpu/api.h>

void my_aes_encrypt(u8 *dst, const u8 *src, const u8 *key)
{
    /* FPU/SIMD 사용 전에 반드시 호출 */
    kernel_fpu_begin();

    /* 이 구간에서만 SSE/AVX/AES-NI 명령어 사용 가능 */
    asm volatile(
        "movdqu (%[key]), %%xmm0\n"
        "movdqu (%[src]), %%xmm1\n"
        "pxor   %%xmm0, %%xmm1\n"
        "aesenc %%xmm0, %%xmm1\n"
        "movdqu %%xmm1, (%[dst])\n"
        :: [dst] "r"(dst), [src] "r"(src), [key] "r"(key)
        : "xmm0", "xmm1", "memory"
    );

    /* FPU/SIMD 사용 후 반드시 호출 */
    kernel_fpu_end();
}

/* ARM64에서의 SIMD 사용 */
#include <asm/neon.h>
kernel_neon_begin();
/* NEON/SVE 명령어 사용 가능 */
kernel_neon_end();
커널 SIMD 사용의 엄격한 규칙:
  1. kernel_fpu_begin/end 필수 — 이 쌍 없이 SIMD 레지스터를 사용하면 유저 프로세스의 FPU 상태가 손상됨. 결과: 프로세스의 부동소수점 연산이 오염되어 무작위 연산 오류
  2. 컨텍스트 제한 — kernel_fpu_begin()은 내부에서 fpregs_lock()을 호출합니다. 이 함수는 CONFIG_PREEMPT_RT가 꺼진 커널에서 local_bh_disable()로 선점과 softirq를 함께 막고, RT 커널에서는 preempt_disable()로 선점만 막습니다(RT에서는 bottom half가 스레드 문맥에서 돌기 때문입니다). 해제는 대응 함수인 fpregs_unlock()이 담당하며, 이는 kernel_fpu_end()에서 호출됩니다. local_bh_disable()도 카운터를 올려 선점을 함께 막으므로 어느 경로든 sleep 불가입니다. 이미 인터럽트가 비활성화된 문맥(irqs_disabled(), 예: 하드 IRQ 핸들러 내부)에서는 이 잠금을 건너뜁니다. softirq 문맥과 softirq가 살아 있는 하드 IRQ 핸들러(Handler)에서는 그대로 사용할 수 있지만, NMI이거나 위 임계 구간 안으로 들어온 하드 IRQ라면 irq_fpu_usable()이 false가 되어 사용할 수 없습니다.
  3. 최소 구간 사용 — FPU begin/end 구간이 길면 preempt 지연이 증가하여 시스템 응답성 저하. 가능한 짧게 유지
  4. 대안: SIMD 전용 함수 — crypto API는 별도 SIMD 최적화 함수를 *-aesni, *-neon 등으로 등록하여 자동 관리
  5. CONFIG_CRYPTO_SIMD — 비동기 crypto에서 SIMD를 안전하게 사용하기 위한 래퍼. softirq에서 자동으로 generic fallback

XSAVE/XRSTOR 메커니즘 상세

XSAVE는 FPU/SIMD 상태를 확장 가능한 방식으로 저장/복원하는 x86 메커니즘입니다. kernel_fpu_begin()의 핵심 구현이며, 새로운 SIMD 확장이 추가될 때마다 자동으로 대응합니다.

XSAVE 상태 영역 컴포넌트 구조 (CPUID Leaf 0x0D 기반) 0x000 0x200 0x240 가변 레거시 영역 — 512B, FXSAVE 호환 (오프셋 0x000 고정) 비트 0 — x87 FPU 상태 ST0-ST7, FPU 제어·상태 워드 비트 1 — SSE 상태 XMM0-XMM15, MXCSR XSAVE 헤더 — 64B (XSTATE_BV · XCOMP_BV) 비트 2 — AVX (YMM0-YMM15 상위 128비트, 256B) 확장 상태 영역 — 컴포넌트마다 크기와 오프셋이 달라집니다 비트 3 — MPX BNDREGS 64B, 경계 레지스터 4개 비트 4 — MPX BNDCSR 64B, BNDCFGU · BNDSTATUS 비트 5 — AVX-512 opmask 64B, k0-k7 마스크 비트 6 — AVX-512 ZMM_Hi256 512B, ZMM0-15 상위 256비트 비트 7 — AVX-512 Hi16_ZMM 1,024B, ZMM16-31 전체 비트 9 — PKRU 8B, 페이지 보호키 레지스터 비트 11 — CET_U 16B, 유저 제어 흐름 보호 비트 12 — CET_S 24B, 시스템 CET 상태 비트 17 — XTILECFG (AMX) 64B, 타일 크기·형태 설정 비트 18 — XTILEDATA (AMX) 8,192B, TMM0-TMM7 타일 데이터 확장 컴포넌트는 CPUID Leaf 0x0D Sub-leaf i로 크기(EAX)와 오프셋(EBX)을 확인합니다. 새 확장이 추가되면 커널은 그 비트를 감지해 저장 영역 크기를 계산합니다.
XSAVE 상태 영역의 컴포넌트 배치 — 비트 0·1은 FXSAVE와 호환되는 레거시 영역(0x000, 512B)에 함께 들어가고, 이어서 XSAVE 헤더(0x200, 64B)와 AVX 상위 128비트(0x240, 256B)가 옵니다. 그 뒤의 확장 컴포넌트는 저마다 크기와 오프셋(Offset)이 달라 CPUID Leaf 0x0D Sub-leaf i가 알려주는 EAX(크기)·EBX(오프셋)로 배치가 정해집니다. 개념 개요 수준의 배치도입니다.
/* XSAVE 상태 컴포넌트 (CPUID Leaf 0x0D로 확인) */
/*
 * 비트  컴포넌트              크기       오프셋
 * ─────────────────────────────────────────────────
 *  0   x87 FPU              512 bytes  0 (고정)
 *  1   SSE (XMM0-XMM15)     (위에 포함)  0 (고정)
 *  2   AVX (YMM 상위 128)   256 bytes  576
 *  3   MPX BNDREGS          64 bytes   (가변)
 *  4   MPX BNDCSR           64 bytes   (가변)
 *  5   AVX-512 opmask (k)   64 bytes   (가변)
 *  6   AVX-512 ZMM_Hi256    512 bytes  (가변)
 *  7   AVX-512 Hi16_ZMM     1024 bytes (가변)
 *  9   PKRU                 8 bytes    (가변)
 * 11   CET_U                16 bytes   (가변)
 * 12   CET_S                24 bytes   (가변)
 * 17   TILECFG (AMX)        64 bytes   (가변)
 * 18   TILEDATA (AMX)       8192 bytes (가변)
 */

/* 커널의 XSAVE 영역 크기 결정 — arch/x86/kernel/fpu/xstate.c */
void fpu__init_system_xstate(unsigned int legacy_size)
{
    u64 xfeatures_mask;
    unsigned int eax, ebx, ecx, edx;

    /* CPUID Leaf 0x0D, Sub-leaf 0: 지원 컴포넌트 마스크 */
    cpuid_count(0x0D, 0, &eax, &ebx, &ecx, &edx);
    xfeatures_mask = eax | ((u64)edx << 32);
    /* ebx = 현재 XCR0 기준 XSAVE 크기 */
    /* ecx = 지원되는 모든 컴포넌트의 최대 XSAVE 크기 */

    /* 각 컴포넌트의 크기/오프셋 조회 */
    for (int i = 2; i < 64; i++) {
        if (!(xfeatures_mask & (1ULL << i))) continue;
        cpuid_count(0x0D, i, &eax, &ebx, &ecx, &edx);
        /* eax = 컴포넌트 크기, ebx = 오프셋 */
        /* ecx bit 1: 이 컴포넌트가 compacted XSAVE에서 정렬 필요 */
    }
}

/* XSAVE 변종 명령어 */
/* XSAVE:    모든 컴포넌트 저장 (느림, 초기 변종) */
/* XSAVEOPT: 변경된 컴포넌트만 저장 (최적화) */
/* XSAVEC:   Compacted 형식으로 저장 (커널 기본) */
/* XSAVES:   Supervisor 컴포넌트 포함 + compacted */
/* XRSTOR:   컴포넌트 복원 */
/* XRSTORS:  Supervisor 컴포넌트 포함 복원 */

/* kernel_fpu_begin() 내부 흐름 */
void kernel_fpu_begin_mask(unsigned int kfpu_mask)
{
    preempt_disable();

    if (!test_thread_flag(TIF_NEED_FPU_LOAD)) {
        /* 유저 FPU 상태가 레지스터에 live → 메모리에 저장 */
        fpu_save_regs(fpu);
        /* 내부적으로 XSAVES 또는 XSAVEC 실행 */
    }
    /* 이제 커널 코드가 FPU/SIMD 레지스터를 자유롭게 사용 가능 */
}

void kernel_fpu_end(void)
{
    /* TIF_NEED_FPU_LOAD 플래그 설정 → 다음 유저 복귀 시 lazy restore */
    set_thread_flag(TIF_NEED_FPU_LOAD);
    preempt_enable();
}

XSAVE 크기의 실무적 영향: SSE만 사용하면 XSAVE 영역이 ~576바이트이지만, AVX-512 전체를 사용하면 ~2.5KB 이상으로 증가합니다. AMX(TILEDATA)까지 포함하면 ~10KB에 달합니다. 이는 태스크(Task)당 thread.fpu 메모리 사용량과 컨텍스트 스위치 시간에 직접 영향을 미칩니다. 커널은 init_fpstate에서 각 프로세스가 실제로 사용하는 컴포넌트만 추적하여 불필요한 저장/복원을 최소화합니다.

FPU Lazy Restore 메커니즘 상세

Linux 커널은 FPU 상태 복원을 최대한 지연시켜 컨텍스트 스위치 비용을 줄입니다. TIF_NEED_FPU_LOAD 플래그가 이 최적화의 핵심입니다.

TIF_NEED_FPU_LOAD 기반 FPU 지연 복원 흐름 유저 실행 (Task A) FPU 레지스터에 상태 live 컨텍스트 스위치 switch_fpu(prev_p, cpu) XSAVES/XSAVEC로 FPU 상태 저장 TIF_NEED_FPU_LOAD = 1, last_cpu = cpu 다른 태스크로 전환 다른 태스크 실행 중 Task A의 FPU 상태는 메모리에 있음 유저 복귀 직전 TIF_NEED_FPU_LOAD 값 검사 clear (0) set (1) switch_fpu_return() fpregs_restore_userregs() 호출 fpregs_state_valid() last_cpu·owner ctx 일치 valid invalid 복원 불필요 레지스터가 이미 유효 switch_fpu_return() 미호출 XRSTOR 생략 last_cpu가 현재 CPU와 같고 소유 FPU 컨텍스트가 자신이면 XRSTOR 실행 restore_fpregs_from_fpstate() 메모리에서 레지스터로 적재
TIF_NEED_FPU_LOAD 기반 FPU 지연 복원 흐름 — 컨텍스트 스위치에서 switch_fpu()가 XSAVES/XSAVEC로 이전 태스크의 상태를 메모리에 저장하고 플래그와 last_cpu만 기록한 뒤, 유저 복귀 시 arch_exit_work()가 플래그를 보고 복원 여부를 정합니다. 플래그가 세워져 있어도 fpregs_state_valid()가 참이면(같은 CPU로 복귀하고 이 CPU의 소유 컨텍스트가 자신이면) XRSTOR를 건너뜁니다. x86_64 상세 구현 수준의 흐름도입니다.
/* arch/x86/include/asm/fpu/sched.h — switch_fpu() */
/* __switch_to()에서 호출: 이전 태스크의 FPU 상태를 메모리에 저장 */
static inline void switch_fpu(struct task_struct *old, int cpu)
{
    if (!test_tsk_thread_flag(old, TIF_NEED_FPU_LOAD) &&
        cpu_feature_enabled(X86_FEATURE_FPU) &&
        !(old->flags & (PF_KTHREAD | PF_USER_WORKER))) {
        struct fpu *old_fpu = x86_task_fpu(old);

        set_tsk_thread_flag(old, TIF_NEED_FPU_LOAD);
        save_fpregs_to_fpstate(old_fpu);   /* XSAVES / XSAVEC */
        old_fpu->last_cpu = cpu;              /* 같은 CPU로 복귀하면 복원 생략 */
        trace_x86_fpu_regs_deactivated(old_fpu);
    }
}

/* arch/x86/include/asm/entry-common.h — arch_exit_work() */
/* 유저 복귀 경로(exit_to_user_mode)에서 ti_work 플래그를 검사 */
static inline void arch_exit_work(unsigned long ti_work)
{
    /* ... 유저 리턴 노티파이어, IO bitmap 갱신 ... */

    if (unlikely(ti_work & _TIF_NEED_FPU_LOAD))
        switch_fpu_return();
}

/* arch/x86/kernel/fpu/core.c — switch_fpu_return() */
void switch_fpu_return(void)
{
    if (!static_cpu_has(X86_FEATURE_FPU))
        return;

    fpregs_restore_userregs();
}

/* arch/x86/kernel/fpu/context.h — fpregs_restore_userregs()
 *
 * last_cpu가 현재 CPU와 같고 이 CPU의 fpu_fpregs_owner_ctx가
 * 자신을 가리키면 레지스터가 이미 유효 → XRSTOR 생략
 */
static inline void fpregs_restore_userregs(void)
{
    struct fpu *fpu = x86_task_fpu(current);
    int cpu = smp_processor_id();

    if (WARN_ON_ONCE(current->flags & (PF_KTHREAD | PF_USER_WORKER)))
        return;

    if (!fpregs_state_valid(fpu, cpu)) {
        /* PKRU는 switch_to()/flush_thread()에서 이미 설정됨 → 제외 */
        restore_fpregs_from_fpstate(fpu->fpstate, XFEATURE_MASK_FPSTATE);

        fpregs_activate(fpu);
        fpu->last_cpu = cpu;
    }
    clear_thread_flag(TIF_NEED_FPU_LOAD);
}

/* kernel_fpu_begin()이 lazy restore에 미치는 영향:
 *
 * 1. 유저 FPU 상태가 레지스터에 live:
 *    → XSAVE로 메모리에 저장
 *    → TIF_NEED_FPU_LOAD 설정
 *    → 커널 SIMD 코드 실행
 *
 * 2. 이미 TIF_NEED_FPU_LOAD 상태 (이전 컨텍스트 스위치 이후):
 *    → XSAVE 불필요 (이미 메모리에 있음)
 *    → 바로 커널 SIMD 코드 실행 가능
 *    → 이 경우 kernel_fpu_begin()의 비용이 현저히 줄어듦!
 *
 * 결론: 컨텍스트 스위치 직후의 kernel_fpu_begin()은 매우 저렴
 *       유저 코드가 FPU를 집중적으로 쓰는 태스크에서는 비용이 높음
 */

kernel_fpu_begin/end 오버헤드 측정

시나리오SSE만+ AVX (YMM)+ AVX-512 (ZMM)+ AMX (TILE)
XSAVE 크기 ~576 B ~832 B ~2,688 B ~10,880 B
kernel_fpu_begin (FPU live) ~0.15 μs ~0.25 μs ~0.6 μs ~2.5 μs
kernel_fpu_begin (lazy) ~0.02 μs ~0.02 μs ~0.02 μs ~0.02 μs
kernel_fpu_end (항상) ~0.01 μs ~0.01 μs ~0.01 μs ~0.01 μs
컨텍스트 스위치 복원 ~0.15 μs ~0.25 μs ~0.55 μs ~2.0 μs
총 왕복 비용 (worst) ~0.30 μs ~0.50 μs ~1.15 μs ~4.5 μs

측정 기준: 위 수치는 Skylake/Ice Lake 급 CPU에서의 대략적 참고값입니다. 실제 값은 CPU 모델, 캐시 상태, 메모리 대역폭(Bandwidth)에 따라 달라집니다. 핵심 판단 기준: kernel_fpu_begin/end의 오버헤드가 SIMD 가속으로 얻는 이득보다 작아야 합니다. 일반적으로 1KB 이상의 데이터를 처리할 때만 SIMD가 유리합니다. crypto API의 simd_skcipher 래퍼는 이 판단을 자동화합니다.

데이터 정렬 — #GP 예외와 진단

SIMD 정렬 명령어(movaps, vmovaps 등)는 데이터가 정해진 바이트 경계에 맞지 않으면 #GP(General Protection Fault)를 발생시킵니다. 커널에서 이를 잘못 처리하면 oops로 이어집니다.

SIMD 정렬 요구사항과 #GP 발생 조건 SSE — 16바이트 정렬 정렬 필수: movaps, movdqa 비정렬 가능: movups, movdqu 조건: addr & 0xF == 0 AVX — 32바이트 정렬 정렬 필수: vmovaps (YMM 256비트) 비정렬 가능: vmovups, vmovdqu 조건: addr & 0x1F == 0 AVX-512 — 64바이트 정렬 정렬 필수: vmovaps (ZMM 512비트) 비정렬 가능: vmovdqu32/64 조건: addr & 0x3F == 0 정렬 요구 명령어에 정렬되지 않은 주소를 넘기면 #GP(General Protection Fault) 발생 커널에서 처리하지 못하면 Oops로 이어지므로, 위반 명령어를 찾아 교체하거나 정렬을 보장해야 합니다 #GP 진단 절차 dmesg에서 general protection fault와 RIP 확인 objdump -d vmlinux로 RIP 명령어 역어셈블 movaps·vmovaps 등 정렬 요구 명령어 특정 movups·vmovdqu로 교체 — 즉시 해결 정렬 보장 방법 DECLARE_ALIGNED(N, u8, buf[count]) __attribute__((aligned(N))) PTR_ALIGN(ptr, N) · ROUND_UP(size, N) pahole -C struct vmlinux로 레이아웃 검증 권장: movups·vmovdqu(비정렬 변종)를 기본으로 사용하고, 정렬은 벤치마크로 효과를 확인한 경로에만 적용합니다 예외: Non-Temporal 스토어(movntps·vmovntps)는 항상 정렬이 필요합니다

정렬 요구사항 상세 테이블

ISA정렬 명령어비정렬 명령어정렬 바이트#GP 조건
SSEmovaps, movdqamovups, movdqu16Baddr % 16 ≠ 0
AVX / AVX2vmovaps (256-bit)vmovups, vmovdqu32Baddr % 32 ≠ 0
AVX-512vmovaps (512-bit)vmovdqu32/6464Baddr % 64 ≠ 0
Non-Temporalmovntps, vmovntps없음 (항상 정렬 필수)16/32/64B정렬 위반 시 #GP

커널 정렬 보장 패턴

/* 커널에서 정렬을 보장하는 주요 방법 */
#include <linux/align.h>
#include <linux/slab.h>

/* 1. DECLARE_ALIGNED: 정적 배열 정렬 */
DECLARE_ALIGNED(32, u8, simd_buf[256]); /* 32B 정렬 */
DECLARE_ALIGNED(64, u8, avx512_buf[512]); /* 64B 정렬 */

/* 2. __attribute__((aligned(N))): GCC 정렬 지시 */
struct my_simd_ctx {
    u8 key[32] __attribute__((aligned(32)));
    u8 state[64] __attribute__((aligned(64)));
};

/* 3. PTR_ALIGN: 런타임 포인터 정렬 조정 */
void *buf = kmalloc(size + 63, GFP_KERNEL);
void *aligned = PTR_ALIGN(buf, 64); /* 64B 경계로 올림 */

/* 4. IS_ALIGNED: 정렬 여부 런타임 검사 */
if (!IS_ALIGNED((unsigned long)ptr, 32)) {
    /* 비정렬 → vmovdqu 변종 사용 경로 */
    do_simd_unaligned(ptr, len);
} else {
    /* 정렬 확인 → vmovaps 사용 경로 (최적) */
    do_simd_aligned(ptr, len);
}

/* 5. ROUND_UP: 정렬된 크기로 올림 */
size_t aligned_size = ROUND_UP(size, 64);

#GP 예외 진단 및 수정

# 1. dmesg에서 #GP 확인
dmesg | grep "general protection"
# 출력 예: general protection fault#GP 0000 [#1] SMP NOPTI
#          RIP: 0010:my_aes_encrypt+0x2a/0x60

# 2. RIP 주소에서 위반 명령어 특정
objdump -d vmlinux | grep -A 3 "<my_aes_encrypt>"
# 출력 예:
#   movaps (%rsi), %xmm0     ← 16B 정렬 필요인데 위반!

# 3. 즉시 수정: 비정렬 변종으로 교체
# movaps  → movups  (SSE)
# vmovaps → vmovups  (AVX/AVX-512)
# vmovdqa → vmovdqu  (AVX 정수)

# 4. 근본 수정: 구조체/버퍼(Buffer) 정렬 보장
# DECLARE_ALIGNED(32, u8, buf[256]);

# 5. 정렬 상태 확인 도구
pahole -C my_struct vmlinux        # 구조체 레이아웃 출력

현대 CPU에서의 비정렬 패널티: Intel Nehalem(2008+)과 AMD Zen 이후부터는 movups(비정렬 변종)도 캐시 라인 경계를 넘지 않는 한 movaps와 동등한 성능을 냅니다. 따라서 커널에서는 안전한 비정렬 명령어를 기본으로 사용하고, 벤치마크로 정렬 효과를 확인한 후에만 정렬 명령어로 교체하는 것이 권장됩니다. Non-Temporal 스토어는 예외로 항상 정렬이 필수입니다.

커널 내 SIMD 활용 사례

용도x86 구현ARM64 구현커널 코드
AES 암호화 AES-NI (AESENC/AESDEC) ARMv8 Crypto Extension arch/x86/crypto/aesni-intel_glue.c
SHA 해싱 SHA-NI (Goldmont+) CE SHA instructions arch/x86/crypto/sha256_ssse3_glue.c
CRC32 SSE4.2 CRC32 명령, PCLMULQDQ CRC32 명령 arch/x86/crypto/crc32c-intel_glue.c
ChaCha20 AVX2/AVX-512 (4/8블록 병렬) NEON (4블록 병렬) arch/x86/crypto/chacha_glue.c
Poly1305 AVX2 (radix 2^26 표현) NEON arch/x86/crypto/poly1305_glue.c
RAID5/6 XOR AVX2/AVX-512 병렬 XOR NEON XOR lib/raid6/sse*.c, lib/raid6/neon*.c
memcpy/memset REP MOVSB (ERMS) / AVX NEON/SVE 최적화 copy arch/x86/lib/memcpy_64.S
체크섬 (IP/TCP) ADC 체인 + SIMD NEON 체크섬 arch/x86/lib/csum-partial_64.c
SM3/SM4 (중국 표준) AVX2 + AES-NI 활용 CE SM3/SM4 명령 arch/x86/crypto/sm4_aesni_avx2_glue.c
Zstd/LZ4 압축 SIMD 직접 사용 안 함 (스칼라) 스칼라 lib/zstd/ (유저 라이브러리 포팅)

crypto API와 SIMD 통합 구조

/* 커널 crypto API는 SIMD 가속을 투명하게 관리하는 구조를 갖추고 있음 */
/* arch/x86/crypto/aesni-intel_glue.c — 실제 등록 예시 */

static struct skcipher_alg aesni_skciphers[] = {
    {
        .base.cra_name        = "cbc(aes)",
        .base.cra_driver_name = "cbc-aes-aesni",  /* AES-NI 가속 */
        .base.cra_priority    = 400,             /* generic(100)보다 높은 우선순위(Priority) */
        .setkey               = aesni_skcipher_setkey,
        .encrypt              = cbc_encrypt,      /* SIMD 사용 */
        .decrypt              = cbc_decrypt,
    },
};

/* 우선순위 기반 자동 선택:
 *   cbc(aes) 요청 시:
 *     1. cbc-aes-aesni   (priority=400, AES-NI 필요)
 *     2. cbc-aes-generic (priority=100, 항상 사용 가능)
 *   → CPU가 AES-NI를 지원하면 자동으로 #1 선택 */

/* simd_skcipher_create_compat(): softirq에서의 SIMD 안전 처리 */
/*
 * 문제: softirq 컨텍스트에서 kernel_fpu_begin()이 실패할 수 있음
 *       (이미 process context에서 FPU를 사용 중일 때)
 *
 * 해결: simd 래퍼가 softirq를 감지하면 자동으로 generic fallback
 *       → cryptd (crypto daemon) 워크큐로 지연 처리
 */
struct simd_skcipher_alg *simd_skcipher_create_compat(
    const char *algname,      /* "cbc(aes)" */
    const char *drvname,      /* "cbc-aes-aesni" (내부 SIMD 알고리즘) */
    const char *basename);    /* "cbc(aes)" (fallback) */

/* 실제 호출 경로:
 *   유저 요청 → crypto_alloc_skcipher("cbc(aes)")
 *   → 커널이 "cbc-aes-aesni"의 simd 래퍼 선택
 *   → process context → kernel_fpu_begin() + AES-NI 직접 사용
 *   → softirq context → cryptd 워크큐 → process context에서 실행
 */

RAID6 SIMD 최적화

/* RAID6는 GF(2^8) 갈루아 필드 연산을 대량으로 수행 → SIMD 핵심 활용처 */
/* lib/raid6/algos.c — 런타임 벤치마크로 최적 알고리즘 자동 선택 */

const struct raid6_calls *const raid6_algos[] = {
    /* 속도 순 (느림 → 빠름) */
    &raid6_intx1,     /* 순수 정수 (fallback) */
    &raid6_intx2,     /* 정수, 2-way unroll */
    &raid6_sse1x1,    /* SSE 128-bit */
    &raid6_sse1x2,    /* SSE 128-bit, 2-way unroll */
    &raid6_sse2x1,    /* SSE2 128-bit */
    &raid6_sse2x2,    /* SSE2 128-bit, 2-way unroll */
    &raid6_sse2x4,    /* SSE2 128-bit, 4-way unroll */
    &raid6_avx2x1,    /* AVX2 256-bit */
    &raid6_avx2x2,    /* AVX2 256-bit, 2-way unroll */
    &raid6_avx2x4,    /* AVX2 256-bit, 4-way unroll */
    &raid6_avx512x1,  /* AVX-512 512-bit */
    &raid6_avx512x2,  /* AVX-512 512-bit, 2-way unroll */
    &raid6_avx512x4,  /* AVX-512 512-bit, 4-way unroll */
    NULL,
};

/* 부팅 시 각 알고리즘을 벤치마크하여 최적 선택 */
/* dmesg 출력 예시:
 * raid6: avx512x4  gen() 29430 MB/s
 * raid6: avx512x2  gen() 27891 MB/s
 * raid6: avx2x4    gen() 18560 MB/s
 * raid6: using algorithm avx512x4 gen() 29430 MB/s
 */

/* AVX2 RAID6 P+Q 신드롬 계산 핵심 루프 */
/* lib/raid6/avx2.c */
static void raid6_avx21_gen_syndrome(int disks, size_t bytes, void **ptrs)
{
    kernel_fpu_begin();

    asm volatile(
        "vmovdqa %[x0f], %%ymm3\n"   /* 마스크 0x0F */
        "vpxor   %%ymm0, %%ymm0, %%ymm0\n" /* P = 0 */
        "vpxor   %%ymm1, %%ymm1, %%ymm1\n" /* Q = 0 */
        /* 각 디스크 데이터를 순회하며:
         *   P ^= data[i]          (단순 XOR)
         *   Q = GF_MUL(Q) ^ data[i]  (GF(2^8) 곱셈 후 XOR)
         * GF 곱셈은 VPSHUFB로 4-bit 룩업테이블 사용 */
        ::: "ymm0","ymm1","ymm2","ymm3","ymm4","ymm5","memory"
    );

    kernel_fpu_end();
}

커널 memcpy/memset SIMD 최적화

x86 커널의 arch/x86/lib/memcpy_64.S에는 대형 SIMD 복사 루프가 없습니다. 진입부의 ALTERNATIVE 패치가 FSRM 지원 여부를 한 번에 가르고, 지원하지 않는 CPU는 memcpy_orig로 내려가 복사 길이에 따라 방향을 먼저 고른 뒤 32바이트 단위 언롤 루프를 돌고, 남은 길이를 4단계 테일 경로로 처리합니다. 아래 다이어그램과 코드 인용은 커널 v6.6의 arch/x86/lib/memcpy_64.S 실제 소스와 일치합니다.

__memcpy(dst, src, len) X86_FEATURE_FSRM? ALTERNATIVE 패치 지점 rep movsb 한 줄 movq %rdi, %rax movq %rdx, %rcx rep movsb → RET 예 아니오 memcpy_orig cmpq $0x20, %rdx jb .Lhandle_tail 32 B 미만 32 B 이상 .Lhandle_tail 길이 32 B 미만 처리 4개 크기 클래스로 분기 32 B 언롤 루프 movq ×4 = 32 B / 회 cmp %dil, %sil → 방향 선택 잔여분 16 ~ 31 B 앞 2 × movq 뒤 2 × movq 앞·뒤로 32 B 커버 8 ~ 15 B 앞 1 × movq 뒤 1 × movq .Lless_16bytes 4 ~ 7 B 앞 1 × movl 뒤 1 × movl .Lless_8bytes 1 ~ 3 B movzbl 첫 바이트 movzbq / movb .Lless_3bytes
arch/x86/lib/memcpy_64.S의 복사 크기별 전략 — 진입부의 ALTERNATIVE가 X86_FEATURE_FSRM을 보고, 지원 CPU에서는 세 명령(movq %rdi, %rax·movq %rdx, %rcx·rep movsb)만 실행한 뒤 RET로 끝냅니다. 미지원 CPU는 memcpy_orig로 가서 cmpq $0x20, %rdx로 갈림길을 만듭니다. 32바이트 이상이면 언롤 루프가 한 번에 4개씩 movq로 옮기고, 루프를 마친 잔여분은 다시 .Lhandle_tail로 모여 크기 4구간으로 나뉩니다. v6.6 소스 기준의 제어 흐름도입니다.

__memcpy 진입부와 memcpy_orig

파일은 크게 두 심볼로 이루어집니다. 컴파일러가 호출하는 것은 __memcpy뿐이고 memcpy는 별칭(SYM_FUNC_ALIAS_MEMFUNC)이므로 실제 코드는 한 벌입니다. 진입부는 부트 타임에 패치되는 ALTERNATIVE 한 줄이 전부입니다.

/* arch/x86/lib/memcpy_64.S — 커널 v6.6, 인용부 */
SYM_TYPED_FUNC_START(__memcpy)
	ALTERNATIVE "jmp memcpy_orig", "", X86_FEATURE_FSRM

	movq %rdi, %rax
	movq %rdx, %rcx
	rep movsb
	RET
SYM_FUNC_END(__memcpy)
EXPORT_SYMBOL(__memcpy)

SYM_FUNC_ALIAS_MEMFUNC(memcpy, __memcpy)
EXPORT_SYMBOL(memcpy)

ALTERNATIVE의 첫 인자는 패치 전에 실행되는 코드, 두 번째 인자는 해당 CPU 기능을 지원할 때 덧붙는 코드, 세 번째는 판단 기준이 되는 기능 플래그입니다. 여기서는 두 번째 인자가 빈 문자열이므로 FSRM을 지원하면 앞의 jmp memcpy_orig가 사라지고 바로 rep movsb로 흘러갑니다. 즉 memcpy_orig는 FSRM을 지원하지 않는 CPU 전용 경로입니다.

X86_FEATURE_FSRM은 짧은 길이의 REP MOVSB도 빠르다는 CPUID 비트에 대응합니다. 이 비트가 서 있는 CPU에서는 크기별 분기 자체가 이득이 없으므로 코드 전체를 우회합니다.

파일은 .section .noinstr.text, "ax"로 시작해 계측(instrumentation) 대상에서 제외됩니다. 계측이 켜져서는 안 되는 경로에서도 호출되기 때문입니다.

	.section .noinstr.text, "ax"

FSRM 경로에는 크기 분기가 없으므로 코드가 세 명령으로 끝납니다. 반면 memcpy_orig는 8바이트 레지스터 4개(%r8~%r11)를 써서 32바이트를 한 번에 옮깁니다.

SYM_FUNC_START_LOCAL(memcpy_orig)
	movq %rdi, %rax

	cmpq $0x20, %rdx
	jb .Lhandle_tail

	/* 메모리 false dependence 가능성을 보고 복사 모드를 고른다 */
	cmp  %dil, %sil
	jl .Lcopy_backward
	subq $0x20, %rdx
.Lcopy_forward_loop:
	subq $0x20,	%rdx

	/* Move in blocks of 4x8 bytes */
	movq 0*8(%rsi),	%r8
	movq 1*8(%rsi),	%r9
	movq 2*8(%rsi),	%r10
	movq 3*8(%rsi),	%r11
	leaq 4*8(%rsi),	%rsi

	movq %r8,	0*8(%rdi)
	movq %r9,	1*8(%rdi)
	movq %r10,	2*8(%rdi)
	movq %r11,	3*8(%rdi)
	leaq 4*8(%rdi),	%rdi
	jae  .Lcopy_forward_loop
	addl $0x20,	%edx
	jmp  .Lhandle_tail

읽은 값을 곧바로 저장하지 않고 %r8~%r11에 먼저 담는 이유는 movq 4개의 로드를 먼저 발행하고 스토어 4개를 몰아서 뒤에 붙이면 로드·스토어 유닛이 동시에 채워지기 때문입니다. 소스 주석에도 "4x8 바이트 블록으로 옮긴다"는 설명만 달려 있습니다.

cmp %dil, %sil은 두 포인터의 최하위 바이트만 비교합니다. 전체 주소를 비교하지 않는 이유는 소스 주석대로 "메모리 false dependence가 생길 수 있는지"만 보기 위한 검사라서, 값싼 비교로 앞·뒤 중 한 방향을 정하면 충분하기 때문입니다. 오버랩이 의심되면 .Lcopy_backward로 가서 같은 32바이트 폭을 역방향으로 처리하고, 루프가 끝나면 addl $0x20, %edx로 길이를 되돌린 뒤 다시 .Lhandle_tail로 넘어갑니다.

ERMS와 FSRM, 그리고 v6.4에서 사라진 경로

같은 파일의 다른 커널 버전을 나란히 보면 경로가 하나 줄어든 것을 알 수 있습니다.

버전진입부 분기비고
v6.3 이하ALTERNATIVE_2 "jmp memcpy_orig", "", X86_FEATURE_REP_GOOD, "jmp memcpy_erms", X86_FEATURE_ERMSmemcpy_erms 레이블이 별도로 존재하며 본문은 rep movsb 한 줄
v6.4 ~ v6.6ALTERNATIVE "jmp memcpy_orig", "", X86_FEATURE_FSRMmemcpy_erms 삭제, 본문도 rep movsq·rep movsb 조합에서 rep movsb 한 줄로 정리

FSRM(Fast Short REP MOV)은 짧은 길이에서도 REP MOVSB가 빠르다는 CPUID 비트입니다. 이전 버전은 ERMS와 별도 언롤 경로를 함께 두고 두 비트로 갈랐지만, v6.4에서 memcpy_erms 레이블이 제거되면서 지금은 FSRM 단일 분기만 남았습니다.

/* v6.3 이하: 분기가 두 개였다 */
	ALTERNATIVE_2 "jmp memcpy_orig", "", X86_FEATURE_REP_GOOD, \
		      "jmp memcpy_erms", X86_FEATURE_ERMS

	movq %rdi, %rax
	movq %rdx, %rcx
	shrq $3, %rcx
	andl $7, %edx
	rep movsq
	movl %edx, %ecx
	rep movsb
	RET

여기서 shrq $3·andl $7은 길이를 8의 배수와 나머지로 나누는 계산입니다. 64비트 단위를 먼저 rep movsq로 처리하고 남은 0~7바이트를 rep movsb로 붙이는 구성이라, rep movsq가 이득이 되는 긴 복사를 염두에 둔 형태였습니다.

.Lhandle_tail의 네 구간과 Non-Temporal 부재

루프를 마치거나 처음부터 32바이트 미만이면 .Lhandle_tail로 모입니다. 여기서부터는 남은 길이를 네 구간으로 나눠 각 구간에 맞는 폭의 명령을 씁니다.

	.p2align 4
.Lhandle_tail:
	cmpl $16,	%edx
	jb   .Lless_16bytes

	/* 16 ~ 31 바이트: 앞 2개 + 뒤 2개로 32바이트를 덮는다 */
	movq 0*8(%rsi), %r8
	movq 1*8(%rsi),	%r9
	movq -2*8(%rsi, %rdx),	%r10
	movq -1*8(%rsi, %rdx),	%r11
	movq %r8,	0*8(%rdi)
	movq %r9,	1*8(%rdi)
	movq %r10,	-2*8(%rdi, %rdx)
	movq %r11,	-1*8(%rdi, %rdx)
	RET
	.p2align 4
.Lless_16bytes:
	cmpl $8,	%edx
	jb   .Lless_8bytes
	/* 8 ~ 15 바이트: 앞 1개 + 뒤 1개 */
	movq 0*8(%rsi),	%r8
	movq -1*8(%rsi, %rdx),	%r9
	movq %r8,	0*8(%rdi)
	movq %r9,	-1*8(%rdi, %rdx)
	RET

주목할 점은 %rdx를 인덱스로 쓰는 -2*8(%rsi, %rdx) 형태입니다. 길이가 코드에 박혀 있지 않으므로 어느 구간에서든 앞에서 1~2개, 뒤에서 1~2개만 찍으면 가운데는 자동으로 덮입니다. 크기별 코드가 .Lhandle_tail 아래 한 곳에 모여 있다는 점도 이 구조의 특징입니다.

레이블담당 길이(%rdx)실제 명령
.Lhandle_tail 본문cmpl $16 비교 후 진입movq 앞 2개 + movq 뒤 2개
.Lless_16bytes8 이상 16 미만movq 앞 1개 + movq 뒤 1개
.Lless_8bytes4 이상 8 미만movl 앞 1개 + movl 뒤 1개
.Lless_3bytes4 미만(1 ~ 3)movzbl 첫 바이트 + movzbq·movb로 나머지

여기서 다룬 경로에는 Non-Temporal 스토어(movntq·movntdq)나 sfence가 등장하지 않습니다. 캐시를 우회하는 대형 복사는 별도 경로가 담당하며, 해당 함수인 __memcpy_flushcache()는 arch/x86/include/asm/string_64.h에서 선언됩니다.

/* arch/x86/include/asm/string_64.h — v6.6 */
#define __HAVE_ARCH_MEMCPY_FLUSHCACHE 1
void __memcpy_flushcache(void *dst, const void *src, size_t cnt);

정리하면 arch/x86/lib/memcpy_64.S가 다루는 것은 REP MOVSB와 32바이트 언롤 두 가지이며, 크기 판정은 진입부가 아니라 memcpy_orig 안쪽에서 한 번만 일어납니다.

AVX-512 커널 사용 시 특별 주의

AVX-512의 양면성:
  • 주파수 다운클럭 — AVX-512 명령 사용 시 CPU가 자동으로 터보 주파수를 낮춤 (Intel "AVX offset"). 512비트 실행 유닛 전력 소비 때문
  • 컨텍스트 스위치 비용 — ZMM0-ZMM31 + 마스크 레지스터를 저장/복원해야 하므로 XSAVE 크기 증가 (SSE: 512B, AVX: 1KB, AVX-512: 2.5KB+)
  • 커널 정책 — 리눅스 커널에서는 AVX-512를 crypto와 RAID에만 제한적으로 사용. 일반 커널 코드에서 AVX-512 사용은 권장되지 않음
  • clearcpuid 부트 옵션 — clearcpuid=avx512f로 AVX-512 비활성화 가능. 서버 환경에서 주파수 안정성을 위해 사용되기도 함
  • 하이브리드 CPU 주의 — Alder Lake 이후 일부 하이브리드 플랫폼은 P-core/E-core 간 AVX-512 지원이 비대칭입니다. 실제 사용 가능 여부는 CPU 모델, BIOS/마이크로코드, 커널 설정 조합에 따라 달라집니다.

SIMD 대안: 컴파일러 자동 벡터화

/* 커널 코드에서 직접 SIMD 인라인 어셈블리 대신 */
/* 컴파일러 자동 벡터화를 활용하는 방법 */

/* GCC/Clang __attribute__((target)) 으로 특정 ISA 확장 활성화 */
__attribute__((target("avx2")))
void xor_block_avx2(u8 *dst, const u8 *src, size_t len)
{
    /* 컴파일러가 루프를 자동으로 AVX2 벡터화 */
    for (size_t i = 0; i < len; i++)
        dst[i] ^= src[i];
}

/* 주의: 커널 빌드 시 기본적으로 -mno-sse, -mno-mmx 플래그 적용 */
/* SIMD를 사용하는 .c 파일은 Makefile에서 명시적으로 활성화: */
/* CFLAGS_myfile.o += -msse2 -mavx2 */
x86-64 마이크로아키텍처 레벨보장 ISA대표 CPU
x86-64-v1 (기본)SSE2, CMOV, CMPXCHG8B모든 x86-64 CPU
x86-64-v2SSE4.2, POPCNT, SSSE3, CMPXCHG16BNehalem / K10 이상
x86-64-v3AVX2, FMA, BMI1/BMI2, MOVBEHaswell / Excavator 이상
x86-64-v4AVX-512F/BW/CD/DQ/VLSkylake-X / Icelake 이상
# GCC 벡터화 보고서: -fopt-info-vec
gcc -O2 -march=native -fopt-info-vec-optimized myfile.c -o myfile
# 출력 예시:
# myfile.c:42:5: optimized: loop vectorized using 32-byte vectors
# myfile.c:58:5: optimized: basic block part vectorized using 32-byte vectors
# myfile.c:71:5: missed: couldn't vectorize loop (data dependency)

# 벡터화 실패 원인 상세 확인
gcc -O2 -march=native -fopt-info-vec-missed myfile.c -o myfile
# 실패 이유: aliasing, 정렬 불확실, 복잡한 제어 흐름 등

# __builtin_assume_aligned으로 힌트 제공
# void process(float *p, size_t n) {
#     p = __builtin_assume_aligned(p, 32);  // 32B 정렬 보장
#     for (size_t i = 0; i < n; i++) p[i] *= 2.0f;  // 자동 벡터화
# }

SIMD 코드 디버깅과 성능 분석

# SIMD 레지스터 확인 (GDB)
(gdb) info vector                   # SSE/AVX 레지스터 전체 출력
(gdb) p $xmm0.v4_float              # XMM0을 4개 float로 해석
(gdb) p $xmm0.v16_int8              # XMM0을 16개 int8로 해석
(gdb) p $ymm0.v8_int32              # YMM0을 8개 int32로 해석
(gdb) p $zmm0.v64_int8              # ZMM0을 64개 int8로 해석

# perf로 SIMD 명령어 사용 분석
perf stat -e fp_arith_inst_retired.128b_packed_single \
          -e fp_arith_inst_retired.256b_packed_single \
          -e fp_arith_inst_retired.512b_packed_single \
          -- ./my_program
# 128/256/512비트 packed FP 명령어 사용 횟수 측정

# AVX-512 다운클럭 모니터링
perf stat -e core_power.lvl0_turbo_license \
          -e core_power.lvl1_turbo_license \
          -e core_power.lvl2_turbo_license \
          -- ./my_program
# Level 0=기본, Level 1=AVX2 offset, Level 2=AVX-512 offset

# 커널 RAID6 벤치마크 결과 확인
dmesg | grep raid6
# raid6: avx512x4  gen() 29430 MB/s
# raid6: using algorithm avx512x4 gen() 29430 MB/s

# 현재 CPU의 SIMD 지원 확인
grep -o 'sse\|sse2\|ssse3\|sse4_1\|sse4_2\|avx\|avx2\|avx512' /proc/cpuinfo | sort -u

# XSAVE 상태 크기 확인
cat /proc/cpuinfo | grep -o 'xsave[^ ]*'

# Intel PMU SIMD 전용 이벤트 (Skylake 기준)
perf stat -e avx_insts.all \
          -e fp_arith_inst_retired.256b_packed_single \
          -e fp_arith_inst_retired.256b_packed_double \
          -e fp_arith_inst_retired.512b_packed_single \
          -- ./my_program
# avx_insts.all: AVX/AVX2/AVX-512 명령어 총 실행 횟수

# SSE-AVX 전환 페널티 측정
perf stat -e assists.sse_avx_mix -- ./my_program
# 0이 아닌 값 → VEX/EVEX 인코딩 혼용 → vzeroupper 누락 의심
명령어지연 (clk)처리량 (/clk)비고 (Skylake)
VPADDD ymm10.33정수 덧셈, 3개 포트 분산
VMULPS ymm40.5float 곱셈, 2 포트
VDIVPS ymm11~145~14나눗셈, 매우 느림
VGATHERDPS ymm~24~24Gather, 캐시 미스 시 더 느림
VAESENC ymm41AES 라운드, 높은 처리량
VPSHUFB ymm10.5바이트 셔플, SSSE3

SIMD 명령어는 objdump으로 확인할 수 있습니다:

objdump -d vmlinux | grep -E 'vmov|vpxor|vaes|vpshufb|vpadd'

커널 SIMD 코드의 일반적인 실수와 디버깅:

  1. kernel_fpu_begin/end 누락
    • 증상: 유저 프로세스의 부동소수점 결과가 무작위로 틀립니다.
    • 진단: KASAN/UBSAN으로는 감지할 수 없습니다. CONFIG_X86_DEBUG_FPU를 활성화해야 합니다.
  2. clobber 리스트 누락
    • 증상: 최적화 수준에 따라 간헐적 오류가 발생합니다.
    • 진단: -O0에서 정상이고 -O2에서 비정상이면 clobber를 확인합니다.
  3. 정렬 문제
    • 증상: movaps에서 #GP (General Protection) 예외가 발생합니다.
    • 해결: movdqu/vmovdqu (비정렬 변종) 사용 또는 __attribute__((aligned(32)))으로 정렬을 보장합니다.
  4. SSE/AVX 전환 페널티
    • 증상: 예상보다 성능이 낮습니다.
    • 진단: perf stat -e assists.sse_avx_mix로 확인합니다.
    • 해결: VZEROUPPER로 상위 YMM을 클리어하거나 VEX 인코딩을 통일합니다.

VZEROUPPER와 SSE/AVX 전환

/* AVX→SSE 전환 시 발생하는 성능 페널티 방지 */
/*
 * 문제: AVX 코드(ymm 사용)에서 SSE 코드(xmm만 사용)로 전환 시,
 *       CPU가 상위 128비트 상태를 추적하느라 성능 저하
 *
 * Intel Sandy Bridge~Haswell: 큰 전환 페널티
 * Intel Skylake+: "dirty upper state" 추적 비용 (더 작지만 존재)
 * AMD Zen: 전환 페널티 없음 (다른 물리 레지스터 파일)
 */

/* AVX 코드 종료 시 반드시 실행 */
vzeroupper                       /* 모든 YMM/ZMM의 상위 비트를 0으로 */

/* 커널에서의 적용: kernel_fpu_end() 전에 호출 */
/* 또는 AVX 함수 끝에 __attribute__((target("avx"))) + vzeroupper */

/* 커널 crypto 코드에서의 전형적 패턴 */
asm volatile(
    /* ... AVX2 연산 ... */
    "vzeroupper"                 /* SSE 코드로 돌아가기 전 필수 */
    ::: "ymm0", "ymm1", ..., "memory"
);

커널 빌드와 SIMD: 커널은 -mno-sse -mno-mmx -mno-sse2 -mno-3dnow -mno-avx 플래그로 빌드됩니다. 이는 일반 C 코드에서 컴파일러가 SIMD를 자동 생성하는 것을 방지합니다. SIMD를 사용하는 특정 파일만 Makefile에서 CFLAGS_<file>.o += -msse2로 개별 활성화합니다. 어셈블리 파일(.S)에서는 이 제한이 없으므로 직접 SIMD 명령어를 사용할 수 있습니다.

하이브리드 CPU AVX-512 비대칭 처리

Intel Alder Lake(12세대) 이후 하이브리드 CPU는 P-core(고성능)와 E-core(효율) 간 SIMD 지원 능력이 비대칭적입니다. 커널은 이 차이를 투명하게 처리하기 위한 특별한 로직을 포함합니다.

플랫폼P-core 마이크로아키E-core 마이크로아키P-core AVX-512E-core AVX-512
Alder Lake (2021)Golden CoveGracemont하드웨어 지원미지원
Raptor Lake (2022)Raptor CoveGracemont하드웨어 지원미지원
Meteor Lake (2023)Redwood CoveCrestmont미지원미지원
Arrow Lake (2024)Lion CoveSkymont미지원미지원
Lunar Lake (2024)Lion CoveSkymont미지원미지원
Panther Lake (2026)Cougar CoveDarkmontAVX10.1-512 (P코어)AVX10.1-256 (E코어)
Nova Lake (2027 예정)(미정)(미정)AVX10.2-512AVX10.2-512
/* 커널의 하이브리드 CPU AVX-512 처리 전략
 * arch/x86/kernel/cpu/intel.c */

/* Alder Lake/Raptor Lake에서 BIOS가 AVX-512를 CPUID에 노출할 수 있음.
 * 하지만 E-core가 없으면 P-core에서만 실행할 때 문제가 없습니다.
 * E-core 존재 시: 태스크가 E-core로 이주되면 AVX-512 명령 → #UD 예외!
 *
 * 커널 해결책: 하이브리드 CPU에서 E-core AVX-512 미지원 감지 시
 * CPUID에서 AVX-512 비트를 강제 클리어하여 사용 불가 처리 */

static void intel_clear_avx512_on_hybrid(struct cpuinfo_x86 *c)
{
    if (cpu_feature_enabled(X86_FEATURE_HYBRID_CPU) &&
        !cpu_feature_enabled(X86_FEATURE_AVX512F_ECORE)) {
        /* E-core가 AVX-512를 지원하지 않으면 시스템 전체에서 비활성화 */
        setup_clear_cpu_cap(X86_FEATURE_AVX512F);
        pr_info("Disabling AVX-512 on hybrid CPU (E-core lacks support)\n");
    }
}

/* 런타임 AVX-512 사용 가능 여부 확인 패턴 */
void my_simd_function(u8 *data, size_t len)
{
    if (cpu_feature_enabled(X86_FEATURE_AVX512F)) {
        /* AVX-512 경로: 커널이 시스템 전체에서 안전 보장 */
        kernel_fpu_begin();
        process_avx512(data, len);
        kernel_fpu_end();
    } else if (cpu_feature_enabled(X86_FEATURE_AVX2)) {
        /* AVX2 fallback */
        kernel_fpu_begin();
        process_avx2(data, len);
        kernel_fpu_end();
    } else {
        /* 스칼라 fallback (항상 동작) */
        process_scalar(data, len);
    }
}

/* clearcpuid 부트 파라미터로 수동 비활성화:
 *   clearcpuid=avx512f     → AVX-512F 비활성화
 *   clearcpuid=avx512f,avx → AVX-512와 AVX 모두 비활성화
 *
 * /proc/cpuinfo에서 확인:
 *   grep avx512 /proc/cpuinfo  → 항목 없으면 비활성화됨
 */
하이브리드 CPU 주의사항:
  • BIOS 설정 영향 — 일부 Alder Lake/Raptor Lake BIOS는 AVX-512를 CPUID에 노출할지 말지를 선택할 수 있습니다. 같은 하드웨어라도 BIOS 설정에 따라 /proc/cpuinfo의 avx512f 플래그 유무가 달라집니다.
  • 커널 자동 처리 — Linux 5.16+에서 E-core AVX-512 미지원 감지 시 자동으로 AVX-512를 시스템 전체에서 비활성화합니다. 개별 드라이버/모듈은 cpu_feature_enabled()만 확인하면 됩니다.
  • 마이크로코드 업데이트 — 마이크로코드 버전에 따라 AVX-512 지원 여부가 변경될 수 있습니다. 서버 운영 환경에서는 마이크로코드 갱신 시 SIMD 기능 재확인이 필요합니다.

SIMD 성능 엔지니어링

SIMD 명령어를 사용한다고 자동으로 성능이 향상되지 않습니다. 레지스터 압력, 명령어 수준 병렬성(ILP), 실행 포트 병목(Bottleneck), 메모리 대역폭 한계를 이해해야 최적의 SIMD 코드를 작성할 수 있습니다.

레지스터 압력과 스택 스필

/* 레지스터 압력(Register Pressure) — SIMD 성능의 핵심 제약 */
/*
 * 사용 가능한 벡터 레지스터 수:
 *
 *   ISA              벡터 레지스터    마스크 레지스터
 *   ─────────────────────────────────────────────────
 *   SSE/AVX          16개 (xmm/ymm)   없음
 *   AVX-512          32개 (zmm)        8개 (k0-k7)
 *   ARM NEON         32개 (v0-v31)     없음
 *   ARM SVE          32개 (z0-z31)     16개 (p0-p15)
 *   RISC-V RVV       32개 (v0-v31)     v0 (1개)
 *
 * LMUL/레지스터 그룹핑 시 사용 가능 수 감소:
 *   RVV LMUL=4 → 8개 논리 레지스터
 *   RVV LMUL=8 → 4개 논리 레지스터 (매우 제한적)
 */

/* 레지스터가 부족하면 스택 스필(Stack Spill) 발생 */
/*
 * 나쁜 예: 너무 많은 라이브 변수 (SSE/AVX, 16개 레지스터)
 *
 * void bad_example(float *a, float *b, float *c, ...) {
 *     __m256 v0 = _mm256_load_ps(a);
 *     __m256 v1 = _mm256_load_ps(b);
 *     __m256 v2 = _mm256_load_ps(c);
 *     ...
 *     __m256 v17 = _mm256_load_ps(r);  // 17번째 → 스택에 스필!
 *     // vmovaps %ymm0, -0x120(%rsp)  ← 레지스터 → 스택 저장
 *     // ... 나중에 ...
 *     // vmovaps -0x120(%rsp), %ymm0  ← 스택 → 레지스터 복원
 * }
 *
 * 스필 비용: ymm 스필 = L1 캐시 접근 (~4 사이클)
 *           zmm 스필 = L1 캐시 접근 (~5-6 사이클, 64바이트)
 *
 * 좋은 예: 루프 내에서 8-12개 레지스터만 라이브
 */

/* 커널 인라인 asm에서의 clobber 목록 관리 */
asm volatile(
    /* 8개 레지스터만 사용 → 나머지 8개는 컴파일러가 활용 가능 */
    "vmovdqu (%[src]), %%ymm0\n"
    "vmovdqu 32(%[src]), %%ymm1\n"
    "vmovdqu 64(%[src]), %%ymm2\n"
    "vmovdqu 96(%[src]), %%ymm3\n"
    "vpxor   %%ymm4, %%ymm0, %%ymm0\n"
    "vpxor   %%ymm5, %%ymm1, %%ymm1\n"
    "vpxor   %%ymm6, %%ymm2, %%ymm2\n"
    "vpxor   %%ymm7, %%ymm3, %%ymm3\n"
    "vzeroupper\n"
    :: [src] "r"(src)
    : "ymm0", "ymm1", "ymm2", "ymm3",
      "ymm4", "ymm5", "ymm6", "ymm7", "memory"
    /* clobber에 ymm0-ymm7을 명시 → 컴파일러가 이 레지스터를 다른 용도로 쓰지 않음 */
    /* clobber 목록이 너무 크면(16개 전부) 컴파일러가 주변 코드에서 스필 유발 */
);

명령어 수준 병렬성(ILP)과 파이프라이닝

/* ILP(Instruction-Level Parallelism) — SIMD 처리량 극대화 */

/* 데이터 의존성 체인 — 직렬 실행, ILP 없음 */
/*
 * vaddps %ymm0, %ymm1, %ymm1    ; 3 사이클 레이턴시
 * vaddps %ymm1, %ymm2, %ymm2    ; ← ymm1 대기 (3 사이클)
 * vaddps %ymm2, %ymm3, %ymm3    ; ← ymm2 대기 (3 사이클)
 * vaddps %ymm3, %ymm4, %ymm4    ; ← ymm3 대기 (3 사이클)
 * 총: 4 × 3 = 12 사이클 (의존성 체인 길이)
 */

/* 독립 연산으로 ILP 확보 — 병렬 실행 */
/*
 * vaddps %ymm0, %ymm1, %ymm1    ; 포트 0/1에서 실행
 * vaddps %ymm2, %ymm3, %ymm3    ; 동시에 다른 포트에서 실행 (독립)
 * vaddps %ymm4, %ymm5, %ymm5    ; 동시에 실행 가능 (독립)
 * vaddps %ymm6, %ymm7, %ymm7    ; 동시에 실행 가능 (독립)
 * 총: ~3-4 사이클 (4개가 병렬 실행)
 */

/* SIMD 명령어 레이턴시 vs 처리량 (Skylake/Ice Lake) */
/*
 * 명령어                레이턴시  처리량(CPI)  실행 포트
 * ──────────────────────────────────────────────────────
 * vaddps  (FP 덧셈)     4 clk    0.5 (p0/p1)  FMA 유닛
 * vmulps  (FP 곱셈)     4 clk    0.5 (p0/p1)  FMA 유닛
 * vfmadd* (FMA)         4 clk    0.5 (p0/p1)  FMA 유닛
 * vpaddd  (INT 덧셈)    1 clk    0.33 (p0/p1/p5) 다중 포트
 * vpshufb (바이트셔플)  1 clk    0.5 (p5)     셔플 유닛
 * vaesenc (AES 라운드)  4 clk    1.0 (p0)     AES 유닛
 * vdivps  (FP 나눗셈)  11 clk    5.0 (p0)     분할기
 * vsqrtps (제곱근)     12 clk    6.0 (p0)     분할기
 *
 * 핵심: 레이턴시가 높은 명령어(div, sqrt, AES)는
 *       독립된 연산을 인터리빙하여 파이프라인 채워야 함
 */

/* AES-NI 파이프라이닝 예시 — 커널 crypto 코드의 실제 패턴 */
/* arch/x86/crypto/aesni-intel_asm.S 스타일 */

/* 단일 블록 AES — 파이프라인 미활용 */
aesenc  %xmm0, %xmm1            ; 라운드 1 → 4 사이클 대기
aesenc  %xmm0, %xmm1            ; 라운드 2 → 4 사이클 대기
; ... 총 10 라운드 = 40 사이클/블록

/* 4블록 인터리빙 AES — 파이프라인 충만 */
aesenc  %xmm0, %xmm1            ; 블록1 라운드1 (포트0 사용)
aesenc  %xmm0, %xmm2            ; 블록2 라운드1 (다음 사이클, 독립)
aesenc  %xmm0, %xmm3            ; 블록3 라운드1
aesenc  %xmm0, %xmm4            ; 블록4 라운드1
aesenc  %xmm0, %xmm1            ; 블록1 라운드2 (xmm1 ready)
aesenc  %xmm0, %xmm2            ; 블록2 라운드2
; ... 4블록 × 10라운드 = 40 aesenc → ~10-12 사이클/블록
; 처리량: 단일 블록 대비 ~3.5x 향상!

메모리 대역폭과 SIMD

SIMD 코드의 병목 판별 — 연산 vs 메모리

산술 강도(Arithmetic Intensity) = 연산 수 / 메모리 접근 바이트

워크로드 유형산술강도병목SIMD 효과
memcpy0 ops/B메모리 대역폭낮음
XOR (RAID)1 ops/B메모리 대역폭중간
AES-CTR~8 ops/B연산(CPU)높음
GHASH (GCM)~4 ops/B연산높음
SHA-256~10 ops/B연산매우 높음
행렬 곱셈 (AMX)~16 ops/B연산매우 높음

메모리 바운드 (산술강도 ≤ 2): SIMD 레지스터 폭을 늘려도 성능 향상이 제한적입니다. NT 스토어, 프리페치, 캐시 최적화가 더 효과적이며, memcpy의 경우 REP MOVSB(FSRM)이 AVX보다 나을 수 있습니다.

연산 바운드 (산술강도 ≥ 4): SIMD 폭 증가가 직접적인 성능 향상으로 이어집니다. AES는 128-bit SSE → 256-bit AVX → 512-bit VAES로, SHA는 파이프라인 인터리빙으로 처리량을 극대화합니다.

커널 memcpy에서 SIMD가 유리한 구간:

크기방식설명
< 64B레지스터 이동 (mov)SIMD 오버헤드만 증가
64B ~ 2KBAVX2 vmovdqu 루프L1/L2 캐시 히트, SIMD 유리
2KB ~ 256KBREP MOVSB (FSRM)L2/L3 최적화된 마이크로코드
> 256KBNT 스토어 (vmovntdq)캐시 오염 방지, 대역폭 최대화

perf stat으로 확인할 수 있습니다:

perf stat -e L1-dcache-load-misses,LLC-load-misses \
  -- taskset -c 0 dd if=/dev/dm-0 of=/dev/null bs=4K count=10000

SIMD 최적화 결정 트리: ① 산술 강도 확인 → 2 이하면 메모리 최적화 우선. ② perf stat으로 IPC 확인 → 1.0 이하면 메모리 바운드, 2.0 이상이면 프론트엔드/백엔드 병목. ③ 레지스터 압력 확인 → objdump -d에서 mov.*%[xyz]mm.*,.*(%rsp) 패턴(스택 스필) 검색. ④ 실행 포트 병목 → perf stat -e uops_dispatched_port.port_*로 확인.

RISC-V 벡터 확장 (RVV)

RISC-V V 확장(RVV 1.0, 2021년 비준)은 ARM SVE와 유사한 벡터 길이 비종속(VLA) 프로그래밍 모델을 채택합니다. 하드웨어 구현에 따라 VLEN(최소 128비트, 최대 65536비트)이 달라지며, 동일 바이너리가 모든 구현에서 동작합니다. 커널에서의 RVV 사용은 ARM NEON/SVE와 동일한 원칙(FPU 상태 보존, preempt 비활성화)을 따릅니다.

RISC-V 벡터 레지스터 구조 및 LMUL 그룹핑 1 벡터 레지스터 파일 — v0-v31 (각 VLEN 비트, 총 32개) v0 마스크 레지스터 v1 v2 v3 v4 v5 v6 v7 ⋯ v8 – v31 (총 32개) VLEN(레지스터당 비트 수)은 128 ~ 65536 범위에서 구현 정의 — 벡터 길이 비종속(VLA) 모델 2 LMUL(Length MULtiplier) — 연속 레지스터 묶음으로 논리 레지스터 폭 확대 LMUL=1 1개 묶음 = 1×VLEN · 논리 32개 (기본값) LMUL=2 2개 묶음 = 2×VLEN · 논리 16개 LMUL=4 4개 묶음 = 4×VLEN · 논리 8개 LMUL=8 8개 묶음 = 8×VLEN · 논리 4개 묶음 시작 레지스터 번호는 LMUL의 배수 (LMUL=8 → {v0-v7}·{v8-v15}·{v16-v23}·{v24-v31}) — 위 도식은 8개 구간만 표시 3 vtype CSR — vsetvli가 런타임에 설정하는 벡터 타입 vill [XLEN-1] vma [7] vta [6] vsew [5:3] vlmul [2:0] vill = 불법 vtype 표시 · vma = 마스크 agnostic · vta = 테일 agnostic · vsew = 요소 폭(SEW) · vlmul = 레지스터 그룹 배수 vsew [5:3] → SEW: e8 / e16 / e32 / e64 vlmul [2:0] → LMUL: 1/8 · 1/4 · 1/2 · 1 · 2 · 4 · 8 VLMAX = VLEN / SEW × LMUL (예: VLEN=256, SEW=32, LMUL=4 → 256/32×4 = 32개 요소) 커널 RVV 사용 규칙 — arch/riscv/include/asm/vector.h kernel_vector_begin() / kernel_vector_end() — x86의 kernel_fpu_begin() / kernel_fpu_end()에 대응 preempt_disable() 포함 · sleep/blocking 금지 · 상태 저장·복원은 riscv_v_vstate_save() / restore() · CONFIG_RISCV_ISA_V=y 필요
RISC-V V 확장(RVV 1.0)의 벡터 레지스터 모델 개념도입니다. 32개 벡터 레지스터(v0-v31)는 각각 VLEN 비트이고, v0는 마스크 레지스터로 주로 쓰입니다. LMUL은 연속한 레지스터를 묶어 논리 레지스터 폭을 1×에서 8×VLEN까지 확대하며, 논리 레지스터 개수는 32/LMUL입니다. 요소 폭(SEW)과 LMUL·테일·마스크 정책은 vtype CSR의 비트 필드(vsew[5:3]·vlmul[2:0]·vta[6]·vma[7]·vill[XLEN-1])에 담기고 vsetvli 계열 명령이 런타임에 설정합니다. 마지막 블록은 커널에서 RVV를 쓸 때의 규칙(짧은 구간 안에서 커널 벡터 사용, 재진입·sleep 금지)을 요약합니다.

RVV 핵심 명령어

/* RISC-V 벡터 확장 — 핵심 명령어 패턴 */

/* === vsetvli: 벡터 길이/타입 설정 (모든 벡터 연산 전에 필수) === */
vsetvli a0, a1, e32, m4, ta, ma
/*   a0 = 실제 처리될 요소 수 (출력)
 *   a1 = 요청 요소 수 (AVL: Application Vector Length)
 *   e32 = SEW=32비트 요소
 *   m4  = LMUL=4 (v0-v3 그룹으로 사용)
 *   ta  = tail agnostic (테일 요소 미정의)
 *   ma  = mask agnostic (마스크 비활성 요소 미정의) */

vsetivli a0, 16, e8, m1, ta, ma
/* 즉시값 16으로 AVL 설정 (vsetvli의 즉시값 변종) */

/* === 벡터 로드/스토어 === */
vle32.v  v4, (a0)              /* 연속 로드: 32비트 요소, v4에 저장 */
vse32.v  v4, (a1)              /* 연속 스토어 */
vlse32.v v4, (a0), a2          /* Strided 로드: stride = a2 바이트 간격 */
vluxei32.v v4, (a0), v8        /* Indexed(Gather): base + v8[i]*4 */
vsuxei32.v v4, (a0), v8        /* Indexed(Scatter): v4[i] → base + v8[i]*4 */

/* 마스크 기반 로드 (v0이 마스크 레지스터) */
vle32.v  v4, (a0), v0.t        /* v0[i]=1인 요소만 로드, 나머지 유지 */

/* === 벡터 산술 === */
vadd.vv  v4, v8, v12           /* v4[i] = v8[i] + v12[i] */
vadd.vx  v4, v8, a0            /* v4[i] = v8[i] + a0 (스칼라 브로드캐스트) */
vadd.vi  v4, v8, 5             /* v4[i] = v8[i] + 5 (즉시값) */
vsub.vv  v4, v8, v12           /* 뺄셈 */
vmul.vv  v4, v8, v12           /* 곱셈 */
vmacc.vv v4, v8, v12           /* v4[i] += v8[i] * v12[i] (Multiply-Accumulate) */

/* === 벡터 논리/시프트 === */
vxor.vv  v4, v8, v12           /* XOR (RAID, 암호에 핵심) */
vand.vv  v4, v8, v12           /* AND */
vor.vv   v4, v8, v12           /* OR */
vsll.vx  v4, v8, a0            /* 좌측 시프트 (스칼라) */
vsrl.vv  v4, v8, v12           /* 논리 우측 시프트 */

/* === 비교 → 마스크 레지스터 === */
vmseq.vv v0, v4, v8            /* v0[i] = (v4[i] == v8[i]) ? 1 : 0 */
vmslt.vv v0, v4, v8            /* v0[i] = (v4[i] < v8[i]) ? 1 : 0 */
vcpop.m  a0, v0                /* a0 = popcount(v0) 활성 요소 수 */
vfirst.m a0, v0                /* a0 = 첫 번째 활성 비트 인덱스 */

/* === 리덕션 (벡터 → 스칼라) === */
vredsum.vs v4, v8, v12         /* v4[0] = v12[0] + sum(v8[0..vl-1]) */
vredmax.vs v4, v8, v12         /* v4[0] = max(v12[0], max(v8[0..vl-1])) */
vredxor.vs v4, v8, v12         /* v4[0] = v12[0] ^ xor(v8[0..vl-1]) */

/* === 퍼뮤테이션 === */
vslidedown.vx v4, v8, a0       /* v4[i] = v8[i + a0] (요소 슬라이드 다운) */
vslideup.vx   v4, v8, a0       /* v4[i + a0] = v8[i] (요소 슬라이드 업) */
vrgather.vv   v4, v8, v12      /* v4[i] = v8[v12[i]] (임의 퍼뮤트) */
vcompress.vm  v4, v8, v0       /* v0=1인 요소만 v4에 밀착 배치 */

커널 RVV 사용 패턴

/* arch/riscv/include/asm/vector.h — 커널 벡터 API */
#include <asm/vector.h>

/* 커널에서 RVV 사용 전 필수 API */
void my_rvv_xor(u8 *dst, const u8 *src, size_t len)
{
    if (!has_vector())
        return my_scalar_xor(dst, src, len);

    kernel_vector_begin();    /* preempt_disable() + 벡터 상태 저장 */

    /* RVV 명령어 사용 가능 구간 */
    asm volatile(
        "1:\n"
        "  vsetvli t0, %[len], e8, m8, ta, ma\n"
        "  vle8.v  v0, (%[src])\n"
        "  vle8.v  v8, (%[dst])\n"
        "  vxor.vv v8, v8, v0\n"
        "  vse8.v  v8, (%[dst])\n"
        "  add     %[src], %[src], t0\n"
        "  add     %[dst], %[dst], t0\n"
        "  sub     %[len], %[len], t0\n"
        "  bnez    %[len], 1b\n"
        : [dst] "+r"(dst), [src] "+r"(src), [len] "+r"(len)
        :: "t0", "v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7",
           "v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15",
           "memory"
    );

    kernel_vector_end();      /* preempt_enable() + lazy restore 설정 */
}

/* 벡터 상태 관리 내부 — arch/riscv/kernel/vector.c */
/*
 * kernel_vector_begin():
 *   1. preempt_disable()
 *   2. 현재 태스크의 벡터 상태가 레지스터에 live이면 메모리에 저장
 *      → riscv_v_vstate_save(current, task_pt_regs(current))
 *   3. 벡터 레지스터를 커널이 자유롭게 사용 가능
 *
 * kernel_vector_end():
 *   1. TIF_RISCV_V_DEFER_RESTORE 플래그 설정 (lazy restore)
 *   2. preempt_enable()
 *   3. 다음 유저 복귀 시 벡터 상태 복원
 *
 * 컨텍스트 스위치 시:
 *   riscv_v_vstate_save()  → vsseg/vse 명령어로 벡터 레지스터 저장
 *   riscv_v_vstate_restore() → vlseg/vle 명령어로 복원
 *   저장 크기: 32 × VLEN/8 바이트 (VLEN=256 → 1024바이트)
 */

/* 커널 RVV 활용 사례 (v6.5+) */
/* arch/riscv/lib/xor.S          — RAID XOR 벡터화 */
/* arch/riscv/crypto/aes-riscv64-zvkned.S — AES (Zvkned 확장) */
/* arch/riscv/crypto/sha256-riscv64-zvknha.S — SHA-256 (Zvknha 확장) */
/* arch/riscv/crypto/chacha-riscv64-zvkb.S  — ChaCha20 (Zvkb 확장) */
/* arch/riscv/lib/memcpy.S       — 벡터화된 memcpy */
/* arch/riscv/lib/memset.S       — 벡터화된 memset */

RISC-V 벡터 암호 확장 (Zvk*): RVV 1.0 위에 암호 전용 벡터 확장이 추가되었습니다. Zvkned(AES 암복호화), Zvknha/Zvknhb(SHA-256/SHA-512), Zvkb(비트 조작: vbrev, vrev8, vandn), Zvkg(GCM/GHASH), Zvksed/Zvksh(SM4/SM3 중국 표준). 이들은 x86의 AES-NI/SHA-NI, ARM의 Crypto Extension에 대응합니다. 커널 v6.9부터 arch/riscv/crypto/에 SIMD 가속 알고리즘이 등록되었고, 이후 ChaCha·SHA-256·SHA-512·Poly1305·AES 구현이 lib/crypto/riscv/로 옮겨졌습니다.

AMX — 행렬 연산 가속

Intel AMX(Advanced Matrix Extensions)는 Sapphire Rapids(2023)에서 도입된 타일 기반 행렬 곱셈 가속기입니다. 8개의 타일 레지스터(TMM0-TMM7, 각 최대 1KB)를 사용하여 INT8/BF16 행렬 외적(Outer Product)을 하드웨어에서 수행합니다. 커널에서는 직접 사용하지 않지만, 유저 공간 상태 관리가 커널의 핵심 역할입니다.

Intel AMX 타일 구조 및 TDPB* 행렬 곱셈 연산 1) 타일 레지스터 (TMM0–TMM7) 최대 8개 타일(TMM0–TMM7). 각 타일은 최대 16행 × 64열(바이트) = 1,024 B 저장 INT8: rows ≤ 16, cols ≤ 64 / BF16: rows ≤ 16, cols ≤ 32 — TILECFG로 구성 TMM0 (예: 16×64B) TMM1 src/dst TMM2 src TMM3 src TMM4 TMM5 TMM6 TMM7 2) TILECFG (타일 구성 레지스터, 64 B) 타일 행/열 크기(colsb/rows)와 팔레트(palette) 정의 palette[0]=1 (AMX-INT8/BF16), start_row[1]=인터럽트 재시작 행 colsb[0..7] [16–23]: 각 TMM의 열 크기(바이트) rows[0..7] [48–55]: 각 TMM의 행 수 INT8/BF16 공통: rows ≤ 16, cols ≤ 64 (BF16은 요소 수 기준 절반) ldtilecfg / sttilecfg (메모리 ↔ TILECFG) TILELOAD/TILESTORE는 TMM ↔ 메모리 전송에 사용 3) TDPB* (Tile Dot-Product) — 타일 단위 행렬 곱셈 누적 AMX는 타일 단위 외적(outer product)을 누적. C = A×B를 타일로 분할해 하드웨어 병렬 처리 메모리: A_tiles (M×K) TILELOAD → TMM src1 C_{i,j} += ∑_k A_{i,k}×B_{k,j} TDPB* 메모리: B_tiles (K×N) TILELOAD → TMM src2 타일 단위 외적 누적 메모리: C_tiles (M×N) TILESTORE ← TMM dst (누적) TDPBSSD/BSUD/BUSD/BUUD, TDPBF16PS C += A×B TILELOAD/TILESTORE: 메모리 ↔ TMM 전송 / TDPB*: 타일 내적 누적(Tile Dot Product) — INT8/BF16 행렬 곱셈 가속 참고: 커널은 AMX 상태를 직접 계산에 사용하지 않고, 유저 공간 AMX 상태(XSAVE 영역) 관리에 주로 관여합니다. 명령어 예시: TILELOADD, TILESTORED, TDPBSSD/TDPBSUD/TDPBUSD/TDPBUUD (INT8), TDPBF16PS (BF16) 최초 TILELOAD/TDPB* 실행 시 CPU가 #NM 예외를 발생시켜 AMX 활성화/상태 초기화를 처리합니다.
Intel AMX TMM 타일, TILECFG, TDPB* 흐름
/* 커널의 AMX 상태 관리 — Lazy Allocation via XFD */
/*
 * 문제: AMX TILEDATA는 태스크당 8,192바이트. 모든 프로세스에
 *       미리 할당하면 메모리 낭비 (대부분의 프로세스는 AMX를 쓰지 않음)
 *
 * 해결: XFD (eXtended Feature Disable) 메커니즘
 *
 *   1. 프로세스 생성 시: TILEDATA에 대한 XFD 비트 = 1 (사용 금지)
 *   2. 처음 TILELOAD/TDPB* 명령 실행 시: CPU가 #NM 예외 발생
 *   3. 커널 #NM 핸들러:
 *      a. TILEDATA용 8,192바이트 메모리 동적 할당
 *      b. thread.fpu.fpstate 확장
 *      c. XFD 비트 = 0으로 클리어 (이제 AMX 사용 가능)
 *   4. 이후 컨텍스트 스위치에서 XSAVE/XRSTOR로 저장/복원
 *
 * → 실제로 AMX를 사용하는 프로세스만 8KB 추가 메모리 소비
 */

/* arch/x86/kernel/fpu/xstate.c */
static int xfd_enable_feature(u64 xfd_err)
{
    struct fpu *fpu = &current->thread.fpu;
    int ret;

    /* TILEDATA 요청인지 확인 */
    if (!(xfd_err & XFEATURE_MASK_XTILE_DATA))
        return -EINVAL;

    /* fpstate를 확장하여 TILEDATA 공간 할당 */
    ret = fpstate_realloc(xfd_err, 0, fpu);
    if (ret)
        return ret;

    /* XFD MSR에서 해당 비트 클리어 → 이후 AMX 명령 정상 실행 */
    xfd_update_state(fpu->fpstate);
    return 0;
}

/* AMX 명령어 요약 */
/*
 * ldtilecfg  mem64  — TILECFG를 메모리에서 로드 (팔레트, 행/열 설정)
 * sttilecfg  mem64  — TILECFG를 메모리에 저장
 * tileloadd  tmm, mem — 타일에 행렬 데이터 로드 (stride 지정)
 * tilestored mem, tmm — 타일에서 행렬 데이터 저장
 * tilerelease        — 모든 타일 상태 해제 (INIT 상태로)
 * tilezero   tmm     — 지정 타일을 0으로 초기화
 *
 * tdpbssd  tmm0, tmm1, tmm2 — C += A × B (signed×signed → int32)
 * tdpbsud  tmm0, tmm1, tmm2 — C += A × B (signed×unsigned → int32)
 * tdpbusd  tmm0, tmm1, tmm2 — C += A × B (unsigned×signed → int32)
 * tdpbuud  tmm0, tmm1, tmm2 — C += A × B (unsigned×unsigned → int32)
 * tdpbf16ps tmm0, tmm1, tmm2 — C += A × B (BF16×BF16 → FP32)
 * tdpfp16ps tmm0, tmm1, tmm2 — C += A × B (FP16×FP16 → FP32, AMX-FP16)
 */

/* AMX 확장 진화 (세대별) */
/*
 * Sapphire Rapids (2023):  AMX-INT8 + AMX-BF16 (기본 타일 곱셈)
 *   → tdpbssd/tdpbsud/tdpbusd/tdpbuud (INT8), tdpbf16ps (BF16)
 *
 * Granite Rapids (2024):    AMX-FP16 + AMX-COMPLEX 추가
 *   → tdpfp16ps (FP16×FP16→FP32, AMX-FP16)
 *   → AMX-COMPLEX: 복소수(Complex) FP16 행렬 곱셈 (Granite Rapids-D 한정)
 *
 * Diamond Rapids (2026 예정): 확장 AMX + SM4 EVEX + MOVRS
 *   → AMX-FP8·AMX-AVX512·AMX-TRANSPOSE·AMX-MOVRS (컴파일러 사전 지원, Intel ISA 매뉴얼 기준)
 *   → MOVRS (Move Read-Shared Value): 공유 읽기 최적화
 *   → SM4 암호화 EVEX 인코딩 확장 (중국 표준 블록 암호 가속)
 */

/* XSAVE 크기 비교 (태스크당 FPU 상태 메모리) */
/*
 * SSE만 사용:     ~576 바이트
 * + AVX:          ~832 바이트  (+256)
 * + AVX-512:      ~2,688 바이트 (+1,856)
 * + AMX:          ~10,880 바이트 (+8,192)
 *
 * → AMX 활성 프로세스는 컨텍스트 스위치 시 ~10KB 저장/복원
 * → XFD Lazy Allocation으로 실제 사용 프로세스만 부담
 */
AMX 커널 영향:
  • 컨텍스트 스위치 비용 — AMX 활성 프로세스의 XSAVE/XRSTOR는 ~10KB 메모리 접근. 빈번한 스위칭 시 성능 영향 큼
  • 커널에서 미사용 — 커널 자체는 AMX를 사용하지 않음. XFD/#NM 핸들러와 XSAVE 상태 관리만 담당
  • prctl(ARCH_REQ_XCOMP_PERM) — 유저 공간에서 AMX 사용 전에 명시적 권한 요청 필요 (Linux 5.16+)
  • KVM 가상화(Virtualization) — 게스트 VM의 AMX 상태도 커널이 관리. IA32_XFD_ERR MSR VMEXIT 처리 필요

아키텍처별 SIMD 비교

x86, ARM, RISC-V 세 아키텍처의 SIMD 확장을 직접 비교합니다. 커널 개발자가 크로스 플랫폼 SIMD 코드를 작성할 때 참고할 수 있습니다.

특성x86 SSE/AVX/AVX-512ARM NEON/SVE/SVE2RISC-V RVV 1.0
벡터 길이 고정: 128/256/512-bit NEON: 고정 128-bit
SVE: 128~2048-bit (하드웨어 정의)
128~65536-bit (VLEN, 하드웨어 정의)
프로그래밍 모델 벡터 길이 종속 (VLD) NEON: VLD
SVE: 벡터 길이 비종속 (VLA)
벡터 길이 비종속 (VLA)
레지스터 수 XMM/YMM: 16개
ZMM: 32개
NEON: V0-V31 (32개)
SVE: Z0-Z31 (32개)
v0-v31 (32개)
마스크/프레디케이트 AVX-512: k0-k7 (8개)
SSE/AVX: 없음 (비교→블렌드)
SVE: P0-P15 (16개)
NEON: 비교→BSL 블렌드
v0 레지스터가 마스크 역할
(전용 마스크 레지스터 없음)
Gather/Scatter AVX2+: VGATHER
AVX-512: + VSCATTER
SVE: LD1/ST1 인덱스 형태 기본 지원 vluxei/vsuxei 기본 지원
그룹핑/확장 없음 (고정 벡터 폭) 없음 (VL이 고정, 레지스터 결합 없음) LMUL (1/8~8): 레지스터 그룹핑으로 논리 폭 확장
테일 처리 AVX-512: 마스크
SSE/AVX: 수동 스칼라 루프
SVE: whilelt 프레디케이트 자동 vsetvli가 실제 처리 수 반환, 자동
커널 FPU 보호 kernel_fpu_begin/end() kernel_neon_begin/end() kernel_vector_begin/end()
상태 저장 메커니즘 XSAVE/XRSTOR (확장 가능) fpsimd_save/load_state() vstate_save/restore()
암호 전용 확장 AES-NI, SHA-NI, PCLMULQDQ, VAES ARMv8 CE: AESE/AESD, SHA, PMULL Zvkned(AES), Zvknha(SHA), Zvkg(GCM)
행렬 연산 AMX (TMM0-TMM7, INT8/BF16) SME (ZA 타일, FMOPA, BF16) 미정 (향후 확장 예상)
주파수 영향 AVX-512: 다운클럭 (AVX offset) SVE: 구현 의존 (보통 영향 없음) 구현 의존 (보통 영향 없음)
상태 저장 크기 SSE: 512B, AVX: 1KB,
AVX-512: 2.5KB, +AMX: 10KB+
NEON: 528B, SVE(256-bit): 1KB+
SME: ZA 크기 추가 (512B~8KB)
32 × VLEN/8 (VLEN=256: 1KB)

크로스 아키텍처 커널 SIMD 코드 패턴

/* 커널에서 아키텍처 독립적 SIMD 가속 구현 패턴 */
/* 예: crypto 서브시스템의 multi-arch 알고리즘 등록 */

/* === 아키텍처별 구현 파일 구조 === */
/*
 * crypto/chacha_generic.c          ← 순수 C fallback (모든 아키텍처)
 * arch/x86/crypto/chacha_glue.c    ← x86 SIMD 가속 (SSE3/AVX2/AVX-512)
 * arch/arm64/crypto/chacha-neon-glue.c ← ARM64 NEON 가속
 * arch/riscv/crypto/chacha-riscv64-zvkb.S ← RISC-V RVV 가속
 *
 * 각 아키텍처 구현은 동일한 crypto API 인터페이스를 제공:
 *   .cra_name = "chacha20"
 *   .cra_priority = 300 (SIMD) > 100 (generic)
 *   → 런타임에 가장 높은 우선순위의 구현이 자동 선택
 */

/* 조건부 컴파일 가드 패턴 */
#if defined(CONFIG_X86_64)
#include <asm/fpu/api.h>
#define SIMD_BEGIN()  kernel_fpu_begin()
#define SIMD_END()    kernel_fpu_end()
#define HAS_SIMD      cpu_feature_enabled(X86_FEATURE_AVX2)

#elif defined(CONFIG_ARM64)
#include <asm/neon.h>
#define SIMD_BEGIN()  kernel_neon_begin()
#define SIMD_END()    kernel_neon_end()
#define HAS_SIMD      cpu_have_named_feature(ASIMD)

#elif defined(CONFIG_RISCV) && defined(CONFIG_RISCV_ISA_V)
#include <asm/vector.h>
#define SIMD_BEGIN()  kernel_vector_begin()
#define SIMD_END()    kernel_vector_end()
#define HAS_SIMD      has_vector()

#else
#define HAS_SIMD      0
#endif

/* 공통 호출 패턴 */
void my_crypto_op(u8 *dst, const u8 *src, size_t len)
{
    if (HAS_SIMD && may_use_simd()) {
        SIMD_BEGIN();
        my_crypto_simd(dst, src, len); /* arch별 SIMD 구현 */
        SIMD_END();
    } else {
        my_crypto_generic(dst, src, len); /* C fallback */
    }
}

/* may_use_simd(): softirq/hardirq 컨텍스트에서는 false 반환
 * → 이미 다른 코드가 FPU를 사용 중일 수 있으므로 중첩 방지
 * → crypto API의 simd_skcipher 래퍼가 자동으로 cryptd 워크큐 fallback */

크로스 플랫폼 SIMD 전략: 커널 crypto 서브시스템은 crypto_register_skciphers()로 아키텍처별 구현을 등록합니다. cra_priority 값이 높은 SIMD 구현이 자동 선택되며, may_use_simd()가 false를 반환하면 cryptd 워크큐로 지연 실행되거나 generic C fallback이 사용됩니다. 새로운 아키텍처에서 SIMD 가속을 추가할 때는 arch/<arch>/crypto/에 구현을 추가하고 높은 priority로 등록하면 됩니다.

커널 암호 서브시스템 SIMD 통합 상세

Linux crypto API는 SIMD 가속을 투명하게 관리하는 다층 구조를 제공합니다. 디스크 암호화(dm-crypt), 네트워크 암호화(IPsec/WireGuard), 파일시스템(Filesystem) 암호화(fscrypt)가 모두 이 프레임워크를 통해 SIMD 가속의 혜택을 받습니다.

커널 crypto API SIMD 가속 계층 구조 소비자 (Consumer Layer) dm-crypt (디스크) IPsec/WireGuard fscrypt (파일) CIFS/NFS (네트워크) IMA/EVM (무결성) crypto_alloc_skcipher("aes-xts") crypto API 디스패치 (crypto/api.c, crypto/algapi.c) 알고리즘 이름 검색 → cra_priority 기반 최적 구현 자동 선택 may_use_simd() 검사: process context → SIMD 사용 / 제한 컨텍스트(softirq 등) → cryptd 또는 fallback 아키텍처별 SIMD 가속 구현 (priority 높음) x86: aes-ni, chacha-avx2, sha-ni ARM64: aes-ce, chacha-neon, sha-ce RISC-V: aes-zvkned, chacha-zvkb, sha-zvknha SIMD 래퍼 (crypto/simd.c) 프로세스 컨텍스트: kernel_fpu_begin()/end() 제한 컨텍스트(softirq 등): cryptd 워크큐로 지연 CONFIG_CRYPTO_SIMD 필요 순수 C fallback (priority 낮음) SIMD 미지원 또는 사용 불가능한 경우 process/interrupt 경계에서 안전한 경로 모든 아키텍처에서 동작 아키텍처별 SIMD 프리미티브 (FPU/NEON/Vector 컨텍스트) x86: kernel_fpu_begin()/end(), XSAVE/FXSAVE AVX-512 등 대형 레지스터 컨텍스트 포함 ARM64: kernel_neon_begin()/end() SVE/SME 컨텍스트 처리 RISC-V: kernel_vector_begin()/end() RVV 레지스터 세트 (VLEN에 의존)

dm-crypt SIMD 가속 경로

dm-crypt 디스크 암호화의 SIMD 가속 데이터 흐름:

  1. 유저 write() 호출
  2. VFS → Block Layer → dm-crypt
  3. dm-crypt가 crypto API를 호출합니다:
    skcipher_request_set_crypt(req, src_sg, dst_sg, nbytes, iv);
    crypto_skcipher_encrypt(req);
  4. crypto API 내부 동작:
    • process context (kcryptd 워크큐): may_use_simd() == true이면 kernel_fpu_begin() → AES-NI/VAES로 직접 암호화 → kernel_fpu_end()
    • softirq context (드물지만 가능): may_use_simd() == false이면 cryptd 워크큐로 지연하거나 aes-generic fallback을 사용합니다.
  5. 암호화된 데이터 → Block Layer → 디스크 기록

dm-crypt 성능 최적화 포인트:

dm-crypt 설정 시 SIMD 확인:

$ cat /proc/crypto | grep -A4 "aes"
  driver       : aes-aesni
  module       : aesni_intel
  priority     : 300
  type         : cipher

driver가 "aesni"이면 AES-NI 가속이 활성화된 것이고, "aes-generic"이면 소프트웨어 fallback을 사용 중입니다.

SIMD로 구현된 커널 암호 알고리즘 지도

여기서 중요한 점은 커널 SIMD 암호가 AES 몇 종만의 문제로 끝나지 않는다는 것입니다. 최신 커널 소스를 기준으로 보면 arch/x86/crypto/, arch/arm64/crypto/, arch/riscv/crypto/에는 아키텍처별 등록 구현이 있고, lib/crypto/에는 ChaCha, Poly1305, GHASH, POLYVAL, NH, BLAKE2s, Curve25519, SHA 계열 같은 공용 primitive 최적화가 들어 있습니다. 즉 상위 알고리즘 하나를 빠르게 만들려면 내부 primitive까지 함께 SIMD화되어 있어야 합니다.

계층대표 알고리즘SIMD/전용 확장대표 파일주요 사용처
x86 arch/cryptoAES, GCM, Camellia, Serpent, Twofish, ARIA, SM4, AEGIS128AES-NI, VAES, PCLMULQDQ, VPCLMULQDQ, SSE2, AVX, AVX2, AVX-512, GFNIaesni-intel_glue.c, aes-gcm-vaes-avx512.Sdm-crypt, IPsec, fscrypt, AF_ALG
arm64 arch/cryptoAES, GHASH, CCM, GCM, SM4NEON, ARMv8 Crypto Extensions, PMULLaes-glue-ce.c, ghash-ce-glue.c, aes-neonbs-glue.cdm-crypt, kTLS, IPsec, fscrypt
riscv arch/cryptoAES, SM4Zvkned, Zvkb, Zvbb, Zvkg, Zvksedaes-riscv64-glue.c, sm4-riscv64-glue.c범용 skcipher, 저장장치 암호화
lib/cryptoChaCha20, Poly1305, GHASH, POLYVAL, NH, BLAKE2s, Curve25519, SHA-1/256/512, SM3SSSE3, AVX2, AVX512VL, NEON, CE, RVV/Zvk*chacha.c, poly1305.c, gf128hash.cWireGuard, CRNG, Adiantum, HCTR2, HMAC/KDF
crypto wrapperSIMD 래퍼 자체may_use_simd(), cryptd, 비동기 래핑crypto/simd.c, include/crypto/internal/simd.hsoftirq-safe AEAD fallback

이 표를 실무적으로 읽는 방법은 간단합니다. 상위 알고리즘 이름과 실제 SIMD primitive를 분리해서 봐야 합니다. 예를 들어 AES-GCM은 AES 라운드만 빠르다고 끝나지 않고 GHASH도 빨라야 하며, ChaCha20-Poly1305는 ChaCha와 Poly1305 양쪽이 동시에 최적화되어야 의미가 있습니다. Adiantum도 마찬가지로 XChaCha12보다 NH와 Poly1305 품질이 전체 처리량에 큰 영향을 줍니다.

x86 SIMD crypto 구현 목록

x86은 현재 커널에서 가장 넓은 SIMD 암호 구현 범위를 제공합니다. Kconfig와 실제 소스 파일 기준으로 보면 AES 계열뿐 아니라 Camellia, Serpent, Twofish, ARIA, SM4, AEGIS128까지 별도 SIMD fast path가 존재합니다.

알고리즘구현주요 SIMD 명령설명
AES ECB/CBC/CTS/CTR/XCTR/XTS/GCMCRYPTO_AES_NI_INTELAES-NI, VAES, PCLMULQDQ, VPCLMULQDQdm-crypt, IPsec, kTLS에서 가장 많이 쓰이는 핵심 fast path
Camelliacamellia_aesni_avx_glue.c, camellia_aesni_avx2_glue.cAES-NI, AVX, AVX2다블록 병렬 처리로 ECB/CBC fast path 제공
Serpentserpent_sse2_glue.c, serpent_avx_glue.c, serpent_avx2_glue.cSSE2, AVX, AVX2비트슬라이스 성격이 강해 SIMD 폭 증가 효과가 큼
Twofishtwofish_glue_3way.c, twofish_avx_glue.c3-way OoO, AVX전통적인 3-way 병렬과 AVX 8-block 병렬이 공존
ARIAaria_aesni_avx_glue.c, aria_aesni_avx2_glue.c, aria_gfni_avx512_glue.cAES-NI, AVX2, GFNI, AVX-512GFNI가 있으면 S-Box 및 선형 변환 경로가 더 직접적으로 빨라짐
SM4sm4_aesni_avx_glue.c, sm4_aesni_avx2_glue.cAES-NI, AVX, AVX2AES 보조 명령을 변환해 SM4 라운드를 가속
AEGIS128aegis128-aesni-glue.cAES-NI, SSE2고속 AEAD 구현. 전용 워크로드에서 매우 높은 처리량

x86에서 놓치기 쉬운 지점은 블록 암호 외의 보조 primitive입니다. lib/crypto에는 x86/chacha-avx2-x86_64.o, x86/chacha-avx512vl-x86_64.o, x86/ghash-pclmul.o, x86/polyval-pclmul-avx.o, x86/poly1305-x86_64-cryptogams.o, x86/sha256-avx2-asm.o, x86/sha1-ni-asm.o, x86/blake2s-core.o가 함께 존재합니다. 따라서 AES-GCM, WireGuard, HCTR2 같은 상위 알고리즘의 실효 성능은 주변 primitive까지 포함한 묶음 최적화 품질로 결정됩니다.

ARM64·RISC-V SIMD crypto 구현 목록

ARM64는 NEON과 ARMv8 Crypto Extensions, RISC-V는 Zvk* 벡터 암호 확장이 중심입니다. 둘 다 x86보다 arch 등록 알고리즘 수는 적어 보일 수 있지만, lib/crypto 층까지 합치면 실제 범위는 꽤 넓습니다.

아키텍처알고리즘주요 확장대표 파일설명
ARM64AES ECB/CBC/CTR/XTSCE, NEONaes-glue-ce.c, aes-glue-neon.c하드웨어 AES가 없을 때도 NEON fallback 가능
ARM64bit-sliced AES / XCTR / XTSNEONaes-neonbs-glue.c테이블 의존을 줄이고 일정한 데이터 흐름을 유지
ARM64GHASH / GCMPMULL, CEghash-ce-glue.cGCM 태그 계산에서 핵심인 GF 곱셈을 직접 가속
ARM64SM4 / SM4-CCM / SM4-GCMSM4 CE, PMULL, NEONsm4-ce-gcm-glue.c, sm4-neon-glue.cSM4도 AEAD까지 포함한 완전한 fast path 보유
RISC-VAES ECB/CBC/CTS/CTR/XTSZvkned, Zvkb, Zvbb, Zvkgaes-riscv64-zvkned-zvbb-zvkg.SXTS, CTR를 벡터 암호 확장 조합으로 처리
RISC-VSM4Zvksed, Zvkbsm4-riscv64-zvksed-zvkb.SSM4 라운드와 비트 조작을 함께 벡터화

RISC-V는 arch/riscv/crypto/의 AES·GHASH·SM3·SM4 벡터 구현과 lib/crypto/riscv/의 ChaCha·SHA-256·SHA-512·Poly1305 구현을 함께 제공합니다. v7.0 기준으로 lib/crypto/riscv/에는 chacha-riscv64-zvkb.S, sha256-riscv64-zvknha_or_zvknhb-zvkb.S, sha512-riscv64-zvknhb-zvkb.S, poly1305-riscv.pl, aes-riscv64-zvkned.S가 있고, ghash-riscv64-zvkg.S와 sm3-riscv64-zvksh-zvkb.S는 arch/riscv/crypto/에 남아 있습니다. 즉 "RISC-V는 AES만 벡터화되었다"라고 이해하면 현재 트리를 지나치게 단순화하게 됩니다.

라이브러리 primitive와 상위 알고리즘의 연결

상위 알고리즘/모드내부 primitiveSIMD 가속 포인트실사용 위치
AES-GCMAES-CTR + GHASHVAES/AES-NI + VPCLMULQDQ 또는 CE+PMULLIPsec, kTLS, TLS offload
ChaCha20-Poly1305ChaCha20 + Poly1305AVX2/AVX512VL, NEON, RVV, arch asm Poly1305WireGuard, TLS 1.3, AEAD
AdiantumXChaCha12 + NHPoly1305 + AESChaCha SIMD, NH NEON/AVX2, Poly1305 asm, AES 단일 블록fscrypt, dm-crypt
HCTR2AES + XCTR + POLYVALAES-NI/CE + POLYVAL(CLmul/PMULL)fscrypt 최신 contents mode
WireGuard handshake 보조BLAKE2s + Curve25519 + ChaCha20BLAKE2s arch core, Curve25519 arch core, ChaCha SIMDNoise IK 키 교환
SHA 계열SHA-1/256/512 압축 함수SHA-NI, AVX2, ARM CE, RISC-V Zvknh*HMAC, IMA, module signing, KDF
실무 해석: "fscrypt가 HCTR2를 쓴다"는 것은 실제로는 AES 경로와 POLYVAL 경로가 둘 다 충분히 빠른가를 확인해야 한다는 뜻입니다. "Adiantum이 빠르다"는 말도 XChaCha12만이 아니라 NH와 Poly1305 구현 품질까지 포함합니다. 따라서 성능이나 fallback을 점검할 때는 상위 알고리즘 이름만 보지 말고 하위 primitive와 driver 우선순위를 함께 봐야 합니다.

IPsec/WireGuard SIMD 가속

/* IPsec: ESP 변환에서의 SIMD 사용 */
/*
 * IPsec ESP 패킷(Packet) 처리 경로:
 *   xfrm_output() → esp_output() → crypto_aead_encrypt()
 *   → GCM(AES) = AES-CTR + GHASH
 *
 * AES-CTR: AES-NI/VAES로 가속
 * GHASH:   PCLMULQDQ/VPCLMULQDQ (Galois Field 곱셈)
 *
 * 주의: IPsec은 softirq(NET_RX)에서도 실행됨
 *   → 이미 process ctx에서 FPU 사용 중이면 충돌
 *   → simd_skcipher 래퍼가 자동으로 cryptd 워크큐 fallback
 *
 * ESP 가속 알고리즘 등록:
 *   rfc4106(gcm(aes))  — RFC4106 GCM-AES (IPsec 표준)
 *   cra_driver_name = "rfc4106-gcm-aesni"
 *   cra_priority = 400  (generic보다 높음)
 */

/* WireGuard: ChaCha20-Poly1305 SIMD 가속 */
/*
 * WireGuard는 ChaCha20-Poly1305 AEAD 사용 (고정 알고리즘)
 *
 * ChaCha20:
 *   x86: AVX2로 8블록(512B) 병렬 처리 → ~7 GB/s
 *        AVX-512로 16블록(1KB) 병렬 → ~12 GB/s
 *   ARM64: NEON으로 4블록(256B) 병렬 → ~4 GB/s
 *
 * Poly1305:
 *   x86: AVX2로 radix 2^26 표현, 4-way 병렬 → ~10 GB/s
 *   ARM64: NEON 4-way 병렬 → ~5 GB/s
 *
 * WireGuard 특수성:
 *   chacha20poly1305_encrypt()가 직접 구현 사용 (crypto API 미경유)
 *   → include/crypto/chacha20poly1305.h
 *   → lib/crypto/chacha20poly1305.c + arch별 SIMD 구현
 *   → kernel_fpu_begin/end를 내부에서 직접 관리
 */

/* 실제 WireGuard SIMD 호출 경로 */
/* drivers/net/wireguard/noise.c */
bool chacha20poly1305_encrypt(
    u8 *dst, const u8 *src, const size_t src_len,
    const u8 *ad, const size_t ad_len,
    const u64 nonce, const u8 key[CHACHA20POLY1305_KEY_SIZE])
{
    /* 내부적으로 may_use_simd() 확인 후 경로 분기:
     *   SIMD 가능: chacha20_simd() + poly1305_simd()
     *   SIMD 불가: chacha20_generic() + poly1305_generic()
     */
}

/* 성능 비교: cryptsetup benchmark (4KB 블록) */
/*
 * 알고리즘           generic     AES-NI      AVX2       AVX-512
 * ────────────────────────────────────────────────────────────
 * aes-xts-256        200 MB/s   3,500 MB/s     —       6,000 MB/s
 * chacha20-poly1305  350 MB/s      —        5,500 MB/s  8,000 MB/s
 * aes-gcm-256        180 MB/s   4,200 MB/s     —       7,000 MB/s
 * sha256             250 MB/s   1,200 MB/s     —       2,000 MB/s
 * crc32c             800 MB/s   9,000 MB/s     —          —
 */

SIMD 가속 확인 방법: cat /proc/crypto | grep -B2 "priority.*[3-9][0-9][0-9]"으로 높은 우선순위(Priority)(300+) 알고리즘을 찾으면 SIMD 가속 구현입니다. cryptsetup benchmark 명령으로 현재 시스템의 실제 crypto 처리량을 측정할 수 있습니다. dm-crypt가 어떤 알고리즘을 사용하는지는 dmsetup table --showkeys로 확인합니다.

x86 데이터 이동 명령어 시각화

SIMD 연산의 첫 단계는 데이터를 메모리에서 레지스터로, 또는 레지스터 간에 이동하는 것입니다. 정렬 여부, 논템포럴 힌트, 브로드캐스트 패턴에 따라 명령어 선택이 성능에 큰 영향을 미칩니다.

MOVAPS vs MOVDQU — 정렬/비정렬 메모리→레지스터 전송 MOVAPS (정렬 필수, 16B 경계) 메모리 (16-byte 정렬 주소: 0x...0) float[0] | float[1] | float[2] | float[3] 1 µop L1 hit: 4c XMM0 float[0] | float[1] | float[2] | float[3] 비정렬 주소 → #GP(0) General Protection Fault MOVDQU / VMOVDQU (비정렬 허용) 메모리 (임의 주소, 정렬 불필요) byte[0..15] — 캐시라인 경계를 걸칠 수 있음 1 µop split: +11c XMM0 byte[0] | byte[1] | ... | byte[15] VMOVNTPS (Non-Temporal Store, WC 기록) XMM/YMM src 캐시 오염 없이 WC 버퍼로 직접 기록 bypass L1/L2 메모리 (WC 결합 후 한 번에 flush) 64B 라인 단위로 결합, sfence 필요 Haswell+: MOVAPS ≈ MOVDQU 성능 동일 (정렬 시). 캐시라인 split penalty만 차이. NT store는 대용량 memcpy(>LLC 크기)에서만 유리. 소량이면 일반 store가 더 빠름. VBROADCAST / VINSERT / VEXTRACT 동작 시각화 VBROADCASTSS ymm, mem32 3.14f 스칼라 (mem32) 3.14 3.14 3.14 3.14 3.14 3.14 3.14 3.14 YMM: 8 × float — 모든 레인에 동일 값 복제 VINSERTF128 ymm1, ymm2, xmm3, imm8[0] ymm2[255:128] ymm2[127:0] xmm3[127:0] ymm2[255:128] 유지 xmm3[127:0] 삽입 imm8[0]=0 → ymm1 하위 128비트가 xmm3로 교체 VEXTRACTF128 xmm1, ymm2, imm8[0] ymm2[255:128] ymm2[127:0] xmm1 ← ymm2[255:128] imm8[0]=1 → ymm2 상위 128비트를 추출

broadcast + FMA 패턴: 행렬 곱셈에서 행/열 원소를 broadcast한 뒤 FMA와 결합하면 루프 본문을 최소화할 수 있습니다. vbroadcastss는 별도 로드 포트를 사용하므로 FMA와 동시 실행 가능합니다.

x86 스칼라 FP 이동 — MOVSS/VMOVSS와 MOVSD/VMOVSD

MOVSS/VMOVSS와 MOVSD/VMOVSD는 SIMD 레지스터를 사용하지만 실제로는 하위 32비트 또는 64비트만 이동합니다. 중요한 점은 low lane만 바꾸고 나머지 비트를 어떻게 처리하는지가 인코딩마다 다르다는 것입니다. 또한 AVX scalar move는 128비트 form만 유효하므로 VEX.L=0 규칙을 따릅니다. 커널에서 마지막 잔여 원소를 처리하거나, 이미 계산된 XMM 누산기의 low lane만 덮어쓸 때 이 차이를 잘못 이해하면 상위 lane이 남아 오염되거나 반대로 0으로 지워질 수 있습니다.

legacy MOVSS와 VEX VMOVSS는 low lane 외 비트 처리 규칙이 다릅니다 ■ 기존 dst 유지 ■ merge src 복사 ■ low lane 신규 값 ■ 0으로 정리 1. legacy MOVSS xmm, xmm (reg→reg) dst 이전 A3 A2 A1 A0 src low32로 교체 A3 A2 A1 B0 dst 이후 — low32는 src 값(B0), 상위 96비트 그대로 상위 96비트: 기존 dst 유지 128비트 초과(YMM/ZMM): 기존 값 유지 2. VEX VMOVSS xmm1, xmm2, xmm3 merge 소스 (두 번째 오퍼랜드) C3 C2 C1 C0 세 번째 오퍼랜드 low32로 교체 C3 C2 C1 D0 dst 이후 — low32는 세 번째 오퍼랜드(D0), 상위는 merge 소스 상위 96비트: merge 소스에서 복사 128비트 초과(YMM/ZMM): 0으로 정리 3. VEX VMOVSS xmm, m32 메모리 m32 3.14 0 0 0 3.14 dst 이후 — low32에 3.14 로드, 상위 96비트는 0 상위 96비트: 0으로 정리 128비트 초과(YMM/ZMM): 0으로 정리 MOVSD/VMOVSD도 같은 패턴입니다. 차이는 low lane 폭이 32비트가 아니라 64비트라는 점뿐입니다. 모든 scalar FP 명령은 SIMD/FPU 상태를 사용하므로 커널에서는 packed 명령과 동일하게 kernel_fpu_begin/end 규칙을 지켜야 합니다.
명령어형식low lane나머지 low 128비트128비트 초과 상위
MOVSS xmm, xmmlegacy reg→regsrc의 32비트 복사dst의 상위 96비트 유지YMM/ZMM 상위는 legacy 상태 유지
MOVSS xmm, m32legacy mem→reg메모리 32비트 로드xmm[127:32] = 0YMM/ZMM 상위는 legacy 상태 유지
VMOVSS xmm, xmm, xmmVEX reg,reg,reg세 번째 오퍼랜드의 32비트두 번째 오퍼랜드의 상위 96비트 복사0으로 클리어
VMOVSS xmm, m32VEX mem→reg메모리 32비트 로드xmm[127:32] = 00으로 클리어
MOVSD / VMOVSD64-bit scalar double64비트 low lane 이동legacy는 upper 64 유지, VEX reg-form은 merge, mem-form은 0VEX는 0, legacy는 기존 상태 유지
명령어오퍼랜드동작지연(cycle)처리량(CPI)
MOVAPSxmm, m128정렬 128b 로드4–50.25–0.5
VMOVUPSymm, m256비정렬 256b 로드4–50.25–0.5
VMOVDQU32zmm{k}, m512마스크 512b 로드5–70.5
MOVSSxmm, m32스칼라 float 로드, xmm[127:32]=04–50.5
VMOVSSxmm,xmm,xmm / xmm,m32low32 merge 또는 load, VEX 상위 비트 정리1–50.5
MOVSDxmm, m64스칼라 double 로드, xmm[127:64]=04–50.5
VMOVSDxmm,xmm,xmm / xmm,m64low64 merge 또는 load, VEX 상위 비트 정리1–50.5
VBROADCASTSSymm, m32스칼라→8레인 복제5–70.5
VBROADCASTSDymm, m64스칼라→4레인 복제5–70.5
VINSERTF128ymm,ymm,xmm,i128b 레인 삽입31
VEXTRACTF128xmm,ymm,i128b 레인 추출31
VMOVNTPSm256, ymmNT 스토어 (WC)—1
/* low lane만 새 값으로 교체하고 상위 3개 lane은 그대로 유지 */
#include <immintrin.h>

static inline __m128 replace_lane0_f32(__m128 acc, const float *tail)
{
    __m128 scalar = _mm_load_ss(tail);   /* MOVSS/VMOVSS mem → xmm */
    return _mm_move_ss(acc, scalar);      /* acc[127:32] 유지, low32만 교체 */
}

static inline double load_store_scalar_f64(const double *src)
{
    __m128d v = _mm_load_sd(src);        /* MOVSD/VMOVSD mem → xmm */
    double out;
    _mm_store_sd(&out, v);               /* low64만 저장 */
    return out;
}

vmovss와 vbroadcastss의 차이: vmovss는 low lane 한 개만 이동하거나 merge합니다. 모든 lane에 동일 값을 채우려면 vbroadcastss를 사용해야 합니다. 커널 tail 처리에서는 보통 vmovss, packed 루프 진입 전 스칼라 상수 준비에는 vbroadcastss가 더 적합합니다.

/* broadcast + FMA 닷프로덕트: 4×4 float 행렬 곱 한 행 */
#include <immintrin.h>

static inline __m128 dot_row_4x4(
    const float *row_a,   /* 1×4 행 벡터  */
    const float *mat_b)   /* 4×4 열-우선 행렬 */
{
    __m128 col0 = _mm_load_ps(mat_b);       /* B 열 0 */
    __m128 col1 = _mm_load_ps(mat_b + 4);   /* B 열 1 */
    __m128 col2 = _mm_load_ps(mat_b + 8);
    __m128 col3 = _mm_load_ps(mat_b + 12);

    /* a[i]를 broadcast 후 FMA 누적 */
    __m128 sum = _mm_mul_ps(_mm_set1_ps(row_a[0]), col0);
    sum = _mm_fmadd_ps(_mm_set1_ps(row_a[1]), col1, sum);
    sum = _mm_fmadd_ps(_mm_set1_ps(row_a[2]), col2, sum);
    sum = _mm_fmadd_ps(_mm_set1_ps(row_a[3]), col3, sum);
    return sum;  /* 결과 행 [C00,C01,C02,C03] */
}
/* NT store 대용량 memcpy 패턴 — LLC 크기 초과 복사에 유리 */
void nt_memcpy_avx(void *dst, const void *src, size_t n)
{
    const char *s = src;
    char *d = dst;
    for (size_t i = 0; i < n; i += 64) {
        __m256 v0 = _mm256_load_ps((float*)(s + i));
        __m256 v1 = _mm256_load_ps((float*)(s + i + 32));
        _mm256_stream_ps((float*)(d + i),      v0);  /* VMOVNTPS */
        _mm256_stream_ps((float*)(d + i + 32), v1);
    }
    _mm_sfence();  /* NT store 가시성 보장 */
}

x86 정수 산술 명령어 상세

SIMD 정수 산술은 이미지/비디오 코덱, 양자화, 해싱 등에서 핵심입니다. 포화 연산과 확장 곱셈-누적(PMADDWD)은 고정소수점 DSP 패턴의 기반이며, VNNI는 INT8 추론 가속의 핵심입니다.

VPADDD ymm1, ymm2, ymm3 — 8레인 i32 병렬 덧셈 8개 레인(i = 7~0)이 동시에 독립 덧셈 — C[i] = A[i] + B[i] ymm2 (A) A[7]=10 A[6]=20 A[5]=30 A[4]=40 A[3]=50 A[2]=60 A[1]=70 A[0]=80 + + + + + + + + ymm3 (B) B[7]=1 B[6]=2 B[5]=3 B[4]=4 B[3]=5 B[2]=6 B[1]=7 B[0]=8 ymm1 (C) 11 22 33 44 55 66 77 88 각 레인 독립: 캐리 없음, 오버플로 무시 (wraparound). 포화 덧셈은 i8/i16 전용 — VPADDSB/VPADDSW, VPADDUSB/VPADDUSW. VPADDSW: 32767+1=32767 (포화, 클램프) VPADDW: 32767+1=-32768 (랩어라운드) PMADDWD — i16×i16 쌍별 곱셈 후 i32 합산 C[i] = A[2i]×B[2i] + A[2i+1]×B[2i+1] (8×i16 → 4×i32) A (8×i16) a7 a6 a5 a4 a3 a2 a1 a0 B (8×i16) b7 b6 b5 b4 b3 b2 b1 b0 a7×b7 + a6×b6 a5×b5 + a4×b4 a3×b3 + a2×b2 a1×b1 + a0×b0 C (4×i32) C[3] = a7b7+a6b6 C[2] = a5b5+a4b4 C[1] = a3b3+a2b2 C[0] = a1b1+a0b0 용도: 오디오 FIR 필터, 이미지 컨볼루션, 닷프로덕트 — VNNI(VPDPBUSD)는 u8×i8→i32 4쌍 누적으로 확장
명령어동작데이터 폭용도
PADDB/W/D/Q레인별 덧셈 (랩어라운드)i8/i16/i32/i64일반 정수 연산
PADDSB/SW부호있는 포화 덧셈i8/i16오디오, 이미지 클램핑
PADDUSB/USW부호없는 포화 덧셈u8/u16픽셀 합산
PMULLWi16×i16→i16 (하위 16비트)i16소규모 곱셈
PMULHWi16×i16→i16 (상위 16비트)i16고정소수점 곱셈
PMULLDi32×i32→i32 (하위 32비트)i32일반 정수 곱셈
PMADDWDi16×i16→i32 쌍별 합i16→i32FIR, 컨볼루션, 닷프로덕트
PMADDUBSWu8×i8→i16 쌍별 합u8×i8→i16INT8 양자화 곱셈
PSADBW|a-b| 절대차 합 (8바이트)u8→u64모션 추정 SAD
PAVGB/W(a+b+1)>>1 평균u8/u16이미지 보간
VPDPBUSDu8×i8→i32 4쌍 누적u8×i8→i32VNNI INT8 추론
/* PSADBW 기반 8×8 블록 SAD — 비디오 코덱 모션 추정 핵심 */
#include <immintrin.h>

uint32_t sad_8x8_sse2(const uint8_t *blk1, int stride1,
                       const uint8_t *blk2, int stride2)
{
    __m128i sum = _mm_setzero_si128();
    for (int y = 0; y < 8; y++) {
        __m128i a = _mm_loadl_epi64((__m128i*)(blk1 + y * stride1));
        __m128i b = _mm_loadl_epi64((__m128i*)(blk2 + y * stride2));
        sum = _mm_add_epi64(sum, _mm_sad_epu8(a, b));  /* |a-b| 합 */
    }
    return (uint32_t)_mm_cvtsi128_si32(sum);
}

/* VPDPBUSD (VNNI) INT8 행렬 곱 누적 — 4×u8 · 4×i8 → i32 */
__m256i int8_matmul_vnni(const uint8_t *A, const int8_t *B, int K)
{
    __m256i acc = _mm256_setzero_si256();
    for (int k = 0; k < K; k += 4) {
        __m256i a = _mm256_set1_epi32(*(uint32_t*)(A + k));  /* broadcast 4B */
        __m256i b = _mm256_loadu_si256((__m256i*)(B + k * 8));
        acc = _mm256_dpbusd_epi32(acc, a, b);  /* u8·i8 누적 */
    }
    return acc;
}

x86 시프트·회전 명령어 상세

SIMD 시프트는 스칼라 SHL/SHR/SAR/ROL/ROR의 벡터 버전으로, 비트필드 추출·삽입, 엔디안 변환, 고정소수점 정규화, 해시·암호 혼합(mixing) 단계에서 필수입니다. 핵심 구분은 세 가지입니다. 첫째, 고정량 시프트(PSLL*, PSRL*, PSRA*)는 모든 레인에 같은 시프트량을 imm8 또는 xmm 카운트로 적용하고, 둘째, 가변량 시프트(VPSLLV*, VPSRLV*, VPSRAV*)는 레인마다 다른 시프트량을 두 번째 소스에서 읽습니다. 셋째, 회전(VPROLD/Q, VPRORD/Q, AVX-512)은 밀려 나간 비트를 반대쪽 끝으로 순환시킵니다. 산술 우시프트(PSRA*)만 부호 비트를 복제하고, 논리 시프트는 0을 채우는 차이도 반드시 기억해야 합니다.

SIMD 시프트 3형식 — 고정량 vs 가변량 vs 산술·회전 PSLLD xmm1, xmm2, imm8 — 4레인 동일 시프트량 xmm2 00000003 0000000F 0000001F 0000003F ‹‹ 2 0000000C 0000003C 0000007C 000000FC ← 밀려난 상위 비트는 폐기, 하위는 0 채움 (논리 시프트) VPSLLVD xmm1, xmm2, xmm3 — 레인별 가변량 xmm2 03 0F 1F 3F xmm3 ‹‹1 ‹‹2 ‹‹3 ‹‹4 06 3C F8 3F0 ← 각 레인의 시프트량을 xmm3에서 읽음 (1, 2, 3, 4비트) VPSRAD xmm1, xmm2, imm8 — 산술 우시프트 (부호 복제) 80000000 (음수, MSB=1) 40000000 (양수, MSB=0) ›› 1 C0000000 (상위에 1 복제) 20000000 (상위에 0 채움) 부호 비트(MSB)를 복제해 음수 값을 유지 — 논리 우시프트(0 채움)와의 차이 VPROLD — 순환 회전 (AVX-512) 1단위(8비트) 회전 A B C D B C D A 32비트 레인을 4단위로 추상화 — 밀려난 단위가 반대편 끝으로 순환 (비트 손실 없음) VPRORD·VPRORVD — 우회전·레인별 가변 회전도 같은 원리 PSLLDQ/PSRLDQ — 128비트 레인을 바이트 단위로 밀기 (PALIGNR와 함께 버퍼 정렬·스트리밍 파서에서 필수)
명령어동작시프트량확장실전 용도
PSLLW/D/Q논리 좌시프트 (packed)imm8 또는 xmmMMX~SSE2비트필드 배치, 해시 혼합
PSRLW/D/Q논리 우시프트 (상위 0)imm8 또는 xmmMMX~SSE2필드 추출, 언패킹
PSRAW/D산술 우시프트 (부호 복제)imm8 또는 xmmMMX~SSE2고정소수점 스케일링, 부호 보존
PSLLDQ / PSRLDQ128비트 레인 바이트 단위 밀기imm8 (바이트 수)SSE2부분 캐시라인 정렬, 스트림 파싱
VPSLLVD/Q, VPSRLVD/Q레인별 가변량 논리 시프트벡터 (레인마다)AVX2가변 폭 비트필드, 압축 해제
VPSRAVD레인별 가변량 산술 우시프트벡터 (레인마다)AVX2부호 있는 고정소수점 정규화
VPSLLVW / VPSRLVW16비트 레인 가변 시프트벡터 (레인마다)AVX-512BW오디오 샘플별 이득 정규화
VPROLD/Q, VPRORD/Q32/64비트 순환 회전imm8 또는 벡터AVX-512F암호 혼합(예: SHA 계열), 해시
VPRORVD/Q레인별 가변 회전벡터AVX-512F라운드 키 의존 회전
/* 32비트 비트필드 압축 — 고정량 시프트 + OR 조합 */
#include <immintrin.h>

/* [31:24]a, [23:16]b, [15:8]c, [7:0]d → 4개 필드를 서로 다른 오프셋으로 배치 */
__m128i pack_fields_fixed(__m128i a, __m128i b, __m128i c, __m128i d)
{
    __m128i t = _mm_slli_epi32(a, 24);   /* a를 상위 8비트로 */
    t = _mm_or_si128(t, _mm_slli_epi32(b, 16));
    t = _mm_or_si128(t, _mm_slli_epi32(c, 8));
    return _mm_or_si128(t, d);
}

/* 레인별 가변 시프트 — 8비트 필드를 각 레인마다 다른 양만큼 이동 */
__m256i shift_each_lane(__m256i bits, __m256i counts)
{
    /* counts: 레인마다 0~31 사이 시프트량. 31 초과는 결과 0 (하드웨어 정의) */
    return _mm256_sllv_epi32(bits, counts);  /* VPSLLVD */
}

/* 산술 우시프트로 부호 유지 — 16비트 고정소수점 Q15를 12비트로 절단 */
__m128i q15_to_q11(__m128i v)
{
    return _mm_srai_epi16(v, 4);  /* PSRAW — 음수도 부호 유지 */
}

가변 시프트는 분기 제거의 핵심입니다: VPSLLVD는 레인마다 다른 시프트량을 한 번에 적용하므로, 기존 스칼라 shl 루프나 시프트량 테이블 점프를 벡터화할 수 있습니다. 단, 시프트량이 요소 폭 이상이면 결과가 0이 되는 하드웨어 규칙(산술 우시프트 포함)을 반드시 염두에 두어야 합니다.

x86 min/max·절댓값·부호·평균 명령어 상세

레인별 최소/최대 선택은 분기를 제거하고 임계값 클램프(clamp)를 구현하는 가장 기본적인 SIMD 기법입니다. 특히 min(max(x, lo), hi) 체인은 한 번의 VMINPS와 한 번의 VMAXPS로 오디오 리미터, 색상 보정, 센서 캘리브레이션의 포화 범위를 구현합니다. 여기에 절댓값(PABS*), 부호 적용(PSIGN*), 평균(PAVG*)을 더하면 이미지/오디오 DSP의 대부분의 비선형 처리가 분기 없이 완성됩니다. 주의할 점은 부호 있는/부호 없는 min/max 명령이 따로 존재한다는 것입니다. 예를 들어 PMINSB(i8)와 PMINUB(u8)는 완전히 다른 하드웨어 연산이며, 혼동하면 예상과 반대의 클램프 결과가 나옵니다.

변환 없이 구현하는 클램프·절댓값·부호 적용 ① 클램프 — min(max(x, lo), hi) 분기 없이 입력 1단계 하한 (VPMAX*) 2단계 상한 (VPMIN*) x=300 (u32) VPMAXUD(x, 0) = 300 VPMINUD(·, 255) = 255 x=-40 (i32) VPMAXSD(x, 0) = 0 VPMINSD(·, 255) = 0 부호 없는 값에 S 변형을 쓰면 최상위 비트가 음수로 읽혀 클램프가 뒤집힘 — U/S 선택 ② PABS* / PSIGN* — 절댓값·부호 적용 PABSD — 레인별 절댓값 -7 → +7 +7 → +7 (불변) PSIGND — b의 부호를 a에 적용 a=5, b=-1 → -5 a=5, b=+1 → +5 a=5, b=0 → 0 부호 반전·보존·영점화를 b 하나로 분기 없이 처리 (오디오 페이드, 차분 부호) ③ PAVGB — 반올림 평균 (a+b+1) >> 1 a=200, b=61 a+b+1 = 262 (a+b+1)>>1 = 131 단순 (a+b)>>1 = 130 (0.5 내림, 치우침) — PAVG는 +1 후 시프트로 131 (반올림) ④ FP 클램프 — VMAXPS + VMINPS 체인 VMAXPS(x, lo) — 하한 VMINPS(·, hi) — 상한 오디오 리미터·색상 보정·센서 캘리브레이션 — FP도 min(max(x, lo), hi) 체인 그대로
x86 SIMD min/max·절댓값·부호·평균 명령으로 구현하는 분기 없는 비선형 처리 — ① 부호 없는 u32와 부호 있는 i32에 같은 min(max(x, lo), hi) 체인을 적용한 예와, 부호 변형을 잘못 고르면 클램프가 뒤집힌다는 함정. ② PABSD의 절댓값과, PSIGND가 b의 부호로 부호 반전·보존·영점화를 하는 모습. ③ PAVGB가 +1 후 시프트로 반올림 평균을 내는 과정. ④ 같은 체인을 부동소수점에 적용한 VMAXPS·VMINPS. 명령어 수준의 개념 개요이며, 각 명령의 확장(SSE2·SSSE3·AVX-512)과 실전 용도는 아래 표에서 다룹니다.
명령어동작부호확장실전 용도
PMINUB / PMINUW / PMINUD레인별 최소 (부호 없음)u8/u16/u32SSE2 / SSE4.1 / AVX-512픽셀, 색상 채널 클램프
PMINSB / PMINSW / PMINSD레인별 최소 (부호 있음)i8/i16/i32SSE4.1오디오, 센서 음수 범위
PMAXUB / PMAXUW / PMAXUD레인별 최대 (부호 없음)u8/u16/u32SSE2 / SSE4.1 / AVX-512하한 클램프, 로컬 최대 필터
PMAXSB / PMAXSW / PMAXSD레인별 최대 (부호 있음)i8/i16/i32SSE4.1볼륨 리미터, 피크 검출
VMINPS / VMINPDpacked float/double 최소fp32/fp64SSE/AVX수치 안정화, 경계 선택
VMAXPS / VMAXPDpacked float/double 최대fp32/fp64SSE/AVX최댓값 필터, 정규화
VMINSS/SD, VMAXSS/SDscalar 최소/최대 (low lane)fp32/fp64SSE/AVX스칼라 경계 검사 대체
PABSB/W/D레인별 절댓값i8/i16/i32SSSE3오차 계산(SAD), 스펙트럼 분석
PSIGNB/W/Db의 부호를 a에 적용 (0이면 0)i8/i16/i32SSSE3부호 반전, 차동 부호화
PAVGB / PAVGW(a+b+1)>>1 반올림 평균u8/u16SSE이미지 축소, 블러, 보간
/* 16개 i8 샘플을 [-100, 100] 범위로 클램프 — 분기 없음 */
#include <immintrin.h>

__m128i clamp_i8_x16(__m128i v)
{
    __m128i lo = _mm_set1_epi8(-100);
    __m128i hi = _mm_set1_epi8(100);
    v = _mm_max_epi8(v, lo);   /* PMAXSB: 하한 */
    return _mm_min_epi8(v, hi);  /* PMINSB: 상한 */
}

/* 절댓값 합 (SAD) — PSADBW와의 차이: 레인별 절댓값 후 누적 */
__m128i abs_sum_epi16(__m128i a, __m128i b)
{
    __m128i diff = _mm_sub_epi16(a, b);
    __m128i absd = _mm_abs_epi16(diff);       /* PABSW */
    __m128i zeros = _mm_setzero_si128();
    return _mm_sad_epu8(_mm_packus_epi16(absd, zeros), zeros);
}

/* 부호 적용 — diff의 부호를 기반으로 gain 부호 전환 */
__m128i apply_sign(__m128i value, __m128i sign)
{
    return _mm_sign_epi8(value, sign);  /* PSIGNB: sign>0 보존, sign<0 반전, sign=0 영점화 */
}

부호 변형 선택 실수가 가장 흔한 버그입니다: unsigned 데이터(예: u8 픽셀)에 PMAXSD/PMINSD를 쓰면 0x80~0xFF 값이 음수로 해석되어 클램프가 완전히 뒤집힙니다. 커널 이미지/오디오 코드에서는 요소 타입과 일치하는 U/S 변형을 반드시 확인해야 합니다.

x86 부동소수점 SIMD 명령어 상세

FMA(Fused Multiply-Add)는 현대 SIMD 연산의 핵심입니다. 단일 반올림으로 a×b+c를 계산해 정밀도와 처리량 모두 향상시킵니다. 반면 VDIVPS/VSQRTPS는 높은 지연시간으로 병목이 되므로, 근사 역수 + Newton-Raphson 반복이 실전 패턴입니다.

Scalar FP 연산 — ADDSS/MULSS/VDIVSS/VFMADDSS

ADDSS/ADDSD와 VADDSS/VADDSD 계열은 SIMD 레지스터를 사용하지만 계산은 low lane 하나에만 적용합니다. packed 연산으로 올리기 애매한 마지막 잔여 원소, 보상합, 단일 누산기 갱신, vector 루프 뒤의 정리(cleanup) 단계에 적합합니다. 특히 VEX scalar 연산은 결과 low lane만 새로 계산하고 나머지 low 128비트는 merge source에서 복사하므로, 이미 계산된 누산기 상위 lane을 보존하면서 low lane만 갱신하기 쉽습니다.

명령어packed 대응low lane 동작upper lane 정책실전 용도
ADDSS / VADDSSADDPS / VADDPSfloat 1개 덧셈legacy는 dst 유지, VEX는 merge source 복사잔여 원소 누적, 스칼라 보정
MULSS / VMULSSMULPS / VMULPSfloat 1개 곱셈legacy는 dst 유지, VEX는 merge source 복사gain 적용, 단일 샘플 처리
DIVSS / VDIVSSDIVPS / VDIVPSfloat 1개 나눗셈상위 lane 유지 또는 merge정확도 우선 scalar 경로
SQRTSS / VSQRTSSSQRTPS / VSQRTPSfloat 1개 제곱근상위 lane 유지 또는 merge길이 계산, 정규화 tail
VFMADDSS / VFMADDSDVFMADDPS / VFMADDPDlow lane FMAmerge source의 상위 lane 복사닷프로덕트 tail, Horner 다항식
MAXSS/MINSSMAXPS/MINPSfloat 1개 비교 선택상위 lane 유지 또는 merge경계값 clamp, scalar fallback
/* vector 루프 뒤 1개 남은 float를 scalar FMA로 처리 */
#include <immintrin.h>

static inline float tail_fma_ss(const float *x,
                                 const float *y,
                                 float acc)
{
    __m128 vx = _mm_load_ss(x);
    __m128 vy = _mm_load_ss(y);
    __m128 vacc = _mm_set_ss(acc);

    vacc = _mm_fmadd_ss(vx, vy, vacc);  /* low lane만 vx*vy+acc */
    return _mm_cvtss_f32(vacc);
}

/* 커널 inline asm 스타일 scalar update */
static inline float tail_fma_ss_asm(const float *x,
                                     const float *y,
                                     const float *acc)
{
    float out;

    asm volatile(
        "vmovss   (%1), %%xmm1\n\t"         /* low32 = *x, 상위는 0 */
        "vmovss   (%2), %%xmm2\n\t"         /* low32 = *y */
        "vmovss   (%3), %%xmm0\n\t"         /* low32 = acc */
        "vfmadd231ss %%xmm2, %%xmm1, %%xmm0\n\t"
        "vmovss   %%xmm0, %0\n\t"
        : "=m"(out)
        : "r"(x), "r"(y), "r"(acc)
        : "memory", "xmm0", "xmm1", "xmm2");

    return out;
}

scalar FP도 FPU 상태를 사용합니다: vmovss, vaddss, vfmaddss는 packed 명령보다 폭이 좁을 뿐, 커널 입장에서는 동일한 XMM/FPU 상태를 만집니다. 따라서 커널 내부에서 사용할 때는 kernel_fpu_begin()/kernel_fpu_end() 규칙과 preemption 제약을 그대로 적용해야 합니다.

FMA vs MUL+ADD — 반올림 횟수·지연·처리량 비교 FMA — VFMADD231PS (명령 1개) src1 (a) src2 (b) a×b 계산 (정확) 반올림 없음 — 무한 정밀도 중간값 dst (c) a×b + c — 반올림 1회 정확한 곱을 더한 뒤 한 번만 반올림 결과 — 반올림 1회 곱셈 오차 없음 (단일 반올림) 지연 4c · 처리량 0.5 CPI 명령 1개 (VFMADD231PS) MUL + ADD — VMULPS + VADDPS (명령 2개) src1 (a) src2 (b) round(a×b) — 1차 반올림 곱셈 결과를 float로 맞추며 오차 발생 dst (c) round(ab) + c — 2차 반올림 1차 오차가 누적된 값에 c를 더해 다시 반올림 결과 — 반올림 2회 1차 + 2차 오차 (이중 반올림) 지연 4+4=8c · 처리량 1.0 CPI 명령 2개 (VMULPS + VADDPS) 곱셈 반올림 오차가 남지 않아 Horner 다항식·닷프로덕트·보상합에서 FMA가 유리 지연·처리량 수치는 Intel Skylake 기준 참고값(uops.info) — 마이크로아키텍처에 따라 다릅니다 DIV/SQRT vs 근사 역수 — 파이프라인 점유 비교 수치: Intel Skylake 기준 참고값(uops.info) — 마이크로아키텍처에 따라 다릅니다 c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 div ① div ② div ③ div ④ div ⑤ 결과 c11 sqrt ① sqrt ② sqrt ③ sqrt ④ sqrt ⑤ sqrt ⑥ 결과 c12 rcpps mul fnmadd fmadd VDIVPS (256b) 11c 지연 · 5 CPI VSQRTPS (256b) 12c 지연 · 6 CPI VRCPPS + NR (근사 역수) 12-bit → NR 1회 → ~23-bit 4 µop · FMA 포트 divider 비점유 VRCPPS: 12-bit 정밀도(상대오차 < 1.5×2⁻¹²), NR 1회 반복 → ~23-bit(float 유효숫자 충분) VRCP14PS(AVX-512): 14-bit 초기 근사 · double 정밀도가 필요하면 VDIVPD 사용 처리량 비교 (낮을수록 빠름): 5 CPI VDIVPS — divider 5사이클 병목 ~1.3 CPI RCPPS+NR — 4 µop FMA 파이프라인 VDIVPS/VSQRTPS는 divider 유닛(SKL p0)을 공유합니다. 점유 사이클 동안 같은 유닛을 쓰는 명령은 대기하며, NR 경로는 FMA 포트(p0/p1)를 사용해 divider를 거치지 않습니다.
명령어동작지연(c)처리량(CPI)비고
VADDPS/VSUBPS레인별 덧셈/뺄셈40.52포트 동시 발행
VMULPS레인별 곱셈40.5FMA 포트 공유
VFMADD231PSa×b+c (단일 반올림)40.5FMA3, Haswell+
VDIVPS (256b)레인별 나눗셈115divider 독점
VSQRTPS (256b)레인별 제곱근126divider 독점
VRCPPS근사 역수 (12-bit)41NR 반복 필요
VRSQRTPS근사 역제곱근 (12-bit)41NR 반복 필요
VMAXPS/VMINPS레인별 최대/최소40.5NaN 전파 주의
VROUNDPS반올림 모드 선택82floor/ceil/trunc
/* Newton-Raphson 1/sqrt(x) — 게임 물리, 정규화에서 사용 */
#include <immintrin.h>

static inline __m256 fast_rsqrt_nr(__m256 x)
{
    __m256 half  = _mm256_set1_ps(0.5f);
    __m256 three = _mm256_set1_ps(3.0f);
    __m256 y0    = _mm256_rsqrt_ps(x);              /* 12-bit 초기 근사 */

    /* NR: y1 = 0.5 * y0 * (3 - x * y0 * y0) */
    __m256 xy0   = _mm256_mul_ps(x, y0);
    __m256 xy0y0 = _mm256_mul_ps(xy0, y0);
    __m256 diff  = _mm256_sub_ps(three, xy0y0);
    __m256 y1    = _mm256_mul_ps(_mm256_mul_ps(half, y0), diff);
    return y1;  /* ~23-bit 정밀도 (float 유효숫자 충분) */
}

/* Horner 다항식 FMA 체인 — sin(x) 근사 (5차) */
static inline __m256 horner_sin_approx(__m256 x)
{
    /* sin(x) ≈ x(1 + c3·x² + c5·x⁴) = x·P(x²)
     * Horner: P(x²) = c5·x² + c3, then ×x² +1, then ×x */
    const __m256 c3 = _mm256_set1_ps(-0.16666667f);  /* -1/3! */
    const __m256 c5 = _mm256_set1_ps(0.00833333f);   /* +1/5! */
    const __m256 one = _mm256_set1_ps(1.0f);

    __m256 x2 = _mm256_mul_ps(x, x);
    __m256 p  = _mm256_fmadd_ps(c5, x2, c3);        /* c5·x²+c3 */
    p = _mm256_fmadd_ps(p, x2, one);                 /* p·x²+1 */
    return _mm256_mul_ps(p, x);                      /* p·x */
}

FMA 변형 선택 주의: VFMADD132/213/231PS는 오퍼랜드 순서만 다릅니다. 132는 src1×src3+src2, 213은 src2×src1+src3, 231은 src2×src3+src1입니다. 컴파일러가 레지스터 할당에 따라 자동 선택하므로, intrinsic 사용 시 _mm256_fmadd_ps(a,b,c)로 통일하면 됩니다.

x86 셔플/퍼뮤트 명령어 시각화

셔플과 퍼뮤트는 SIMD 프로그래밍에서 가장 복잡하면서도 가장 강력한 범주입니다. 레인 내/간 데이터 재배치(Relocation)는 AoS↔SoA 변환, 전치, 엔디안 변환의 핵심이며, 명령어마다 고유한 선택 패턴을 가집니다.

PSHUFB (SSSE3) — 바이트 단위 임의 셔플 Source (xmm1) — 재배치할 16바이트 (숫자 = 위치) A B C D E F G H I J K L M N O P 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Index (xmm2) — 인덱스/마스크 (하위 4비트 = src 위치, MSB=1 → 0) 3 2 1 0 7 6 5 4 0x80 0x80 0x80 0x80 0x80 0x80 0x80 0x80 D C B A H G F E 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 Result (dst) — 재배치된 바이트 규칙: result[i] = (idx[i] & 0x80) ? 0 : src[idx[i] & 0x0F] — 하위 4비트가 src 위치, MSB=1이면 0 출력 예시: 앞 8바이트 idx = 3,2,1,0,7,6,5,4 → 두 dword의 바이트 역순(엔디안 변환), 뒤 8바이트 idx = 0x80 → 결과 0x00 VPSHUFB (AVX2): 256비트로 확장되지만 128비트 레인마다 독립 셔플 — 레인 경계를 넘지 못함 활용: 엔디안 변환 · LUT 16엔트리 테이블 룩업 · base64 인코딩 · 비트필드 추출 PSHUFD — 8비트 즉시값으로 4×dword 선택 imm8 = 0x1B = 0b_00_01_10_11 비트 [7:6] 00 비트 [5:4] 01 비트 [3:2] 10 비트 [1:0] 11 dst[3] = src[0] dst[2] = src[1] dst[1] = src[2] dst[0] = src[3] 소스 xmm2/m128 — src[3] · src[2] · src[1] · src[0] (왼쪽이 최상위 dword) 규칙: dst[j] = src[ imm8[2j+1 : 2j] ] — 2비트 필드가 소스 dword 인덱스(0~3)를 정한다 매크로: _MM_SHUFFLE(d,c,b,a) = (d<<6) | (c<<4) | (b<<2) | a 다른 예: 0xE4 = _MM_SHUFFLE(3,2,1,0) → 항등 순열 · 0x00 → src[0] 브로드캐스트 AVX2 레인 장벽과 크로스레인 퍼뮤트 ① 레인 장벽 — AVX2의 YMM은 128비트 레인 2개로 나뉩니다 레인 장벽 상위 레인 [255:128] 하위 레인 [127:0] YMM (256비트) 레인 내 셔플은 자기 레인 안에서만 재배열합니다 — 레인 사이 이동에는 별도 명령이 필요합니다. ② VPERM2I128 — 128비트 레인 단위로 4개 중 2개를 고릅니다 소스 레인 풀 (ymm2 · ymm3) 결과 ymm1 (고른 레인 2개) 0 — ymm2[127:0] 1 — ymm2[255:128] 2 — ymm3[127:0] 3 — ymm3[255:128] DEST[255:128] ← imm8[5:4] DEST[127:0] ← imm8[1:0] imm8[5:4] = 1 imm8[1:0] = 2 imm8[3]=1이면 DEST[127:0]을, imm8[7]=1이면 DEST[255:128]을 0으로 채웁니다. ③ VPERMD — dword 단위 크로스레인 (ymm2 = 인덱스, ymm3 = 원본) — 왼쪽이 dword 7 ymm2 (인덱스) ymm3 (원본) ymm1 (결과) 5 0 7 3 4 1 6 2 d7 d6 d5 d4 d3 d2 d1 d0 d5 d0 d7 d3 d4 d1 d6 d2 같은 레인에서 온 값 다른 레인에서 온 값 (레인 장벽 통과) 인덱스는 각 dword의 하위 3비트(0~7)만 사용하므로, 원본의 어느 dword든 레인 경계와 무관하게 고를 수 있습니다. VPERMQ의 VEX(AVX2) 형식은 레지스터 인덱스 대신 imm8을 쓰며, qword마다 2비트로 원본을 고릅니다.
명령어단위크로스레인제어용도
PSHUFBbyteX (레인 내)레지스터(16인덱스)엔디안 변환, LUT
PSHUFDdwordXimm8 (2bit×4)broadcast, 역순
PSHUFHW/LWwordX (상위/하위 half)imm816비트 셔플
PALIGNRbyteXimm8 (shift량)바이트 시프트 결합
VPERMILPSfloatX (레인 내)imm8/reg레인 내 float 셔플
VPERMD/Qdword/qwordO레지스터(8인덱스)크로스레인 재배치
VPERM2I128128b 레인Oimm8레인 교환/복제
VPERMBbyteO레지스터AVX-512VBMI, 전체 바이트 퍼뮤트
/* PSHUFB 엔디안 변환 — 네트워크 바이트 순서 ↔ 호스트 순서 */
#include <immintrin.h>

static inline __m128i bswap32_x4(__m128i v)
{
    /* 각 4바이트 dword 내에서 바이트 역순 */
    const __m128i mask = _mm_set_epi8(
        12,13,14,15,  8, 9,10,11,  4, 5, 6, 7,  0, 1, 2, 3);
    return _mm_shuffle_epi8(v, mask);  /* PSHUFB */
}

/* 4×4 float 행렬 전치 — AoS→SoA 변환 핵심 */
void transpose_4x4_ps(__m128 r0, __m128 r1, __m128 r2, __m128 r3,
                       __m128 *c0, __m128 *c1, __m128 *c2, __m128 *c3)
{
    /* 2단계 unpack으로 전치
     * r0 = [a0 a1 a2 a3], r1 = [b0 b1 b2 b3], ...
     * 결과: c0 = [a0 b0 c0 d0], c1 = [a1 b1 c1 d1], ... */
    __m128 t0 = _mm_unpacklo_ps(r0, r1);  /* [a0 b0 a1 b1] */
    __m128 t1 = _mm_unpackhi_ps(r0, r1);  /* [a2 b2 a3 b3] */
    __m128 t2 = _mm_unpacklo_ps(r2, r3);  /* [c0 d0 c1 d1] */
    __m128 t3 = _mm_unpackhi_ps(r2, r3);  /* [c2 d2 c3 d3] */

    *c0 = _mm_movelh_ps(t0, t2);  /* [a0 b0 c0 d0] */
    *c1 = _mm_movehl_ps(t2, t0);  /* [a1 b1 c1 d1] */
    *c2 = _mm_movelh_ps(t1, t3);  /* [a2 b2 c2 d2] */
    *c3 = _mm_movehl_ps(t3, t1);  /* [a3 b3 c3 d3] */
}

AVX2 레인 장벽 우회 전략: 대부분의 AVX2 셔플(VPSHUFB, VPSHUFD 등)은 128비트 레인 내에서만 동작합니다. 크로스레인이 필요하면: (1) VPERMD/VPERMQ로 dword/qword 단위 재배치, (2) VPERM2I128로 레인 교환 후 in-lane 셔플 조합, (3) AVX-512 VPERMB/VPERMI2B로 전체 바이트 퍼뮤트. 크로스레인은 레인 내 셔플보다 지연 시간이 길며 그 차이는 마이크로아키텍처(Microarchitecture)에 따라 달라지므로, 가능하면 레인 내 셔플로 해결합니다.

x86 패킹/언패킹/확장 명령어

데이터 폭 변환은 이미지/비디오 파이프라인의 핵심입니다. uint8 픽셀을 float로 올려 연산하고, 결과를 다시 uint8로 내려야 합니다. 인터리브(PUNPCK)와 패킹(PACK) 명령어가 이 "깔때기"를 형성합니다.

PUNPCKLBW xmm1, xmm2 — 하위 8바이트 교차 인터리브 xmm1 (A) — 하위 8바이트(a7~a0)만 사용 · 점선 칸(a15~a8)은 결과에 쓰이지 않음 a15 a14 a13 a12 a11 a10 a9 a8 a7 a6 a5 a4 a3 a2 a1 a0 xmm2 (B) — 하위 8바이트(b7~b0)만 사용 b15 b14 b13 b12 b11 b10 b9 b8 b7 b6 b5 b4 b3 b2 b1 b0 PUNPCKLBW xmm1, xmm2 b7 a7 b6 a6 b5 a5 b4 a4 b3 a3 b2 a2 b1 a1 b0 a0 결과 (xmm1) — 16바이트가 모두 채워지고, A·B의 상위 8바이트는 결과에 쓰이지 않음 PUNPCKHBW는 상위 8바이트(a15~a8, b15~b8)를 같은 규칙으로 인터리브 · xmm2 = 0이면 u8→u16 제로확장
PUNPCKLBW xmm1, xmm2의 동작 — 두 피연산자 xmm1(A)·xmm2(B)에서 하위 8바이트만 꺼내 LSB부터 a0, b0, a1, b1 … a7, b7 순으로 번갈아 놓아 16바이트 결과를 만듭니다. 왼쪽이 상위 바이트이고, 점선으로 표시한 상위 8바이트(a15~a8, b15~b8)는 결과에 쓰이지 않습니다. 명령어 수준의 개념 개요입니다.
폭 변환 "깔때기" — 확장과 축소 확장 경로 (Zero-Extend) — 요소 폭 증가 16×u8 (128비트) VPMOVZXBW 하위 8바이트 → 8×u16 8×u16 (128비트) VPMOVZXWD 하위 4워드 → 4×u32 4×u32 (128비트) VCVTDQ2PS 4×u32 → 4×float 4×float (128비트) 축소 경로 (Pack + Saturate) — 요소 폭 감소 4×float (128비트) VCVTPS2DQ 4×float → 4×i32 4×i32 (128비트) PACKSSDW 두 레지스터의 i32 → 8×i16 8×i16 (128비트) PACKUSWB 두 레지스터의 i16 → 16×u8 16×u8 (128비트) [float 연산] 포화(Saturate) — 범위 밖의 값은 경계값으로 고정합니다. PACKSSDW: i32→i16 부호있는 포화 · PACKUSWB: i16→u8 부호없는 포화(음수→0, 255 초과→255) 전체 흐름: u8 픽셀 → VPMOVZXBW → VPMOVZXWD → VCVTDQ2PS → float 연산 → VCVTPS2DQ → PACKSSDW → PACKUSWB → u8 SSE4.1 이전: VPMOVZXBW·VPMOVZXWD 대신 PUNPCKLBW(src, 0)·PUNPCKLWD(result, 0)로 제로 확장을 대신합니다.
데이터 폭 변환 "깔때기" — 왼쪽 확장 경로는 16×u8(128비트) 레지스터에서 VPMOVZXBW(하위 8바이트→8×u16)·VPMOVZXWD(하위 4워드→4×u32)·VCVTDQ2PS를 거쳐 4×float로 요소 폭을 넓히고, 오른쪽 축소 경로는 4×float에서 VCVTPS2DQ·PACKSSDW·PACKUSWB를 거쳐 16×u8로 좁힙니다. 읽는 순서는 왼쪽 위에서 아래로 내려와 아래쪽 float 연산을 지나 오른쪽 아래에서 위로 올라오는 U자입니다. 박스 가로 폭은 요소 크기에 비례하며(1바이트 요소 176px, 2바이트 요소 230px, 4바이트 요소 340px), 요소 폭이 커지면 128비트 레지스터에 담기는 요소 수가 16→8→4→4로 줄고 작아지면 4→4→8→16으로 늘어납니다. 확장 단계는 제로 확장이라 값이 바뀌지 않고, 축소 단계의 PACKSSDW(i32→i16 부호있는 포화)·PACKUSWB(i16→u8 부호없는 포화)만 범위를 벗어난 값을 0이나 255 같은 경계값으로 고정합니다. 확장 경로는 128비트 레지스터의 하위 8바이트만 쓰고 축소 경로는 레지스터 두 개를 하나로 병합하므로, 16개 픽셀을 float으로 올렸다가 되돌리는 전체 변환에는 float 레지스터 4개가 필요합니다. 128비트 레지스터 한 개를 기준으로 한 명령어 수준의 개념 개요입니다.
명령어동작입력→출력포화
PUNPCKLBW하위 바이트 인터리브2×16B→16B—
PUNPCKHBW상위 바이트 인터리브2×16B→16B—
PUNPCKLWD하위 워드 인터리브2×8W→8W—
PACKSSWBi16→i8 축소2×8W→16B부호있는
PACKUSWBi16→u8 축소2×8W→16B부호없는
PACKSSDWi32→i16 축소2×4D→8W부호있는
PACKUSDWi32→u16 축소2×4D→8W부호없는 (SSE4.1)
VPMOVZXBWu8→u16 제로확장8B→8W—
VPMOVZXBDu8→u32 제로확장4B→4D—
VPMOVSXBWi8→i16 부호확장8B→8W—
/* uint8→float 이미지 업변환 + float→uint8 포화 다운변환 */
#include <immintrin.h>

/* 16×u8 픽셀 → 4×__m128(float) — SSE4.1 PMOVZX 체인 */
void u8_to_float_x16(const uint8_t *src, __m128 out[4])
{
    __m128i raw = _mm_loadu_si128((__m128i*)src);       /* 16×u8 */
    __m128i lo16 = _mm_cvtepu8_epi16(raw);              /* 하위 8→8×u16 */
    __m128i hi16 = _mm_cvtepu8_epi16(_mm_srli_si128(raw, 8));

    out[0] = _mm_cvtepi32_ps(_mm_cvtepu16_epi32(lo16));        /* 4×float */
    out[1] = _mm_cvtepi32_ps(_mm_cvtepu16_epi32(
                 _mm_srli_si128(lo16, 8)));
    out[2] = _mm_cvtepi32_ps(_mm_cvtepu16_epi32(hi16));
    out[3] = _mm_cvtepi32_ps(_mm_cvtepu16_epi32(
                 _mm_srli_si128(hi16, 8)));
}

/* 4×__m128(float) → 16×u8 — 포화 다운변환 */
__m128i float_to_u8_x16(__m128 f0, __m128 f1, __m128 f2, __m128 f3)
{
    __m128i i0 = _mm_cvtps_epi32(f0);                  /* 4×i32 */
    __m128i i1 = _mm_cvtps_epi32(f1);
    __m128i i2 = _mm_cvtps_epi32(f2);
    __m128i i3 = _mm_cvtps_epi32(f3);
    __m128i w01 = _mm_packs_epi32(i0, i1);             /* 8×i16 */
    __m128i w23 = _mm_packs_epi32(i2, i3);
    return _mm_packus_epi16(w01, w23);                  /* 16×u8 포화 */
}

AVX-512 마스크 연산 상세

AVX-512의 핵심 혁신은 8개 전용 마스크 레지스터(k0–k7)입니다. 모든 SIMD 명령에 프레디케이트를 부여해, 분기 없는 조건부 처리와 루프 테일 마스킹을 가능하게 합니다.

AVX-512 마스킹 — Zeroing vs Merging VADDPS zmm0{k1}, zmm1, zmm2 — 같은 마스크, 다른 dst 처리 (명령어 수준 개념) 레인 15 14 13 12 11 10 9 8 Merging zmm0{k1} dst 이전 값 필요 — false dependency 연산 결과 연산 결과 dst 기존 값 연산 결과 dst 기존 값 연산 결과 연산 결과 dst 기존 값 마스크 k1 하위 16비트 (16레인) 1 1 0 1 0 1 1 0 Zeroing zmm0{k1}{z} dst 이전 값 불필요 — 의존성 제거 연산 결과 연산 결과 0으로 클리어 연산 결과 0으로 클리어 연산 결과 연산 결과 0으로 클리어 Merging — 마스크가 0인 레인은 dst의 이전 값이 그대로 남습니다(점선 테두리). 그래서 명령이 dst를 읽어야 하므로 false dependency가 생깁니다. Zeroing — 마스크가 0인 레인을 0으로 클리어합니다. dst를 읽지 않으므로 의존성이 끊겨 OoO 실행에 유리합니다. 세 행은 같은 레인 위치를 공유하므로 마스크 k1[j]의 값이 레인 j의 결과를 결정합니다. 마스크가 1인 레인은 두 방식 모두 연산 결과(zmm1+zmm2)를 씁니다. Zeroing({z})은 목적지가 메모리일 때는 쓸 수 없습니다. 마스크 k0은 { } 안에 지정할 수 없고(무마스크로 인코딩되므로), 실제 마스킹에는 k1–k7만 씁니다. 참고 — VCOMPRESSPS·VEXPANDPS는 마스크 비트가 1인 요소만 모아 연속으로 저장하거나 펼쳐 넣는 압축·확장 명령입니다.
명령어동작용도
KMOVW k, r/m마스크 레지스터 로드/저장마스크 초기화
KANDW k, k, k마스크 AND조건 결합
KORW k, k, k마스크 OR조건 합집합
KXORW k, k, k마스크 XOR조건 토글
KNOTW k, k마스크 NOT조건 반전
KTESTW k, k마스크 테스트 (ZF/CF 설정)전체 활성/비활성 확인
KSHIFTLW k, k, imm마스크 좌측 시프트마스크 생성
KUNPCKBW k, k, k두 8비트 마스크 결합→16비트마스크 확장
VCOMPRESSPS활성 레인만 연속 저장필터링, 스트림 압축
VEXPANDPS연속 데이터→마스크 위치 로드스트림 확장
/* 분기없는 조건부 처리 — 양수만 제곱근, 음수는 0 */
#include <immintrin.h>

void sqrt_positive_only(float *data, int n)
{
    for (int i = 0; i < n; i += 16) {
        __m512 v = _mm512_loadu_ps(data + i);
        __mmask16 pos = _mm512_cmp_ps_mask(v,
                            _mm512_setzero_ps(), _CMP_GT_OQ);
        /* zeroing 마스크: 음수 레인 → 0, 양수 레인 → sqrt */
        __m512 result = _mm512_maskz_sqrt_ps(pos, v);
        _mm512_storeu_ps(data + i, result);
    }
}

/* 루프 테일 마스킹 — 배열 길이가 16의 배수가 아닐 때 */
float sum_array_avx512(const float *arr, int n)
{
    __m512 acc = _mm512_setzero_ps();
    int i = 0;
    for (; i + 16 <= n; i += 16)
        acc = _mm512_add_ps(acc, _mm512_loadu_ps(arr + i));

    /* 나머지 요소: 마스크로 유효 레인만 처리 */
    if (i < n) {
        __mmask16 tail = (__mmask16)((1u << (n - i)) - 1);
        acc = _mm512_mask_add_ps(acc, tail, acc,
                  _mm512_maskz_loadu_ps(tail, arr + i));
    }
    return _mm512_reduce_add_ps(acc);
}

/* VCOMPRESSPS — 조건 필터링 (양수만 출력) */
int filter_positive_avx512(const float *in, float *out, int n)
{
    int written = 0;
    for (int i = 0; i < n; i += 16) {
        __m512 v = _mm512_loadu_ps(in + i);
        __mmask16 pos = _mm512_cmp_ps_mask(v,
                            _mm512_setzero_ps(), _CMP_GT_OQ);
        _mm512_mask_compressstoreu_ps(out + written, pos, v);
        written += _mm_popcnt_u32((unsigned)pos);
    }
    return written;
}

마스크 성능 팁: Zeroing 마스킹({z})은 dst 레지스터 의존성을 제거해 OoO 실행에 유리합니다. Merging 마스킹은 dst의 이전 값을 읽어야 하므로 false dependency가 발생합니다. 신규 결과를 쓸 때는 zeroing, 기존 값에 조건부 갱신할 때만 merging을 사용하세요.

x86 수평 연산 및 축소

SIMD의 기본 모델은 "수직"(레인별 독립) 연산이지만, 전체 벡터를 하나의 스칼라로 축소(reduction)하거나 인접 쌍을 합산하는 "수평" 연산이 필요한 경우가 있습니다. 수평 명령은 셔플+수직 연산의 조합이므로, 처리량이 낮고 주의가 필요합니다.

HADDPS — 인접 쌍 수평 덧셈 (SSE3) HADDPS xmm1, xmm2/m128 — 각 소스의 인접한 두 레인을 합산 SRC2 · xmm2 (B) b3 b2 b1 b0 SRC1 · xmm1 (A) a3 a2 a1 a0 DEST = xmm1 (결과가 SRC1을 덮어씀) b3+b2 [127:96] b1+b0 [95:64] a3+a2 [63:32] a1+a0 [31:0] DEST 조립: 상위 64b ← SRC2(xmm2) 쌍합, 하위 64b ← SRC1(xmm1) 쌍합 — 소스 순서와 반대. VHADDPS(YMM): 128비트 레인 안에서만 수평합 — 크로스레인 합산은 별도로 필요합니다. HADDPS 결과는 두 소스가 뒤섞여 한 벡터의 리덕션이 되지 않습니다 — 리덕션에는 셔플+덧셈 캐스케이드를 사용합니다. YMM 8×float → 스칼라 리덕션 (셔플+수직 덧셈 캐스케이드) 3단계 캐스케이드 — 8레인 → 4 → 2 → 1. HADDPS(위 그림)와 달리 전체 벡터를 하나의 스칼라로 축소합니다. 원본 ymm 8×float e7 e6 e5 e4 e3 e2 e1 e0 ① vextractf128 — 상위 128비트 추출 → vaddps — 하위 128비트와 레인별 합산 8 → 4 ① xmm 4×float e7+e3 e6+e2 e5+e1 e4+e0 ② 셔플 — 상위 64비트를 하위로 이동 → vaddps — 두 절반 레인별 합산 4 → 2 ② xmm 2×float e7+e3+e5+e1 e6+e2+e4+e0 ③ 셔플 — 인접 레인 교환 → vaddss — 최종 스칼라 합 2 → 1 ③ 스칼라 float Σ = 8개 전체 합 총 3단계 캐스케이드 = 명령 6개 (추출 1 + 셔플 2 + 덧셈 3) — 지연·처리량은 아래 표 참조 AVX-512: _mm512_reduce_add_ps()도 동일한 셔플+수직 덧셈 캐스케이드 (4단계: ZMM 16레인 → 스칼라) 루프 내 리덕션은 최종 1회만 수행 — 루프 본문은 수직 누적(acc += v)으로 유지.
명령어동작µops지연(c)권장 여부
HADDPS인접 쌍 합 (2소스)36△ 느림, 셔플+add 조합 권장
HSUBPS인접 쌍 차36△
DPPS닷프로덕트 (마스크 선택)49△ SSE4.1, FMA가 빠름
DPPD2×double 닷프로덕트39△
MPSADBW다중 위치 SAD (8위치)25○ 모션 추정 특화
셔플+수직 리덕션캐스케이드 add5–712–15◎ 권장 패턴
/* YMM 8×float → 스칼라 합 (최적 리덕션 패턴) */
#include <immintrin.h>

static inline float hsum_avx(__m256 v)
{
    /* ① 상위 128b를 하위에 더함 */
    __m128 hi = _mm256_extractf128_ps(v, 1);
    __m128 lo = _mm256_castps256_ps128(v);
    __m128 sum4 = _mm_add_ps(lo, hi);       /* 4×float */

    /* ② 상위 64b를 하위에 더함 */
    __m128 shuf = _mm_movehdup_ps(sum4);     /* [s1,s1,s3,s3] */
    __m128 sum2 = _mm_add_ps(sum4, shuf);    /* 2×float */

    /* ③ 인접 교환 후 최종 합 */
    shuf = _mm_movehl_ps(shuf, sum2);
    __m128 sum1 = _mm_add_ss(sum2, shuf);
    return _mm_cvtss_f32(sum1);
}

/* 닷프로덕트 3가지 방식 비교 */

/* 방법 1: DPPS (SSE4.1) — 간단하지만 느림 */
static inline float dot_dpps(__m128 a, __m128 b) {
    return _mm_cvtss_f32(_mm_dp_ps(a, b, 0xF1));
}

/* 방법 2: MUL + HADD×2 — 직관적이지만 비효율 */
static inline float dot_hadd(__m128 a, __m128 b) {
    __m128 prod = _mm_mul_ps(a, b);
    __m128 s1 = _mm_hadd_ps(prod, prod);
    __m128 s2 = _mm_hadd_ps(s1, s1);
    return _mm_cvtss_f32(s2);
}

/* 방법 3: FMA + 셔플 리덕션 — 최적 (권장) */
static inline float dot_fma(__m128 a, __m128 b) {
    __m128 prod = _mm_mul_ps(a, b);
    __m128 shuf = _mm_movehdup_ps(prod);
    __m128 sum2 = _mm_add_ps(prod, shuf);
    shuf = _mm_movehl_ps(shuf, sum2);
    return _mm_cvtss_f32(_mm_add_ss(sum2, shuf));
}

HADDPS 함정: HADDPS는 두 소스를 섞으므로, 서로 다른 두 소스를 그대로 둔 채 2번 호출하면 4-float 전체 합이 되지 않습니다. 결과 레이아웃이 [b3+b2, b1+b0, a3+a2, a1+a0]이므로 두 번째 호출 시 소스 배치에 주의해야 합니다. 대신 movehdup + add + movehl + add 패턴이 더 빠르고 정확합니다.

ARM NEON 명령어 시각화

ARM NEON(Advanced SIMD)은 128비트 고정 폭 벡터로, 모바일/임베디드에서 미디어 코덱과 신호 처리의 핵심입니다. x86과 달리 구조적 로드(ld2/3/4)와 테이블 룩업(TBL)이 ISA에 내장되어 있어, 데이터 재배치에서 독보적 효율을 보입니다.

TBL — 다중 레지스터 테이블 룩업 (최대 4×16B = 64엔트리) 테이블: {v0.16B, v1.16B} — 32엔트리 (2레지스터 예시) v0.16B (인덱스 0~15) v1.16B (인덱스 16~31) T₀ T₁ T₂ T₃ T₄ T₅ T₆ T₇ T₈ T₉ T₁₀ T₁₁ T₁₂ T₁₃ T₁₄ T₁₅ T₁₆ T₁₇ T₁₈ T₁₉ T₂₀ T₂₁ T₂₂ T₂₃ T₂₄ T₂₅ T₂₆ T₂₇ T₂₈ T₂₉ T₃₀ T₃₁ 인덱스: v3.16B — 16바이트 중 대표 5개 5 20 0 31 40 ← 범위 초과(≥32) 결과: v4.16B — 룩업된 테이블 바이트 T₅ T₂₀ T₀ T₃₁ 0 범위 초과 처리: TBL → 0, TBX → dst 기존 값 유지 (merge) x86 PSHUFB와 차이: TBL은 최대 4레지스터(64B) 테이블, PSHUFB는 16B 레인 내 전용 활용: base64/hex 변환 · UTF-8 길이 테이블 · CRC 테이블 · S-box 암호화 지연: 테이블 레지스터 수에 따라 증가 (CPU 구현 의존) TRN / ZIP / UZP — 전치 / 인터리브 / 디인터리브 비교 A = a0 a1 a2 a3 B = b0 b1 b2 b3 아래 결과 셀의 요소 라벨·색 = 출처 (a* ← 소스 A · b* ← 소스 B) TRN1/TRN2 — 전치 (짝/홀수 인덱스 쌍 교차) TRN1 a0 b0 a2 b2 TRN2 a1 b1 a3 b3 짝수 쌍 → TRN1 · 홀수 쌍 → TRN2 — 2×2 행렬 전치: (a0,a1; b0,b1) → (a0,b0; a1,b1) ZIP1/ZIP2 — 인터리브 (하위/상위 half 교차 결합) ZIP1 a0 b0 a1 b1 ZIP2 a2 b2 a3 b3 하위 half → ZIP1 · 상위 half → ZIP2 — x86 PUNPCKLWD/PUNPCKHWD와 동일 · SoA→AoS 변환 UZP1/UZP2 — 디인터리브 (짝수/홀수 요소 추출) UZP1 a0 a2 b0 b2 UZP2 a1 a3 b1 b3 짝수 인덱스 → UZP1 · 홀수 인덱스 → UZP2 — AoS→SoA 변환 · RGB 채널 분리 등 · ZIP의 역연산 TRN: 2×2 블록 전치 / ZIP: 전체 인터리브(x86 UNPACK) / UZP: 전체 디인터리브 모두 저지연 — x86처럼 레인 장벽 없음 (128비트 전체가 단일 레인)
명령어동작x86 대응용도
TBL/TBX바이트 테이블 룩업 (1–4 reg)PSHUFB (16B 제한)LUT, 변환 테이블
TRN1/TRN2짝/홀수 쌍 전치—2×2 행렬 전치
ZIP1/ZIP2하위/상위 인터리브PUNPCKLXX/PUNPCKHXXSoA→AoS
UZP1/UZP2짝수/홀수 디인터리브—AoS→SoA
EXT두 벡터 연결 후 바이트 추출PALIGNR슬라이딩 윈도우
REV16/32/6416/32/64비트 내 바이트 역순PSHUFB+마스크엔디안 변환
DUP/INS스칼라→벡터 복제/삽입VPBROADCAST상수 로드
SADDLP인접 쌍 합 + 확장PMADDWD(유사)수평 축소
SQDMULH포화 배정밀도 곱→상위PMULHRSW(유사)고정소수점 곱셈
LD2/LD3/LD4구조적 로드 (디인터리브)— (셔플 필요)RGB, 스테레오 분리
/* NEON LD3 RGB 디인터리브 — x86에서는 셔플 체인 필요 */
#include <arm_neon.h>

void rgb_brightness_neon(uint8_t *img, int n_pixels, uint8_t boost)
{
    uint8x16_t vboost = vdupq_n_u8(boost);
    for (int i = 0; i < n_pixels; i += 16) {
        /* LD3: R,G,B 채널을 자동 분리 (하드웨어 디인터리브) */
        uint8x16x3_t rgb = vld3q_u8(img + i * 3);

        /* 각 채널에 포화 덧셈 */
        rgb.val[0] = vqaddq_u8(rgb.val[0], vboost);  /* R */
        rgb.val[1] = vqaddq_u8(rgb.val[1], vboost);  /* G */
        rgb.val[2] = vqaddq_u8(rgb.val[2], vboost);  /* B */

        /* ST3: 자동 인터리브 저장 */
        vst3q_u8(img + i * 3, rgb);
    }
}

/* NEON REV + EXT 엔디안 변환 (32비트 단위) */
uint32x4_t bswap32_neon(uint32x4_t v)
{
    return vreinterpretq_u32_u8(
        vrev32q_u8(vreinterpretq_u8_u32(v))  /* 각 32비트 내 바이트 역순 */
    );
}

/* 쌍선형 보간 (2D) — NEON 고정소수점 */
uint8x8_t bilinear_neon(uint8x8_t tl, uint8x8_t tr,
                        uint8x8_t bl, uint8x8_t br,
                        uint8x8_t fx, uint8x8_t fy)
{
    /* 가중 평균: result = (1-fx)(1-fy)·tl + fx(1-fy)·tr
     *                     + (1-fx)fy·bl + fx·fy·br */
    uint8x8_t ifx = vsub_u8(vdup_n_u8(255), fx);
    uint8x8_t ify = vsub_u8(vdup_n_u8(255), fy);

    uint16x8_t top  = vmull_u8(ifx, tl);           /* u8×u8→u16 */
    top = vmlal_u8(top, fx, tr);                     /* += fx·tr */
    uint16x8_t bot  = vmull_u8(ifx, bl);
    bot = vmlal_u8(bot, fx, br);

    uint16x8_t row  = vmull_u8(ify, vshrn_n_u16(top, 8));
    row = vmlal_u8(row, fy, vshrn_n_u16(bot, 8));
    return vshrn_n_u16(row, 8);                      /* u16→u8 축소 */
}

NEON 구조화 로드/저장 — LD2·LD3·LD4 (인터리브 해제)

NEON의 구조화 로드(LD2/LD3/LD4)는 메모리에 인터리브(Interleaved)된 데이터(RGB, RGBA, 스테레오 오디오 등)를 레인별로 분리해 벡터 2~4개에 나누어 로드합니다. 반대 방향(ST2/ST3/ST4)은 여러 벡터를 메모리에 인터리브 저장합니다. 이미지 채널 분리(Planar 변환), RGB→YUV 변환, 행렬 전치에서 UNZIP 조합보다 적은 명령으로 끝납니다. 커널에서는 DRM/KMS 픽셀 포맷 변환, V4L2 캡처 버퍼 포맷 변환에서 사용됩니다.

LD3 {v0.16b, v1.16b, v2.16b}, [x0] — RGB 인터리브 → 채널별 벡터 메모리 — 인터리브된 RGB888 픽셀 스트림 (픽셀당 3바이트) (앞 5픽셀 · 마지막 픽셀만 표시, 총 16픽셀 = 48바이트) R0 G0 B0 R1 G1 B1 R2 G2 B2 R3 G3 B3 R4 G4 B4 R15 G15 B15 ⋯ LD3 한 번 — 48바이트(16픽셀)를 읽어 3개 채널 벡터로 분리 요소 간격 3바이트를 하드웨어가 자동 처리 (UNZIP·셔플 조합 불필요) 결과 — 채널별로 분리된 3개 벡터 (왼쪽이 픽셀 0, 레인 0은 최하위 바이트) v0.16b R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13 R14 R15 v1.16b G0 G1 G2 G3 G4 G5 G6 G7 G8 G9 G10 G11 G12 G13 G14 G15 v2.16b B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 B10 B11 B12 B13 B14 B15 ST3 — 역방향: 채널별 벡터 3개를 인터리브로 되돌려 메모리에 저장 (LD3의 역동작) 활용 Planar 프레임버퍼 변환 (DRM/KMS 픽셀 포맷 변환, V4L2 캡처 버퍼) RGB→YUV 색차 분리 · RGBA는 LD4 · 스테레오는 LD2
NEON LD3 구조화 로드의 동작 — 인터리브된 RGB888 픽셀 스트림(R0 G0 B0 R1 …)을 한 명령으로 읽어, 모든 R은 v0.16b, 모든 G는 v1.16b, 모든 B는 v2.16b로 나누어 담습니다. 요소 간격 3바이트를 하드웨어가 처리하므로 UNZIP·셔플 조합이 필요 없고, 반대 방향 ST3는 세 채널 벡터를 다시 인터리브해 저장합니다. 128비트 레지스터(vld3q_u8) 기준으로 16픽셀(48바이트)을 한 번에 처리하는 명령어 수준의 개념도입니다.
명령어구조분리 개수용도
LD2/ST22-요소 구조2개스테레오 오디오, RGBA 높은 2채널, 짝수/홀수 분리
LD3/ST33-요소 구조3개RGB 픽셀 포맷 변환
LD4/ST44-요소 구조4개RGBA 픽셀, 4채널 이미지 분리
LD2R/LD3R/LD4R복제(Replicate)—같은 구조 요소를 모든 레인에 복제 — 필터 계수 벡터 생성
LD1 {v0,v1}, [x0]1-요소 다중 레지스터—연속 데이터를 2개 레지스터로 (De-interleave 아님)
/* NEON 구조화 로드 — RGB 인터리브 → 채널 분리 (GCC NEON intrinsics) */
#include <arm_neon.h>

/* RGB888 연속 버퍼를 R/G/B 평면으로 분리 */
void rgb_to_planes(uint8_t *rgb, uint8_t *rp, uint8_t *gp, uint8_t *bp,
                     size_t npix)
{
    size_t i;
    for (i = 0; i + 16 <= npix; i += 16) {
        /* LD3: 16픽셀(48바이트) → v0=R, v1=G, v2=B */
        uint8x16x3_t px = vld3q_u8(rgb + i * 3);
        vst1q_u8(rp + i, px.val[0]);   /* R 평면 */
        vst1q_u8(gp + i, px.val[1]);   /* G 평면 */
        vst1q_u8(bp + i, px.val[2]);   /* B 평면 */
    }
    /* tail: 스칼라 처리 */
    for (; i < npix; i++) {
        rp[i] = rgb[i * 3];
        gp[i] = rgb[i * 3 + 1];
        bp[i] = rgb[i * 3 + 2];
    }
}

/* ST4: RGBA 평면 4개 → 인터리브 버퍼 (alpha 블렌딩 후 조립) */
void rgba_assemble(uint8_t *rgba, const uint8_t *r, const uint8_t *g,
                       const uint8_t *b, const uint8_t *a, size_t npix)
{
    size_t i;
    for (i = 0; i + 8 <= npix; i += 8) {
        uint8x8x4_t px;
        px.val[0] = vld1_u8(r + i);
        px.val[1] = vld1_u8(g + i);
        px.val[2] = vld1_u8(b + i);
        px.val[3] = vld1_u8(a + i);
        vst4_u8(rgba + i * 4, px);  /* R0G0B0A0 R1G1B1A1 … */
    }
}

정렬 요건: LD3는 대상 주소가 16바이트 정렬일 필요는 없지만, 요소 간격이 3·4바이트로 고정되어 있어 한 번에 처리하는 픽셀 수 = 레지스터 폭 / 요소 수입니다. 커널에서는 kernel_neon_begin() 구간 안에서만 사용하며, 정렬되지 않은 소스(예: 3바이트 RGB)도 문제없이 읽히므로 포맷 변환 초입에 유용합니다.

ARM SVE 프레디케이트 연산 시각화

SVE(Scalable Vector Extension)는 하드웨어 벡터 길이에 무관한 코드를 작성할 수 있게 합니다. 핵심은 프레디케이트 레지스터(p0–p15)로, WHILELT로 루프 테일을 자동 처리하고, BRKA/COMPACT로 조건부 데이터 조작을 수행합니다.

WHILELT — 루프 테일 프레디케이트 자동 생성 시나리오: N=20개 요소, VL=16 (.s 요소 — 512비트 SVE) · 매 반복 WHILELT p0.s, x_idx, x_N 요소 e가 활성일 조건: (x_idx + e) < N — 참인 동안 비트 1을 연속 생성, 이후 0 요소 e → 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 반복 1 — idx=0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 p0 = 0x1111111111111111 16/16 활성 — 전체 벡터 처리 반복 2 — idx=16 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 p0 = 0x0000000000001111 4/16 활성 — 마지막 테일 자동 마스킹 반복 3 — idx=32 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 p0 = 0x0000000000000000 활성 0개 — b.none → 루프 종료 WHILELT 기반 SVE 루프 — 핵심 이점 1. 별도 테일 루프 불필요 — 마지막 반복에서 남은 요소만 자동 활성화 2. 벡터 길이 무관 — 동일 바이너리가 128비트~2048비트 SVE 하드웨어에서 동작 3. predicated 로드와 병용 — 비활성 레인은 메모리에 접근하지 않아 배열 끝을 넘어도 안전
WHILELT p0.s, x_idx, x_N은 요소 e마다 (x_idx + e) < N이 참인 동안 프레디케이트 비트 1을 연속 생성합니다. N=20, VL=16(.s, 512비트)에서 반복 1(idx=0)·반복 2(idx=16)·반복 3(idx=32)은 각각 16개 전부, 4개만(테일), 0개를 활성화하여 테일 처리가 분기 없이 자동으로 끝나는 과정을 나타냅니다. p0에서 요소 e의 비트는 비트 e×4에 배치되므로(.s) 16개·4개 활성 시 p0는 각각 0x1111111111111111·0x0000000000001111이 됩니다(개념 개요, Arm ARM 'WHILELT (predicate)' 기준).
SPLICE / COMPACT / BRKA — SVE 데이터 조작 3종 COMPACT — 활성 요소 압축 (프레디케이트 기반) 활성(A·C·E) 요소를 낮은 번호 레인부터 연속 배치하고, 남은 레인은 0으로 채움 A B C D E F A C E 0 0 0 BRKA — 첫 번째 활성 비트까지 1 유지 (Break After) 입력 프레디케이트의 첫 활성 비트까지 1로 유지하고, 이후 비트는 전부 0으로 설정 0 0 1 1 0 0 첫 활성 1 1 1 0 0 0 SPLICE — 첫·마지막 활성 사이 구간 + Zn2 앞 요소 Zn1에서 첫 활성~마지막 활성 사이 연속 구간을 복사하고, 남은 레인은 Zn2의 앞 요소로 채움 p Zn1 Zn2 0 1 1 0 1 0 A B C D E F X Y Z W V U 결과 B C D E X Y COMPACT: 조건 필터링(양수만, 0 아닌 값만) · BRKA: 첫 매치 위치 탐색(strlen, strchr) SPLICE: 벡터 간 데이터 연결(스트리밍 파이프라인) · CNTP: 활성 레인 개수 세기(popcnt)
SPLICE·COMPACT·BRKA — SVE 데이터 조작 3종입니다. COMPACT는 활성 요소를 낮은 번호 레인부터 연속 배치하고 나머지 레인은 0으로 채우며, BRKA는 첫 활성 비트까지만 1로 유지해 첫 매치 위치 탐색에 쓰이고, SPLICE는 Zn1의 첫 활성~마지막 활성 사이 연속 구간을 잘라낸 뒤 남은 레인을 Zn2의 앞 요소로 채웁니다. 명령어 수준의 개념 개요이며, Arm ARM 'COMPACT (vector)'·'BRKA'·'SPLICE (vector)' 어휘 정의를 기준으로 합니다.
/* SVE strlen — first-fault 로드 + BRKA로 null 위치 탐색 */
#include <arm_sve.h>

size_t sve_strlen(const char *s)
{
    size_t i = 0;
    svbool_t pg;

    do {
        pg = svwhilelt_b8(i, (uint64_t)SIZE_MAX);
        svuint8_t data = svld1(pg, (uint8_t*)(s + i));
        svbool_t match = svcmpeq(pg, data, 0);  /* null 바이트 비교 */

        if (svptest_any(pg, match)) {
            /* BRKA: 첫 null 위치까지만 활성 → CNTP로 개수 */
            svbool_t before_null = svbrka_z(match, match);
            return i + svcntp_b8(pg, before_null);
        }
        i += svcntb();  /* VL 바이트만큼 전진 */
    } while (svptest_any(pg, pg));

    return i;
}

/* SVE 배열 필터링 — COMPACT로 양수만 추출 */
int filter_positive_sve(const float *in, float *out, int n)
{
    int written = 0;
    for (int i = 0; i < n; i += (int)svcntw()) {
        svbool_t pg = svwhilelt_b32(i, n);
        svfloat32_t v = svld1(pg, in + i);

        /* 양수 마스크 */
        svbool_t pos = svcmpgt(pg, v, 0.0f);
        int cnt = (int)svcntp_b32(pg, pos);

        /* COMPACT: 활성 요소를 앞으로 압축 */
        svfloat32_t packed = svcompact(pos, v);
        svbool_t store_pg = svwhilelt_b32(0, cnt);
        svst1(store_pg, out + written, packed);
        written += cnt;
    }
    return written;
}

SVE fault-first 로드 — LDFF1·RDFFR·SETFFR

SVE의 fault-only-first 로드(LDFF1B/H/W/D)는 페이지(Page) 경계를 넘는 접근에서도 예외(Fault) 없이 안전하게 값을 읽는 기법입니다. 일반 로드(LD1)가 페이지 끝을 넘어가면 #SEGV가 발생하지만, LDFF1은 첫 번째 fault가 나기 전까지의 요소만 로드하고 FFR(First Fault Register)에 성공/실패를 기록합니다. 이후 RDFFR로 FFR을 프레디케이트(레지스터)로 읽어 몇 개 요소까지 안전했는지 알아내고, SETFFR로 FFR을 1로 초기화해 다음 반복을 준비합니다. 커널의 memchr/strlen 탐색 루프나 문자열 파서에서 "페이지 경계 검사 + 스칼라 폴백" 대신 사용하면 분기와 예외 처리를 제거할 수 있습니다.

LDFF1B — 페이지 경계를 넘어도 예외 없이 안전하게 로드 ① 주소 공간 — 페이지 경계를 넘는 LDFF1B 로드 페이지 경계 (4KB) 페이지 A — 유효 메모리 페이지 B — 미매핑(접근 시 fault) 0 1 2 3 4 5 6 7 정상 로드 4요소 fault 4요소 ② FFR에 요소별 결과 기록 적재 성공 1 · fault 이후 0 1 1 1 1 0 0 0 0 예외 없이 중단 fault 이후 요소는 적재되지 않고 FFR만 0 ③ FFR 판독과 재무장 — 반복 루프 LDFF1B — 첫 fault까지 로드 예외 없이 FFR 갱신 RDFFR p0.b 성공 요소만 1인 프레디케이트 SETFFR FFR을 모두 1로 재무장 반복 — 다음 워드로 전진해 같은 루프 수행 활용 — memchr · strlen · 문자열 파서의 탐색 루프 페이지 경계 검사와 스칼라 폴백 분기를 없애고, fault 지점에서 바로 종료를 판정합니다 LDNF1(비-폴트, non-fault)과의 차이 — LDNF1은 fault 요소를 0으로 치고 끝까지 실행하고, LDFF1은 첫 fault에서 중단합니다
SVE fault-first 로드(LDFF1B)의 동작입니다. 페이지 A에 있는 요소는 정상 적재되지만, 페이지 경계를 넘어 미매핑 페이지 B에 닿는 순간부터 접근이 억제되고 예외(#SEGV) 대신 FFR에 요소별 결과가 기록됩니다(적재 성공 1, fault 지점부터 0). 소프트웨어는 RDFFR로 FFR을 프레디케이트로 읽어 안전하게 적재된 요소의 수와 위치를 파악하고, SETFFR로 FFR을 다시 모두 1로 재무장한 뒤 다음 LDFF1B를 수행합니다. 요소 8개와 페이지 크기 4KB는 설명을 위한 예시이며, 주소 공간(Address Space)·FFR 기록·판독 루프·활용의 네 단계와 LDNF1과의 차이를 개념 수준으로 나타낸 그림입니다. 첫 활성 요소 자체가 fault이면 예외가 발생하고 그 이후 요소의 fault만 억제된다는 점은 Arm A64 ISA의 LDFF1B 의사코드에 따른 것입니다.
명령어동작실전 용도
LDFF1B/H/W/D첫 fault 전까지 요소 로드, FFR 갱신memchr, strlen, 파서 정찰 로드
LDNF1B/H/W/Dfault 요소는 0 처리하고 전체 로드주소 검사 없는 안전 메모리 읽기
RDFFRFFR을 프레디케이트 레지스터로 읽기성공 요소 수·위치 파악
RDFFRSFFR을 sve predicate으로 읽기 (성능 최적화)고성능 루프에서 사용
SETFFRFFR 전체를 1로 초기화반복 루프 시작 전 재무장(Re-arm)
/* SVE fault-first 탐색 — 메모리에서 0이 아닌 값 찾기 (arm_sve.h intrinsics) */
#include <arm_sve.h>

uint64_t sve_strlen_ff(const char *s)
{
    uint64_t len = 0;
    svbool_t pred = svptrue_b8();
    while (1) {
        svuint8_t chunk = svldff1_u8(pred, (const uint8_t *)s + len);
        svbool_t nul = svcmpeq_u8(svptrue_b8(), chunk, 0);
        /* fault-first: 페이지 끝에서 멈춘 경우 FFR로 안전 범위만 테스트 */
        svbool_t ok = svpfalse_b();          /* 실제 코드는 svrdffr()로 FFR 읽기 */
        ok = svrdffr();
        nul = svand_b_z(svptrue_b8(), nul, ok);
        if (svptest_any(svptrue_b8(), nul)) {
            uint64_t off = svcntp_b8(svptrue_b8(), svbrkb_b_z(svptrue_b8(), nul));
            return len + off - 1;
        }
        len += svcntb();
        pred = svptrue_b8();
    }
}

FFR은 광범위한 부작용을 가집니다: LDFF1 사이에 다른 SVE 명령이 FFR을 덮어쓸 수 있으므로, fault-first 로드 직후 가능한 한 빨리 RDFFR로 읽어야 합니다. 또한 kernel_neon_begin() 구간 안에서는 예외 테스트를 위해 잘못된 주소를 의도적으로 건드리는 패턴이 커널에서 거부될 수 있으므로, 반드시 명시적 fault-first 로드를 사용합니다.

RISC-V RVV 명령어 시각화

RVV(RISC-V Vector Extension)는 SVE처럼 구현 정의 벡터 길이(VLEN)를 지원하되, vsetvli로 동적으로 SEW(요소 폭)와 LMUL(레지스터 그룹 배수)을 설정하는 독특한 모델입니다. 이 명령어가 VL(실제 처리 요소 수)을 반환하므로, 루프 제어가 자동화됩니다.

VSETVLI — 벡터 길이(VL) 설정 흐름 ① 입력 파라미터 — vsetvli가 받는 값 AVL 요청 요소 수 (예: 100) SEW 요소 폭 (예: 32비트) LMUL 레지스터 그룹 배수 (예: 2) ② VLMAX 계산 — vsetvli가 내부에서 설정 VLMAX = LMUL × VLEN ÷ SEW 예: VLEN=256, SEW=32비트, LMUL=2 → VLMAX = 16 ③ VL 결정 — vl 설정 규칙 (RVV 1.0) AVL ≤ VLMAX → VL = AVL AVL ≥ 2 × VLMAX → VL = VLMAX VLMAX < AVL < 2 × VLMAX → ceil(AVL/2) ≤ VL ≤ VLMAX (구현 정의) 이로부터 항상 VL ≤ min(AVL, VLMAX) · 예: AVL=100, VLMAX=16 → VL = 16 ④ 활용 — 설정된 VL이 이후 벡터 명령과 루프 제어에 쓰이는 방식 vsetvli가 rd에 VL을 반환 → AVL -= VL · 포인터 += VL × (SEW/8) → AVL = 0까지 스트립마인 반복 AVL ≤ VLMAX가 되면 VL = AVL → 남은 요소를 한 번에 처리 (SVE WHILELT 기반 테일 처리와 대비)
vsetvli의 벡터 길이(VL) 설정 흐름입니다. vsetvli는 rs1에서 요청 요소 수 AVL을, vtypei 즉시값에서 요소 폭 SEW와 레지스터 그룹 배수 LMUL을 받습니다. 하드웨어는 먼저 VLMAX = LMUL × VLEN ÷ SEW를 계산하고(VLEN은 구현 정의 값), AVL과 VLMAX의 관계에 따라 vl 레지스터 값 VL을 정합니다. RVV 1.0은 세 가지 규칙을 정의합니다 — AVL ≤ VLMAX이면 VL = AVL, AVL ≥ 2 × VLMAX이면 VL = VLMAX, 그 사이 구간은 ceil(AVL/2) ≤ VL ≤ VLMAX 범위에서 구현이 선택합니다. 이로부터 항상 VL ≤ min(AVL, VLMAX)가 성립합니다. 설정된 VL은 이후 벡터 명령의 활성 요소 수가 되고 스트립마인 루프의 제어 값으로 쓰이므로, 벡터 길이 비종속(Vector Length Agnostic) 방식의 이식성 높은 루프를 작성할 수 있습니다.
RVV 퍼뮤트 3종 — VRGATHER · VSLIDEDOWN · VCOMPRESS ① VRGATHER — 인덱스 기반 임의 재배치 src A B C D E F G H dst F A H D B C E G idx (vs1) 5 0 7 3 1 2 4 6 인덱스 벡터로 원소 순서를 임의로 재배치 x86 VPERMD · SVE TBL 대응 인덱스 ≥ VLMAX → 결과 0 ② VSLIDEDOWN — 요소 시프트 (하향 슬라이드) src A B C D E F G H dst D E F G H 0 0 0 src idx 3 4 5 6 7 — — — 스칼라 offset만큼 아래로 이동 x86 VPALIGNR(유사) · SVE EXT 대응 범위 밖 원소는 0으로 채움 ③ VCOMPRESS — 마스크 기반 압축 src AO BX CX DO EO FX GO HX dst A D E G tail (4개) — 테일 정책 mask (vs1) 1 0 0 1 1 0 1 0 마스크가 1인 원소만 앞으로 밀착 x86 VCOMPRESSPS · SVE COMPACT 대응 나머지 자리는 tail 정책으로 처리 세 명령 모두 소스 원소를 재배열해 목적지에 씁니다 — 어떤 값(인덱스·오프셋·마스크)으로 원소를 고르는지가 차이입니다.
RVV 퍼뮤트 명령 3종의 동작 비교입니다. 각 레인의 src는 공통 소스 벡터 [A, B, C, D, E, F, G, H]이고, 오른쪽의 idx·src idx·mask가 연산 파라미터입니다. VRGATHER는 인덱스 벡터 vs1로 dst[i] = vs2[idx[i]]를 계산하는 임의 재배치이고(인덱스가 VLMAX 이상이면 결과는 0), VSLIDEDOWN은 스칼라 offset(예시 3)만큼 원소를 아래로 밀며 범위를 벗어난 자리는 0으로 채웁니다. VCOMPRESS는 마스크 vs1의 비트가 1인 원소만 원래 순서대로 앞에 밀착시키고 나머지 자리는 tail 정책(undisturbed/agnostic)을 따릅니다. 표시된 값은 VLMAX=8 예시이며, 명령의 의미 수준(개념 개요)을 나타냅니다. RISC-V V 확장 명세(RVV 1.0) 기준입니다.
명령어동작x86 대응SVE 대응
VSETVLI/VSETIVLIVL/SEW/LMUL 설정——(VL 고정)
VLE/VSE단위 스트라이드 로드/스토어VMOVDQULD1/ST1
VLSE/VSSE스트라이드 로드/스토어VPGATHERDD(유사)LD1(stride)
VLUXEI/VSUXEI인덱스(gather/scatter)VPGATHERD/VPSCATTERDLD1(gather)
VRGATHER인덱스 기반 재배치VPERMDTBL
VSLIDEDOWN/UP요소 시프트VPALIGNR(유사)EXT
VCOMPRESS마스크 압축VCOMPRESSPSCOMPACT
VREDSUM/MAX/MIN벡터→스칼라 리덕션셔플 캐스케이드FADDV/SMAXV
VWMUL/VWMACC확장 곱/곱-누적PMADDWD(유사)SMULL/SMLAL
VFMACCFP 곱-누적VFMADDFMLA
/* RVV memcpy 루프 — vsetvli가 자동으로 VL 관리 */
void rvv_memcpy(void *dst, const void *src, size_t n)
{
    const uint8_t *s = src;
    uint8_t *d = dst;
    while (n > 0) {
        size_t vl;
        asm volatile(
            "vsetvli %0, %1, e8, m8, ta, ma\n"  /* SEW=8, LMUL=8 → 최대 대역 */
            "vle8.v  v0, (%2)\n"
            "vse8.v  v0, (%3)\n"
            : "=r"(vl)
            : "r"(n), "r"(s), "r"(d)
            : "memory"
        );
        s += vl;
        d += vl;
        n -= vl;
    }
}

/* RVV RAID XOR 패리티 — 다중 디스크 XOR */
void rvv_xor_parity(uint8_t *parity, uint8_t **disks,
                     int ndisks, size_t len)
{
    for (size_t offset = 0; offset < len; ) {
        size_t vl;
        asm volatile(
            "vsetvli %0, %1, e8, m8, ta, ma"
            : "=r"(vl) : "r"(len - offset));

        /* 첫 디스크 로드 */
        asm volatile("vle8.v v0, (%0)" :: "r"(disks[0] + offset));

        /* 나머지 디스크들과 XOR */
        for (int d = 1; d < ndisks; d++) {
            asm volatile(
                "vle8.v v8, (%0)\n"
                "vxor.vv v0, v0, v8"
                :: "r"(disks[d] + offset) : "memory");
        }

        asm volatile("vse8.v v0, (%0)" :: "r"(parity + offset));
        offset += vl;
    }
}

/* RVV 리덕션 합 — VREDSUM.VS로 벡터→스칼라 */
int32_t rvv_sum(const int32_t *arr, size_t n)
{
    int32_t total = 0;
    for (size_t i = 0; i < n; ) {
        size_t vl;
        asm volatile(
            "vsetvli  %0, %1, e32, m4, ta, ma\n"
            "vle32.v  v4, (%2)\n"
            "vmv.s.x  v0, %3\n"             /* 스칼라 초기값 */
            "vredsum.vs v0, v4, v0\n"        /* v0[0] += Σv4 */
            "vmv.x.s  %3, v0"               /* 스칼라 추출 */
            : "=r"(vl), "+r"(total)
            : "r"(n - i), "r"(arr + i)
            : "memory"
        );
        i += vl;
    }
    return total;
}

RVV vs SVE 설계 철학 차이: SVE는 VL이 루프 전체에서 고정(하드웨어 결정)이고 WHILELT로 테일을 처리합니다. RVV는 vsetvli가 매 반복마다 VL을 동적으로 설정하고, AVL이 자동으로 감소합니다. 두 접근 모두 벡터 길이에 무관한(VLA) 코드를 작성할 수 있지만, RVV가 SEW/LMUL 변경이 더 유연하고, SVE는 프레디케이트 연산(BRKA/BRKN 등)이 더 풍부합니다.

SIMD 명령어 패밀리 지도

앞선 시각화 섹션은 핵심 명령어를 깊게 설명하는 데 초점을 맞췄습니다. 그러나 실제 SIMD 명령어셋은 데이터 폭, 부호 여부, 포화 여부, 마스크 여부, 인코딩(VEX/EVEX/SVE/RVV) 조합까지 포함하면 개별 mnemonic 수가 폭발적으로 늘어납니다. 커널 개발자는 개별 opcode 이름을 외우기보다 어떤 문제를 해결하는 패밀리인지를 먼저 구분해야 합니다. 아래 표는 x86, ARM NEON/SVE, RISC-V RVV를 동일한 관점으로 정리한 실전 지도입니다.

패밀리x86ARM NEON / SVERISC-V RVV커널 활용
연속 로드/스토어VMOVDQU8/16/32/64, VMOVAPS, VMOVUPSLD1/ST1, LD1B/ST1B, LD1W/ST1WVLE/VSEmemcpy, XOR, checksum
구조화/분리 로드UNPCK + SHUF 조합LD2/LD3/LD4, ST2/ST4VLSEG/VSSEG인터리브 패킷(Packet), RGB, SoA 변환
비교 → 마스크VPCMPEQ, VPCMP*, VPMOVMSKB, KORTESTCMEQ/CMGT, PTEST, CNTPVMSEQ/VMSLT, VMAND.MM, VCPOP.Mmemchr, 필터링, 경계 탐색
조건부 선택VPBLEND*, VPTERNLOG*, VSELECT(mask)BSL/BIT/BIF, SELVMERGE.VM마스크 기반 합성, 분기 제거
셔플/테이블 룩업PSHUFB, VPERMB, VPERMD, VPERMT2*TBL/TBX, ZIP/TRN/UZP, EXTVRGATHER, VSLIDEUP/DOWN바이트 재배열, 해시, 문자열 정규화
widen / narrowVPMOVZX*, VPMOVSX*, PACKSS*, PACKUS*USHLL/SSHLL, SQXTN/UQXTN, FCVTL/FCVTNVWADD/VWMUL, VNCLIP/VNCLIPU양자화, 픽셀 변환, 샘플 확장
포화 산술PADDS*, PADDUS*, PSUBS*SQADD/UQADD, SQSUB/UQSUBVSADD/VSADDU, VSSUB/VSSUBU오디오, 이미지, clamp
곱-누적 / 점곱PMADDWD, PMADDUBSW, VPDPBUSD, VDPBF16PSSMLAL/UMLAL, SDOT/UDOT, BFDOTVWMACC/VWMACCSU, VFMACC암호, FIR, INT8 추론, 행렬 곱
리덕션HADD*, DPPS, 셔플 캐스케이드ADDV, SMAXV, FADDVVREDSUM, VWREDSUM, VFREDUSUM합계, 최대/최소, 통계
Gather / ScatterVGATHER*, VSCATTER*, VPSCATTER*NEON은 일반 메모리 gather 부재, SVE는 LD1/ST1 gather 지원VLUXEI/VSUXEI, VLOXEI/VSOXEI희소 인덱스, 룩업 테이블
압축 / 컴팩트VCOMPRESS*, VEXPAND*COMPACT, SPLICEVCOMPRESS.VM필터링, pack-active, sparse write
Fault-first / 탐색문자열 비교 + MOVEMASK 조합LDFF1, BRKA, MATCH, WHILELTVLEFF, VFIRST.M, VIOTA.Mstrlen, copy_to_user 전 검사
논템포럴 / 스트리밍MOVNT*, PREFETCH*, MOVNTDQAPRFM, STNP 계열 보조 사용일반 prefetch 힌트 + 스트라이드 루프대용량 복사, 버퍼 초기화
암호 전용AESENC, PCLMULQDQ, SHA*, GFNIAESE, PMULL, SHA256H, SM4EZvkned, Zvkg, Zvkb, Zvknh*IPsec, dm-crypt, WireGuard

실전 분류 기준: 커널 코드는 보통 load → compare/select → shuffle → accumulate → reduce/store 흐름으로 읽으면 구조가 빠르게 보입니다. 명령어 이름이 달라도 이 다섯 단계 중 어디에 속하는지만 먼저 파악하면 x86용 알고리즘을 NEON, SVE, RVV로 옮길 때 훨씬 수월합니다. 특히 우선순위가 높은 핫패스가 해시, 복호화(Decryption), 패킷 처리, 대형 버퍼 이동 중 어디에 있는지 먼저 판단해야 SIMD 투자 대비 효과를 정확히 계산할 수 있습니다.

x86 논리·비교·선택 명령어 상세

문자열 검색, 패킷 분류, 비트마스크 생성, 조건부 merge는 커널 SIMD 코드에서 매우 자주 등장합니다. 핵심 패턴은 비교(compare) → 마스크(mask) 추출 → 첫 hit 계산 또는 마스크로 선택(blend/select)입니다. AVX2까지는 movemask로 벡터 상태를 스칼라 비트마스크로 내리고, AVX-512에서는 k-mask 레지스터를 유지한 채 kortest, compress, 마스크 스토어로 이어갑니다.

비교 결과를 어떻게 소비하는가: AVX2 MOVEMASK vs AVX-512 k-mask AVX2 — compare → MOVEMASK: 결과를 스칼라 비트마스크로 내린다 입력 32바이트 41 72 2F 10 2F 33 … VPCMPEQB needle 2F와 비교 compare 결과 벡터 00 00 FF 00 FF … VPMOVMSKB mask = 0b00010100 TZCNT / BSF 첫 hit = buf[i+2] AVX-512 — compare → k-mask: 결과를 k 레지스터에 유지한다 입력 32바이트 41 72 2F 10 2F 33 … VPCMPB/VPCMPEQD 비교 → mask 직행 k1 = 0b00010100 레인 활성 비트 KORTESTW any / all 검사 VCOMPRESS* 일치 레인만 저장
비교 결과를 소비하는 두 방식 — AVX2 경로는 VPCMPEQB가 만든 바이트별 compare 벡터를 VPMOVMSKB로 스칼라 GPR 비트마스크(mask = 0b00010100)로 내린 뒤 TZCNT/BSF로 첫 일치 오프셋을 구합니다. AVX-512 경로는 VPCMPB/VPCMPEQD가 비교 결과를 곧바로 k-mask 레지스터(k1)에 남기므로 스칼라로 내리지 않고 KORTESTW로 any/all을 검사하고 VCOMPRESS*로 일치 레인만 압축 저장합니다. 예시 mask 0b00010100은 입력 32바이트 중 needle 0x2F가 오프셋 2와 4에서 일치하는 경우입니다. 명령어 의미 수준의 개념 개요입니다.
패밀리대표 명령어핵심 의미커널 활용 포인트
동등/대소 비교PCMPEQB/W/D, PCMPGTB/W/D, VPCMPB/D/UQ레인별 비교 결과를 all-ones 또는 mask 비트로 생성문자열 검색, 패킷 필터, 경계 조건 검사
문자열 비교PCMPESTRI/M, PCMPISTRI/MSSE4.2 문자열 검색 전용 비교 + 인덱스 반환memchr, strcmp, delimiter 탐색
논리 연산PAND, POR, PXOR, PANDN, VPANDD/Q타입과 무관한 비트 연산암호, RAID XOR, 비트마스크 조합
테스트PTEST, VTESTPS/PD, VPTESTMB/MDAND 결과의 zero/all-one 여부를 플래그 또는 mask로 전달빠른 early-exit, zero-block 탐지
마스크 추출PMOVMSKB, VPMOVMSKB, KMOVW/Q벡터 비교 결과를 스칼라 비트필드로 변환first-hit 탐색, scalar branch 연결
블렌드BLENDVPS, VPBLENDVB, VPBLENDMD조건에 따라 두 소스 중 한쪽을 선택분기 제거, scatter 전 필터링
삼항 논리VPTERNLOGD/Q3입력 Boolean 식을 imm8 truth table로 한 번에 계산AND/OR/XOR/NOT 조합 축약, 암호 비트슬라이스
k-mask 조작KAND, KOR, KXOR, KNOT, KSHIFTL/R, KORTESTAVX-512 mask 레지스터 자체를 산술/분기 입력으로 사용compress, masked store, sparse loop
집합 교차VP2INTERSECTD/Q두 벡터 간 교차 여부를 두 개의 mask로 반환룩업 테이블 membership, Bloom 보조 검사
GF(2) 비트 행렬GF2P8AFFINEQB, GF2P8MULB바이트 단위 갈루아 필드 변환CRC, RAID, 암호 S-box 변형
/* AVX2 memchr — compare → movemask → ctz 패턴의 정석 */
#include <immintrin.h>

ssize_t memchr_avx2(const uint8_t *buf, size_t len, uint8_t needle)
{
    __m256i key = _mm256_set1_epi8((char)needle);
    size_t i = 0;

    for (; i + 32 <= len; i += 32) {
        __m256i v = _mm256_loadu_si256((const __m256i *)(buf + i));
        __m256i eq = _mm256_cmpeq_epi8(v, key);
        unsigned mask = (unsigned)_mm256_movemask_epi8(eq);

        if (mask)
            return (ssize_t)(i + (size_t)__builtin_ctz(mask));
    }

    for (; i < len; i++) {
        if (buf[i] == needle)
            return (ssize_t)i;
    }
    return -1;
}

MOVEMASK가 아직 중요한 이유: AVX-512가 없는 범용 x86 서버에서는 compare → movemask → ctz가 여전히 가장 휴대성이 높고 빠른 바이트 탐색 패턴입니다. 반면 AVX-512에서는 mask를 스칼라로 내리지 않고 kortest, compress, masked load/store로 끝까지 유지하는 편이 더 낫습니다.

SSE4.2 문자열 비교 명령어 — PCMPESTR*/PCMPISTR*

text/string 프로세싱 전용 명령어인 PCMPESTRI/M, PCMPISTRI/M은 네 개의 오퍼랜드를 받아 "문자열 비교 + 인덱스/플래그 생성"을 하나의 명령으로 완성합니다. ESTR(Explicit Length, 길이를 EAX/EDX로 명시)와 ISTR(Implicit Length, 0바이트 종료 문자열)로 나뉘며, 결과는 ECX 인덱스와 EFLAGS(CF/SF/ZF/OF)로 반환됩니다. 8비트 imm8은 SRC(부호/zero 확장), POL(극성), AGG(집계 규칙), IDX(인덱스 폭)를 제어합니다. 커널에서도 memchr, strcspn, delimiter 탐색, URL/헤더 파서에 활용되었지만, 최신 CPU에서는 256비트 compare → movemask → ctz 패턴이 더 널리 사용됩니다.

PCMPISTRI — NUL(0x00)로 종료되는 문자열에서 delimiter 탐색 (ISTR 모드) ISTR(Implicit Length, 내재적 길이): 각 오퍼랜드의 첫 NUL 바이트까지가 유효 문자열 xmm2 — haystack(검색 대상) g e t / a b NUL ← 인덱스 6에서 haystack 종료 xmm1 — needle(검색할 문자) / NUL AGG = EQUAL_ANY: haystack의 각 바이트를 needle 문자 집합과 비교 → 첫 매치 인덱스 ECX = 3 (두 번째 오퍼랜드 xmm2 기준) imm8 제어 바이트 (예: 0x00) [7] 예약 0 (고정) [6] 인덱스 0 = 최소 인덱스 [5:4] 극성(POL) 0b00 = 양(positive) [3:2] 집계(AGG) 0b00 = EQUAL_ANY [1:0] 데이터 타입 0b00 = u8(unsigned) (MSB) bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0 (LSB) PCMPISTRI EFLAGS(결과) CF=1(매치 존재), ZF=1(SRC2에 NUL 존재), SF=1(SRC1에 NUL 존재), OF=0 PCMPISTRI는 첫 오퍼랜드(xmm1: needle 문자 집합)와 두 번째 오퍼랜드(xmm2/m128: haystack)를 비교하고, 매치 인덱스는 xmm2 기준 ECX에 반환됩니다. ISTR은 첫 NUL까지를 유효 길이로 보고, ESTR은 EAX/EDX로 명시한 길이를 사용합니다. 결과 인덱스는 ECX에 반환됩니다.
SSE4.2 PCMPISTRI ISTR(delimiter 탐색)의 동작 — 첫 오퍼랜드 xmm1은 needle 문자 집합('/')을, 두 번째 오퍼랜드 xmm2는 haystack(get/ab)을 담으며, EQUAL_ANY 집계로 xmm2의 각 바이트가 xmm1의 문자 집합과 비교됩니다. 매치 위치는 두 번째 오퍼랜드(xmm2)를 인덱싱해 ECX=3으로 반환됩니다. imm8은 데이터 타입·집계·극성·인덱스 선택을 제어하며, ISTR은 두 문자열 모두 첫 NUL에서 유효 길이가 결정됩니다. SSE4.2 명령어 의미 수준의 개념 개요입니다.
명령어길이 처리단일/비교 반환실전 용도
PCMPISTRI내재적(0바이트 종료)ECX에 첫 매치 인덱스memchr/delimiter 탐색
PCMPISTRM내재적(0바이트 종료)xmm0에 비트마스크로 변환문자 클래스 멤버십 테스트
PCMPESTRI명시적(EAX/EDX)ECX에 첫 매치 인덱스고정 길이 헤더/패킷 파싱
PCMPESTRM명시적(EAX/EDX)xmm0에 비트마스크로 변환토큰 분할, 문자열 유효성 검사
/* PCMPISTRI — '/' delimiter 검색 (ISTR: 0바이트 종료 가정) */
#include <nmmintrin.h>  /* SSE4.2 */

static inline int find_slash_istr(const char *s)
{
    __m128i v   = _mm_loadu_si128((const __m128i *)s);
    __m128i pat = _mm_set1_epi8('/');
    /* _SIDD_CMP_EQUAL_ANY | _SIDD_UBYTE_OPS: 0바이트 종료 u8 비교 */
    return _mm_cmpistri(v, pat, _SIDD_CMP_EQUAL_ANY | _SIDD_UBYTE_OPS);
}

/* ESTR: 정확히 16바이트까지 고정 길이 비교 — 패킷 헤더 파싱 */
static inline int find_delim_estr(const char *buf, int len)
{
    __m128i v   = _mm_loadu_si128((const __m128i *)buf);
    __m128i pat = _mm_set1_epi8('\t');
    /* mm_cmpestri(src1, len1, src2, len2, imm8): len1 = len, len2 = 1 */
    return _mm_cmpestri(v, len, pat, 1,
                       _SIDD_CMP_EQUAL_ANY | _SIDD_UBYTE_OPS);
}

연대기 관점: PCMPISTR/PCMPESTR은 2008년 Nehalem(SSE4.2)에서 도입되어 __intel SSE4.2 문자열 처리의 기준이 되었지만, 256비트+ 바이트 비교가 보편화된 이후에는 VPCMPEQB → VPMOVMSKB → TZCNT 패턴이 더 단순하고 2배 폭을 처리합니다. 문자열 전용 명령이 필요한 경우는 패턴 집합(문자 클래스) 비교처럼 imm8 집계 규칙이 유용한 경우로 한정됩니다.

VPTERNLOG — 3입력 논리 연산 (imm8 truth table)

VPTERNLOGD/Q는 세 개의 벡터 입력과 8비트 imm8 truth table을 받아, 레인마다 imm8이 정의하는 256가지 Boolean 함수 중 하나를 단일 명령으로 계산합니다. 3입력 조합(a,b,c) 8가지 각각에 대한 결과 1비트가 imm8의 bit[7:0]에 대응하므로, AND(0x80), OR(0xFE), XOR(0x96) 같은 기본 연산은 물론 (a & b) | (c & ~a) 같은 복합식을 PAND/PANDN/POR 3개 명령을 하나로 축약합니다. 커널 bit-slicing 암호(예: Serpent), 국소 병합, 마스크 합성에서 자주 등장합니다. AVX-512F에 포함되어 128/256/512비트 모두 사용할 수 있습니다.

VPTERNLOGD dst, a, b, c, imm8 — 256가지 Boolean 함수를 한 명령으로 a = 0x0F0F0F0F b = 0x33333333 c = 0x55555555 VPTERNLOGD imm8 = 0x96 (0b10010110) dst 레인 = a ^ b ^ c dst = 0x69696969 imm8 조회 테이블 3비트 인덱스 → imm8 비트 선택 a b c imm8 비트 0x96 비트값 0 0 0 bit0 0 0 0 1 bit1 1 0 1 0 bit2 1 0 1 1 bit3 0 1 0 0 bit4 1 1 0 1 bit5 0 1 1 0 bit6 0 1 1 1 bit7 1 주요 imm8 상수 — imm8 값이 곧 Boolean 함수를 결정합니다 0x80 = a & b & c 0xFE = a | b | c 0x96 = a ^ b ^ c 0xCA = (a & b) | (~a & c) 세 입력의 같은 위치 비트 3개가 인덱스가 되어 imm8 비트를 고르고, PAND/PANDN/POR 3명령을 1명령으로 축약합니다
VPTERNLOGD의 imm8 truth table 구조 — 세 입력 a·b·c의 같은 위치 비트가 (a<<2)|(b<<1)|c 순서로 3비트 인덱스를 만들고, 그 인덱스가 imm8의 비트 하나를 골라 결과 1비트가 됩니다. 예시는 a=0x0F0F0F0F, b=0x33333333, c=0x55555555, imm8=0x96(a^b^c)이고 dst=0x69696969인 경우입니다. 어셈블리 형식에서는 첫 피연산자(dst)가 첫 소스 a를 겸합니다. 명령어 의미 수준의 개념 개요이며, imm8을 조립하는 방법은 아래 표와 코드에서 다룹니다.
imm8Boolean 식용도
0x80a & b & c3입력 AND (bit-slice S-box)
0xFEa | b | c3입력 OR (합성 마스크)
0x96a ^ b ^ c3입력 XOR (Serpent bit-slice, 패리티)
0x78a ^ (b & c)조건부 반전 — b와 c가 모두 1이면 a 반전
0xCA(a & b) | (~a & c)a가 1이면 b, 아니면 c 선택 (blend)
0xCCb복사 — PANDN 없이 단순 통과
/* VPTERNLOG — 3입력 XOR: 패리티/비트슬라이스 핵심 */
#include <immintrin.h>

/* dst = a ^ b ^ c — imm8 = 0x96 (10010110₂) */
__m256i ternary_xor3(__m256i a, __m256i b, __m256i c)
{
    return _mm256_ternarylogic_epi32(a, b, c, 0x96);
}

/* dst = (a & b) | (~a & c) — a가 1이면 b, 0이면 c 선택 (imm8 = 0xCA) */
__m256i ternary_select(__m256i a, __m256i b, __m256i c)
{
    return _mm256_ternarylogic_epi32(a, b, c, 0xCA);
}

imm8 생성 요령: 진리표에서 (a,b,c) = (0,0,0)일 때 결과를 bit0, (1,1,1)일 때 결과를 bit7로 두고 8비트로 조립하면 됩니다. 예: a ^ b ^ c는 입력 1,2,4,7 조합에서 1 → 0b10010110 = 0x96입니다. GCC/Clang은 __builtin_ia32_ternarylogic 계열 intrinsics로 직접 사용하거나, 이미 컴파일러가 a ^ b ^ c 상등식을 자동 합성합니다.

x86 Gather·Scatter·Prefetch·Streaming 상세

연속 메모리라면 일반 로드/스토어가 가장 빠릅니다. 하지만 라우팅(Routing) 테이블(Routing Table), 희소 인덱스, 압축 블록 메타데이터처럼 주소가 흩어진 자료구조에서는 gather/scatter가 코드 복잡도를 크게 줄여줍니다. 다만 성능은 주소 분산도(index entropy)와 캐시 히트율에 극단적으로 민감합니다. 연속 접근으로 재구성할 수 있다면 대부분 gather보다 낫습니다.

연속 접근, gather/scatter, non-temporal store는 비용 구조가 다릅니다 1. 연속 접근 base[0] base[1] base[2] … base[7] (연속 주소) VMOVDQU32 / VMOVUPS 연속 주소를 한 번에 로드 캐시라인 1~2개 접촉 가장 저렴한 경로 2. Gather base[15] base[2] base[29] base[7] (불연속 인덱스) VGATHERDPS / VPGATHERDD 불연속 주소를 벡터로 모아 읽기 여러 캐시라인 병합 주소 분산도·캐시 히트율에 민감 3. Scatter base[15] base[2] base[29] base[7] (불연속 목적지) VSCATTERDPS / VPSCATTERDD 벡터 값을 불연속 주소로 저장 RFO·line split 증가 write combining 불리 4. Non-temporal store base[0] base[1] base[2] … base[7] (대용량 연속 배열) VMOVNTDQ / MOVNTPS write-combining 버퍼 경유 캐시 비오염 저장 대용량 연속 기록에 유리, 끝에 SFENCE
네 가지 메모리 접근 패턴의 비용 구조 — 연속 접근은 캐시라인 1~2개만 접촉해 가장 저렴하고, gather는 인덱스 벡터가 가리키는 여러 캐시라인을 한 번에 모으므로 주소 분산도와 캐시 히트율에 비용이 좌우됩니다. scatter는 여기에 read-for-ownership과 캐시라인 분할이 더해져 write combining도 불리하며, non-temporal store는 write-combining 버퍼를 거쳐 캐시를 오염시키지 않는 대신 대용량 연속 기록에서만 이득이 있고 마지막에 sfence가 필요합니다. 개념 개요 수준의 비용 비교이며, 개별 명령어와 주의점은 아래 표에서 다룹니다.
패밀리대표 명령어설명주의점
Gather FPVGATHERDPS/DPD/QPS/QPD인덱스 벡터로 불연속 주소를 벡터로 모아 읽기캐시라인이 분산되면 거의 스칼라 다중 로드 수준
Gather INTVPGATHERDD/DQ/QD/QQ정수형 불연속 로드주소 계산 µop와 TLB miss 비용 주의
ScatterVSCATTERDPS/DPD, VPSCATTERDD/DQ벡터 값을 불연속 주소로 저장read-for-ownership와 write combining 불리
Mask gather/scatterVGATHER*{%k}, VSCATTER*{%k}활성 레인만 접근fault가 레인 단위로 발생 가능
PrefetchPREFETCHT0/T1/T2, PREFETCHNTA예상 접근 데이터를 미리 cache 계층으로 당기기과도하면 오히려 cache pollution
PF gather hintVGATHERPF0DPS, VSCATTERPF1DPDXeon Phi 계열의 gather/scatter prefetch일반 서버 CPU 지원 범위가 좁음
Streaming loadMOVNTDQAWC 메모리에서 streaming load일반 WB 메모리에는 효과 제한
Streaming storeMOVNTPS, MOVNTDQ, VMOVNTDQwrite-combining 버퍼를 통한 캐시 비오염 저장마지막에 sfence 필요
Split-line 보정LDDQU구형 SSE 환경에서 비정렬 split load 최적화현대 CPU에선 일반 unaligned load와 차이 축소
/* AVX-512 sparse gather 합산 — 희소 인덱스 스캔 패턴 */
#include <immintrin.h>

static inline float horizontal_sum_m512(__m512 v);

float sparse_sum_avx512(const float *base,
                       const uint32_t *index,
                       size_t n)
{
    __m512 acc = _mm512_setzero_ps();
    size_t i = 0;

    for (; i + 16 <= n; i += 16) {
        __m512i idx = _mm512_loadu_si512((const void *)(index + i));
        __m512 val = _mm512_i32gather_ps(idx, base, 4);
        acc = _mm512_add_ps(acc, val);
    }

    if (i < n) {
        __mmask16 tail = (__mmask16)((1u << (n - i)) - 1u);
        __m512i idx = _mm512_maskz_loadu_epi32(tail, index + i);
        __m512 val = _mm512_mask_i32gather_ps(_mm512_setzero_ps(),
                                              tail, idx, base, 4);
        acc = _mm512_add_ps(acc, val);
    }

    return horizontal_sum_m512(acc);
}

커널에서 gather를 쓸 때의 금기: 인덱스가 사용자 공간(User Space) 포인터, 페이지(Page) 경계, 잠재적으로 fault 가능한 주소를 가리키면 gather는 디버깅이 매우 어려워집니다. 주소가 검증되지 않았다면 먼저 스칼라 경로로 안전성을 확보하고, gather는 이미 fault-safe가 보장된 커널 메모리에서만 사용하는 편이 낫습니다.

VPCONFLICT — 충돌 감지(Conflict Detection)와 중복 키

AVX-512CD의 VPCONFLICTD/Q는 벡터의 각 레인 값과 더 낮은 번호의 레인 값 중 같은 값이 있는 위치를 비트마스크로 반환합니다. 값이 유일하면 0이 됩니다. 이 정보로 히스토그램(Histogram)의 원자적 갱신 충돌, scatter의 동일 주소 중복 쓰기, 중복 키 탐색을 벡터 한 번에 판정할 수 있습니다. 커널에서 포인터 배열 재배열, 해시 테이블(Hash Table) 키 정렬, bulk free(같은 folio 면 여러 번 만나는 경우) 처리에 응용됩니다.

VPCONFLICTD zmm1, zmm0 — 중복 값을 가진 하위 레인을 비트로 표시 소스 벡터 하나 안에서 각 레인의 값을 자기보다 앞선 레인들과 비교해, 값이 같은 레인의 위치를 비트로 표시 (zmm은 32비트 16레인 — 그림은 하위 8레인만 표시) 레인1 레인2 레인3 레인4 레인5 레인6 레인7 레인8 입력 zmm0 5 7 5 9 7 11 5 9 결과 zmm1 0b00000000 없음 0b00000000 없음 0b00000001 → 레인1 0b00000000 없음 0b00000010 → 레인2 0b00000000 없음 0b00000101 → 레인1·레인3 0b00001000 → 레인4 결과는 레인마다 32비트 비트마스크 — 비트 k가 1이면 레인(k+1)과 값이 같고, 나머지 상위 비트는 0 (앞선 레인에 같은 값이 없으면 0)
VPCONFLICTD zmm1, zmm0의 충돌 감지 동작 — 소스 벡터 하나에서 각 레인의 값을 자기보다 하위인 레인들과만 비교해, 값이 같은 위치에만 비트를 세우고 하위에 같은 값이 없으면 0이 되는 규칙을 보여줍니다. Intel SDM 유사코드(FOR k := 0 TO j-1 루프에서 SRC[i+31:i] = SRC[m+31:m]이면 DEST[i+k] := 1)에 따른 ISA 동작 수준의 개념도이며, zmm의 32비트 요소 16개 중 하위 8개 레인만 예시로 그렸습니다. 레지스터 할당과 커널 내 SIMD 사용 규칙은 다루지 않습니다.
/* VPCONFLICT — 중복 키 감지 (AVX-512CD intrinsics) */
#include <immintrin.h>

/* 8개 키 중 중복이 있는 레인 위치를 비트마스크로 반환 */
__mmask8 find_duplicate_keys(__m512i keys)
{
    __m512i conf = _mm512_conflict_epi32(keys);  /* 각 레인에 하위 중복 비트마스크 */
    __mmask16 dup = _mm512_test_epi32_mask(conf, conf); /* 0이 아닌 레인 = 중복 */
    return (__mmask8)dup;                       /* 중복 키 레인 1 */
}

/* scatter 전 충돌 검사 — 같은 주소 2회 쓰기 방지 */
int scatter_safe_if_unique(uint32_t *base, __m512i idx)
{
    __m512i conf = _mm512_conflict_epi32(idx);
    __mmask16 dup = _mm512_cmpeq_epi32_mask(conf, _mm512_setzero_si512());
    if (dup != 0xFFFF)        /* 하나라도 중복 주소면 스칼라 폴백 */
        return -1;
    _mm512_i32scatter_epi32(base, idx, _mm512_set1_epi32(1), 4);
    return 0;
}

히스토그램 갱신의 안전성: VPCONFLICT로 중복 키를 발견한 뒤에도 실제 증가 연산은 원자적(Atomic)이어야 합니다. 커널에서 atomic_inc() 계열을 벡터화할 수 없으므로, 중복이 확인된 레인만 스칼라로 처리하거나 카운터 배열을 레인별로 분산(스플릿)하는 패턴이 필요합니다.

x86 변환·혼합 정밀도·VNNI·BF16·FP16

현대 커널 SIMD는 단순 산술보다 형 변환과 데이터 재해석에서 더 많은 시간을 씁니다. 압축된 바이트 배열을 넓혀 계산하고, 계산 결과를 다시 좁혀 저장하며, INT8/BF16/FP16 같은 혼합 정밀도 데이터 경로를 연결합니다. 이미지, 오디오, 암호, 네트워크 분류기, AI 추론 오프로드 모두 이 경로 위에 있습니다.

좁은 형식에서 넓은 형식으로 확장하고, 다시 압축하는 것이 SIMD 파이프라인의 중심입니다 확장 (Widening) — 좁은 형식을 넓혀 계산 원본 바이트 [u8 × 16] VPMOVZXBW [u16 × 16] VPMOVZXWD [i32 × 8] + [i32 × 8] VCVTDQ2PS [fp32 × 8] + [fp32 × 8] fp32 연산 압축 (Narrowing) — 포화 연산으로 좁은 형식 복원 VCVTPS2DQ [i32 × 8] + [i32 × 8] PACKSSDW · PACKUSWB 포화(saturate)로 i16·u8 압축 저장 — 좁은 형식 복원 [u8 × 16] · 대역폭 절감 INT8 추론 경로 — VNNI 점곱 누적 A: u8 activation B: i8 weight VPDPBUSD · VPDPBUSDS 4쌍씩 곱해 i32로 누적 양자화 추론 · 도메인 필터 패턴 분류기에 활용 BF16 / FP16 혼합 정밀도 경로 fp32 weight / input 훈련·전처리 결과 VCVTNEPS2BF16 fp32 → BF16 압축 VDPBF16PS BF16 × BF16 → FP32 누적
x86 형 변환·혼합 정밀도 데이터 경로 — 위쪽은 좁은 정수(u8·u16·i32)를 fp32까지 넓혀 계산한 뒤 VCVTPS2DQ와 PACKSSDW·PACKUSWB로 포화 압축해 다시 좁은 형식(u8)으로 저장하는 왕복을, 가운데는 u8 activation과 i8 weight를 4쌍씩 곱해 i32로 누적하는 VNNI 점곱 경로를, 아래쪽은 fp32를 BF16으로 압축한 뒤 BF16×BF16을 FP32로 누적하는 혼합 정밀도 경로를 나타냅니다. 명령어 의미 수준의 개념 개요이며, 각 변환 명령의 확장 지원과 커널 내 사용 규칙은 아래 표와 코드에서 다룹니다.
패밀리대표 명령어설명대표 용도
zero/sign extendVPMOVZXBW/BD/WD/DQ, VPMOVSXBW/BD/WD/DQ좁은 정수를 넓은 정수로 확장u8 샘플 처리, 양자화 해제
pack / narrowPACKSSWB, PACKUSWB, VPMOVWB, VPMOVUSDB넓은 결과를 포화 또는 truncate 방식으로 좁힘픽셀 저장, clamp
정수↔실수VCVTDQ2PS, VCVTPS2DQ, VCVTTPS2DQi32와 fp32 사이 변환DSP, 통계, 양자화/역양자화
FP16VCVTPH2PS, VCVTPS2PHhalf precision과 fp32 사이 변환압축 모델, 네트워크 오프로드
BF16 변환VCVTNEPS2BF16, VCVTNE2PS2BF16fp32를 BF16으로 반올림 압축추론/학습 데이터 전송량 절감
BF16 점곱VDPBF16PSBF16 × BF16 → FP32 누적추론 가속, 행렬 곱
INT8 점곱VPDPBUSD, VPDPBUSDS, VPDPWSSD, VPDPWSSDSu8/i8 또는 i16 쌍을 i32로 누적VNNI, 필터, 분류기
고정소수점 반올림 곱PMULHRSWQ15 고정소수점에 적합한 곱 + 반올림오디오, 영상 DSP
큰 정수 누적VPMADD52LUQ/HUQ52-bit limb 곱셈 누적RSA, ECC, 다정밀도 정수
클래스 판정VFPCLASSPS/PD/PHNaN, INF, subnormal 여부를 mask로 생성입력 검증, 수치 안전성 검사
/* u8 16개를 fp32 16개로 확장해 scale/zero-point 적용 */
#include <immintrin.h>

static inline void dequant_u8_16_avx2(
    const uint8_t *src, float *dst, float scale, float zero_point)
{
    __m128i raw = _mm_loadu_si128((const __m128i *)src);
    __m256i u16 = _mm256_cvtepu8_epi16(raw);
    __m128i lo16 = _mm256_castsi256_si128(u16);
    __m128i hi16 = _mm256_extracti128_si256(u16, 1);

    __m256i lo32 = _mm256_cvtepu16_epi32(lo16);
    __m256i hi32 = _mm256_cvtepu16_epi32(hi16);
    __m256 zp = _mm256_set1_ps(zero_point);
    __m256 s = _mm256_set1_ps(scale);

    __m256 f0 = _mm256_mul_ps(_mm256_sub_ps(_mm256_cvtepi32_ps(lo32), zp), s);
    __m256 f1 = _mm256_mul_ps(_mm256_sub_ps(_mm256_cvtepi32_ps(hi32), zp), s);

    _mm256_storeu_ps(dst + 0, f0);
    _mm256_storeu_ps(dst + 8, f1);
}
/* 혼합 정밀도 opcode 요약 — 커널 offload 코드에서 자주 보는 묶음 */
vcvtneps2bf16 %zmm0, %ymm1      /* fp32 → bf16 압축 */
vdpbf16ps     %zmm3, %zmm2, %zmm4 /* bf16 점곱 누적 */
vpdpbusd      %zmm6, %zmm5, %zmm7 /* u8 × i8 → i32 누적 */
vpmadd52luq   %zmm9, %zmm8, %zmm10 /* big integer 하위 limb 누적 */
vpmadd52huq   %zmm9, %zmm8, %zmm11 /* big integer 상위 limb 누적 */

VPMADD52 — 52비트 정수 FMA와 큰 수(Big Integer) 연산

AVX-512 IFMA의 VPMADD52LUQ/HUQ는 52비트 정수 곱셈 후 64비트 누적을 한 명령으로 수행합니다. 52비트는 x86 정수 SIMD가 64비트 곱셈을 지원하지 않는 한계를 우회하는 값입니다. 52비트 × 52비트 곱의 하위 52비트(LUQ) 또는 상위 52비트(HUQ)를 더해 목적 레지스터에 누적합니다. RSA, ECC, 몽고메리(Montgomery) 곱셈 같은 다정밀 정수 연산, FFT, 난수 생성의 64비트 이상 누적에서 사용됩니다. 커널의 crypto/ RSA 구현과 파생 난수 풀 연산에서 가속 경로로 채택된 사례가 있습니다.

VPMADD52LUQ — 52×52비트 곱의 하위 52비트를 64비트 누적 src1 — 52비트 limb src2 — 52비트 limb × 52비트 × 52비트 → 104비트 곱 레인(qword)마다 독립해 계산 — 레인 사이로 자리올림이 넘어가지 않음 VPMADD52LUQ — 하위 52비트 dst[63:0] = dst[63:0] + 곱[51:0] 64비트 누적기, 첫 반복은 0에서 시작 VPMADD52HUQ — 상위 52비트 dst[63:0] = dst[63:0] + 곱[103:52] 별도 64비트 누적기를 사용 큰 수 연산 구성 — 52비트 limb 4개 = 208비트 RSA 몽고메리 곱셈 · ECC · FFT 트위들 계수 · 난수 풀 누적에 사용
VPMADD52의 레인 단위 동작 — src1·src2의 52비트 limb를 곱해 104비트 곱을 만들고, 하위 52비트 [51:0]는 VPMADD52LUQ가, 상위 52비트 [103:52]는 VPMADD52HUQ가 각각 64비트 누적기에 더합니다. 명령어 의미 수준의 개념도이며, qword 레인끼리 자리올림이 넘어가지 않고 가산 결과가 64비트로 절단되는 동작은 Intel SDM의 VPMADD52LUQ·HUQ 연산 정의를 따릅니다.
/* VPMADD52 — 52비트 limb 곱 누적 (AVX-512 IFMA intrinsics) */
#include <immintrin.h>

/* 하위 52비트 누적: dst = src1×src2(하위 52비트) + dst */
__m512i madd52_lo(__m512i acc, __m512i a, __m512i b)
{
    return _mm512_madd52lo_epu64(acc, a, b);
}

/* 상위 52비트 누적: dst = src1×src2(상위 52비트) + dst */
__m512i madd52_hi(__m512i acc, __m512i a, __m512i b)
{
    return _mm512_madd52hi_epu64(acc, a, b);
}

/* 208비트 정수 4-limb 곱 — 몽고메리 리덕션 전 단계 */
/* 각 52비트 구간이 레인 하나에 들어가므로, 곱셈·누적이 캐리 없이 4레인 병렬 진행 */
void mul_4limb(__m512i *r_lo, __m512i *r_hi, __m512i acc)
{
    __m512i a = _mm512_set_epi64(0, 0, 0, 0,
                                 limb3, limb2, limb1, limb0);
    __m512i b = _mm512_set_epi64(0, 0, 0, 0,
                                 limb3, limb2, limb1, limb0);
    *r_lo = madd52_lo(acc, a, b);   /* 하위 52비트 누적 */
    *r_hi = madd52_hi(acc, a, b);   /* 상위 52비트 누적 */
}

52비트를 쓰는 이유: 64비트 정수 곱셈은 하나의 SIMD 명령으로 지원되지 않아 32비트 쪼개기(그리고 여러 번의 곱셈·시프트·가산)가 필요합니다. 52비트는 mulx 계열이 아닌 vpmadd52 전용으로 정의된 값으로, 한 번의 곱셈으로 52×52비트까지 정확히 처리해 몽고메리 곱셈의 반복 횟수를 줄입니다. 다만 HW 지원 CPU(IFMA)에서만 동작하므로 boot_cpu_has(X86_FEATURE_AVX512IFMA) 게이팅과 스칼라 폴백이 필요합니다.

ARM NEON·SVE·SVE2 명령어 패밀리 확장

ARM에서는 NEON이 128비트 고정 폭 벡터, SVE/SVE2가 벡터 길이 비종속(VLA) 모델을 담당합니다. 커널 관점에서 중요한 것은 widen/narrow, dot product, predicate 기반 select, fault-first load입니다. NEON은 짧고 예측 가능한 벡터 코드에 강하고, SVE/SVE2는 문자열/메모리 함수처럼 VL에 독립적인 루프에 강합니다.

패밀리NEONSVE / SVE2실전 의미
widenUSHLL, SSHLL, UMLAL, SMLALUUNPKLO/HI, SUNPKLO/HI, UMLALB/Tu8/i8 데이터를 넓혀 누적
narrow / saturating narrowSQXTN, UQXTN, FCVTNTRN + pack 조합, SQXTNT결과를 저장 폭으로 다시 줄임
포화 산술SQADD, UQADD, SQSUB, UQSUBSQADD, UQADD, SQSUB오디오/이미지 clamp
고정소수점 곱SQDMULH, SQRDMULHSQDMULH, SQRDMLAHQ-format DSP
점곱SDOT, UDOTSDOT, UDOT, BFDOTINT8/BF16 추론
쌍별 누적SADALP, UADALP, ADDVSADDLV, UADDV, FADDV체크섬, 통계, reduce
테이블 룩업TBL, TBX, EXTTBL, EXT, SPLICE바이트 분류, 셔플
비트 선택BSL, BIT, BIF, CMTST, CNTSEL, EOR, AND, PTESTmask merge, bit class
구조화 로드/스토어LD2/LD3/LD4, ST2/ST4LD2B/LD3W/LD4D, ST2B인터리브 데이터 분해
gather / scatter일반 메모리 gather 부재LD1W/LD1D gather, ST1W scatter희소 테이블 탐색
fault-first / 탐색별도 전용 명령 부재LDFF1, WHILELT, BRKA, CNTP, MATCHstrlen, 안전 탐색 루프
압축 / pack-active수동 셔플 필요COMPACT, SPLICE필터링 결과를 앞으로 모음
/* ARM에서 자주 보이는 widening → dot → narrowing 패턴 */
ld1     {v0.16b}, [x0], #16      /* 입력 16바이트 */
ld1     {v1.16b}, [x1], #16      /* 가중치 16바이트 */
udot    v2.4s, v0.16b, v1.16b    /* 4개 int32 누적 */
ushll   v3.8h, v0.8b, #0         /* 하위 8바이트 widen */
ushll2  v4.8h, v0.16b, #0        /* 상위 8바이트 widen */
sqxtn   v5.8b, v3.8h             /* 포화 narrow */
sqxtn2  v5.16b, v4.8h

whilelt p0.s, x2, x3             /* SVE tail predicate */
ld1w    z0.s, p0/z, [x4, x2, lsl #2]
ld1w    z1.s, p0/z, [x5, x2, lsl #2]
sdot    z2.s, z0.b, z1.b         /* SVE2 dot product */
compact z3.s, p0, z2.s           /* 활성 요소 압축 */

TBL과 gather를 혼동하지 마세요: NEON TBL은 레지스터 내부 테이블 룩업이고, 메모리에서 임의 주소를 읽어오는 gather가 아닙니다. 메모리 기반 gather가 필요하면 SVE의 indexed load 계열로 넘어가야 합니다.

RISC-V RVV 명령어 패밀리 확장

RVV는 vsetvli 하나로 요소 폭(SEW), 레지스터 그룹(LMUL), 실제 VL을 동시에 설정합니다. 그래서 개별 연산 자체보다 mask 생성, index 생성, fault-first 탐색, segmented load/store를 함께 읽는 습관이 중요합니다. RVV는 x86처럼 movemask가 없고, SVE처럼 별도 predicate 레지스터가 아니라 v0 mask를 벡터 파이프라인 안에서 계속 재활용(Recycling)하는 점이 특징입니다.

패밀리RVV 명령어x86 / ARM 대응대표 활용
기본 설정VSETVLI, VSETIVLISVE의 VL 고정과 대비동적 VL 루프 제어
segmented load/storeVLSEG2E*, VLSEG4E*, VSSEG*LD2/LD4, 구조 분리 로드패킷 헤더, RGB, 복합 구조
strided / indexedVLSE/VSSE, VLUXEI/VSUXEI, VLOXEI/VSOXEIGather / scatter희소 행렬, 테이블 참조
fault-firstVLE8FF.V, VLE32FF.VLDFF1, 문자열 탐색page boundary 안전 탐색
비교 / maskVMSEQ, VMSLT(U), VMAND.MM, VMXOR.MMVPCMP, CMEQ조건 생성, 필터링
mask 소비VCPOP.M, VFIRST.M, VIOTA.MPOPCNT, TZCNT, prefix indexhit 개수, 첫 hit, 압축 인덱스
조건부 mergeVMERGE.VVMBLENDVPS, SEL분기 제거
압축VCOMPRESS.VMVCOMPRESSPS, COMPACT활성 요소 pack
widen / narrowVWADD, VWMUL, VWMACC, VNCLIP(U)PMADDWD, SQXTN양자화, 누적, clamp
slide / gatherVSLIDEUP/DOWN, VSLIDE1UP/DOWN, VRGATHERVPALIGNR, TBL, VPERMD경계 맞춤, 셔플
리덕션VREDSUM, VWREDSUMU, VFREDUSUMHADD/ADDV합계, 체크섬, 통계
포화 산술VSADD(U), VSSUB(U)PADDS*, SQADD이미지, DSP
암호 벡터 확장Zvkb, Zvkg, Zvkned, ZvknhaGFNI, PCLMUL, AES, SHARISC-V crypto 가속
/* RVV memchr / strlen 계열: compare → first-hit → compress */
vsetvli t0, a1, e8, m1, ta, ma   /* 남은 길이에 맞는 VL 결정 */
vle8ff.v v8, (a0)                /* fault-first 로드 */
vmseq.vi v0, v8, 0               /* null 바이트 비교 → mask v0 */
vfirst.m t1, v0                  /* 첫 번째 hit 인덱스, 없으면 -1 */
bgez     t1, .found
viota.m  v10, v0                 /* 각 hit의 prefix index 생성 */
vcompress.vm v12, v8, v0         /* 활성 요소만 앞으로 압축 */

RVV의 강점: x86은 compare 후 보통 스칼라 비트마스크로 내려와야 하고, SVE는 predicate 전용 레지스터를 별도로 다룹니다. RVV는 v0 mask를 vfirst, vcpop, vcompress, vmerge로 곧바로 소비할 수 있어 필터링 파이프라인을 벡터 내부에서 오래 유지하기 쉽습니다.

SIMD 명령어 색인(Index)과 접미사(Suffix) 규칙

사용자가 원하는 것이 "모든 SIMD 명령" 수준의 정리라면, 개별 mnemonic를 무한정 나열하는 것만으로는 부족합니다. 실제 ISA는 명령어 패밀리 + 데이터 폭 + 부호 여부 + 스칼라/packed + 인코딩 장식자(Decorator) 조합으로 확장되기 때문입니다. 아래 표는 x86, ARM, RVV에서 mnemonic를 해독하는 규칙을 먼저 제시합니다. 이 규칙을 이해하면 문서에 개별 opcode가 직접 적혀 있지 않아도 같은 계열 명령을 빠르게 찾을 수 있습니다.

문법아키텍처의미예시
PS / PDx86packed single / packed doubleADDPS, MULPD
SS / SDx86scalar single / scalar double, low lane만 연산VMOVSS, VADDSD
B / W / D / Qx868/16/32/64비트 정수 요소 폭VPADDB, VPCMPEQD
U / Sx86, ARM, RVV부호 없음 / 부호 있음 또는 zero/sign extension 문맥PADDUSB, VMSLTU
V / VP 접두사x86VEX/EVEX 인코딩. 대개 3-operand, 상위 비트 정리, mask 수식 가능VADDPS, VPTERNLOGD
{k}{z}x86 AVX-512mask 적용과 zeroing merge 정책VMOVDQU32 zmm0{k1}{z}
.16b / .8h / .4s / .2dARM NEON레지스터 해석 폭과 레인 수add v0.4s, v1.4s, v2.4s
p0/z, p0/mARM SVE프레디케이트 기반 zeroing / mergingld1w z0.s, p0/z, [x0]
.vv / .vx / .vi / .vmRVV벡터-벡터, 벡터-스칼라, 벡터-즉시값, mask 소비 형태vadd.vv, vmseq.vi
e8 / e16 / e32 / e64RVVSEW(Standard Element Width) 설정vsetvli t0, a0, e32, m4
m1 / m2 / m4 / m8RVVLMUL(Register Group Multiplier)vsetvli t0, a0, e8, m8
ta/tu, ma/muRVVtail/mask agnostic 또는 undisturbed 정책vsetvli ..., ta, ma

정리 원칙: 아래 카탈로그는 opcode를 "패밀리" 단위로 빠짐없이 묶고, 각 패밀리가 다시 데이터 폭과 수식자 조합으로 확장되는 사실까지 함께 설명합니다. 즉, 여기서 패턴을 익히면 실제로는 수백~수천 개의 세부 mnemonic를 모두 찾을 수 있습니다.

x86 SIMD 명령어 완전 카탈로그

x86은 MMX에서 시작해 SSE, AVX, AVX2, AVX-512, GFNI, VNNI, BF16, FP16, AMX까지 매우 길게 확장되었습니다. 같은 알고리즘도 legacy SSE, VEX, EVEX 세 계열로 나뉘고, 데이터 폭과 mask까지 합치면 변형 수가 급격히 늘어납니다. 따라서 x86에서는 "opcode 하나"보다 "패밀리와 확장 비트"를 함께 기억하는 것이 더 중요합니다.

확장(Extension)대표 명령어 패밀리핵심 의미
MMXPADD*, PSUB*, PUNPCK*64비트 정수 SIMD의 출발점
SSEMOVAPS, ADDPS, MULPS, SHUFPS128비트 packed float
SSE2MOVDQA, PADDQ, PMULLW, PSHUFD, MOVSD128비트 정수와 double, scalar double
SSE3 / SSSE3HADDPS, ADDSUBPS, PSHUFB, PALIGNR, PABS*수평 연산, 바이트 셔플, 정렬 보조
SSE4.1 / SSE4.2PBLEND*, PMULLD, PTEST, PCMPESTR*, PCMPISTR*blend, test, 문자열 비교
AES-NI / PCLMULQDQAESENC, AESDEC, AESKEYGENASSIST, PCLMULQDQ대칭키와 GHASH/CRC 가속
AVXVADDPS, VMULPS, VBROADCAST*, VINSERTF128VEX 3-operand, YMM, scalar merge 규칙
F16C / FMAVCVTPS2PH, VCVTPH2PS, VFMADD*FP16 변환과 FMA3
AVX2VPADDD, VPMASKMOV*, VPGATHER*, VPERMD, PMADDUBSW256비트 정수, gather, 정수 셔플 확장
SHA / GFNI / VAES / VPCLMULQDQSHA256RNDS2, GF2P8AFFINEQB, VAESENC, VPCLMULQDQ현대 암호/비트 행렬 가속
AVX-VNNI / AVX-512 VNNIVPDPBUSD, VPDPBUSDS, VPDPWSSD, VPDPWSSDSINT8/INT16 점곱 누적
AVX-512F/BW/DQ/VLVPCMP*, VCOMPRESS*, VEXPAND*, KAND/KOR/KTEST512비트, mask 레지스터, compress/expand
AVX-512 CD/VBMI/VBMI2/BITALG/VPOPCNTDQVPCONFLICT*, VPERMB, VPSHUFBITQMB, VPOPCNT*충돌 탐지, 바이트 셔플, 비트 알고리즘
AVX-512 BF16 / FP16 / IFMA / VP2INTERSECTVDPBF16PS, VCVTNEPS2BF16, VCVTPH2PS, VPMADD52*, VP2INTERSECT*혼합 정밀도, 다정밀도 정수, 집합 교차
AMXTILELOADD, TILESTORED, TDPBUSD, TDPBF16PS행렬 타일 연산
영역명령어 패밀리세부 mnemonic 묶음설명
연속 이동MOV*MOVAPS/APD/UPS/UPD/DQA/DQU, VMOVDQU8/16/32/64정렬/비정렬 로드와 스토어
스칼라 이동MOVSS/MOVSDMOVSS, VMOVSS, MOVSD, VMOVSDlow lane만 이동, upper lane merge/zero 규칙 존재
브로드캐스트VBROADCAST*VBROADCASTSS/SD, VBROADCASTF32X2/F32X4/F64X2, VPBROADCASTB/W/D/Q스칼라 또는 작은 레인을 전체 레인으로 복제
삽입/추출PINSR*/PEXTR*, VINSERT*/VEXTRACT*PINSRB/W/D/Q, PEXTRB/W/D/Q, VINSERTF128/I128/I32X4/I64X2, VEXTRACT*레지스터 일부 교체, lane 입출력
정수 산술PADD*/PSUB*PADDB/W/D/Q, PSUBB/W/D/Q, VPADD*, VPSUB*기본 레인별 정수 산술
포화/평균/절댓값PADDS*/PAVG/PABS/PSIGNPADDSB/SW, PADDUSB/USW, PAVGB/W, PABSB/W/D, PSIGNB/W/DDSP와 이미지 처리 핵심
min/max/비교 선택PMIN*/PMAX*, MINPS/MAXPSPMINSB/PMAXUB, VMINPS, VMAXPD, VMINSS/VMAXSD클램프, 임계값 분기 제거
곱셈과 점곱PMUL*/PMADD*/VPDP*PMULLW/HW, PMULDQ, PMADDWD, PMADDUBSW, VPDPBUSD, VDPBF16PS고정소수점, INT8, BF16 추론
큰 정수VPMADD52*VPMADD52LUQ, VPMADD52HUQRSA/ECC limb 누적
논리/비트PAND/POR/PXOR/VPTERNLOGPAND, PANDN, POR, PXOR, VPTERNLOGD/Q, KAND/KOR/KXOR/KNOT비트슬라이스, mask 조합
시프트/회전PSLL/PSRL/PSRA/PROL/PRORPSLLW/D/Q, VPSLLV*, VPSRAV*, VPROLD/Q, VPRORD/Q레인별 시프트와 rotate
셔플/퍼뮤트SHUF*/PSHUF*/VPERM*SHUFPS/PD, PSHUFD, PSHUFB, PALIGNR, VPERMB, VPERMD, VPERMT2*바이트~lane 수준 재배열
언팩/팩PUNPCK*/PACK*PUNPCKLBW/HBW/LWD/HWD, PACKSSWB, PACKUSWB인터리브와 narrowing
비교/테스트PCMPEQ/PCMPGT/VPCMP/PTESTPCMPEQB/W/D/Q, PCMPGT*, VPCMP*, PTEST, VTESTPS/PD조건 생성과 빠른 zero/all 검사
문자열/텍스트PCMPESTR*/PCMPISTR*PCMPESTRI/M, PCMPISTRI/M, MOVMSKmemchr, strcmp, delimiter 탐색
수평/축소HADD/HSUB/DPPSHADDPS/PD, HSUBPS/PD, DPPS/DPPD, PHADDW/D리덕션 보조 연산
FP 기본 연산ADD/MUL/SUB/DIV/SQRT/RCP/RSQRTADDPS/PD/SS/SD, MUL*, DIV*, SQRT*, RCPPS, RSQRTPSpacked/scalar FP 전 범위
FMA와 반올림VFMADD*/ROUND*/VFPCLASSVFMADD132/213/231*, VFNMADD*, ROUNDPS/PD/SS/SD, VFPCLASS*정밀도 제어와 수치 클래스 판정
변환CVT*CVTDQ2PS, CVTPS2DQ, CVTTPS2DQ, VPMOVZX*, VPMOVSX*, VCVTPH2PS, VCVTPS2PH정수↔실수, 폭 확장/축소, FP16
압축/확장VCOMPRESS*/VEXPAND*VCOMPRESSPS/PD, VPCOMPRESSD/Q, VEXPANDPS/PD마스크 기반 sparse pack/unpack
충돌/교차VPCONFLICT*/VP2INTERSECT*VPCONFLICTD/Q, VP2INTERSECTD/Q해시, 집합 membership, 중복 탐지
gather/scatterVGATHER*/VSCATTER*VGATHERDPS/DPD/QPS/QPD, VPGATHERDD/DQ, VSCATTER*, VPSCATTER*불연속 메모리 접근
비트 알고리즘VPOPCNT/VPSHUFBITQMB/GF2P8*VPOPCNTB/W/D/Q, VPSHUFBITQMB, GF2P8AFFINEQB, GF2P8MULB비트셋, CRC, 비트 행렬
암호AES/VAES/PCLMUL/SHA/CRC/GFNIAESENC/DEC, VAESENC, PCLMULQDQ, SHA1*/SHA256*, CRC32, GFNIdm-crypt, IPsec, WireGuard

x86에서 "모든 명령"을 읽는 방법: 예를 들어 VPADDUSB를 보면 V=VEX/EVEX, PADD=정수 packed add, U=unsigned saturating, B=byte라는 뜻입니다. 같은 방식으로 VPMOVUSDB, VPCMPUB, VFPCLASSPH 같은 긴 mnemonic도 해독할 수 있습니다.

ARM NEON·SVE·SVE2·SME 완전 카탈로그

ARM 계열은 x86처럼 opcode 이름이 길게 누적되기보다, 같은 명령어에 레지스터 타입과 프레디케이트가 결합되어 의미가 결정되는 경우가 많습니다. 특히 SVE2는 NEON의 바이트/비트 조작을 VLA(Vector Length Agnostic) 방식으로 끌어올리고, SME는 행렬 타일을 별도 레지스터 공간으로 분리합니다.

영역NEONSVE / SVE2 / SME설명
연속 로드/스토어LD1/ST1LD1B/H/W/D, ST1B/H/W/D기본 unit-stride 벡터 접근
구조화 로드/스토어LD2/LD3/LD4, ST2/3/4LD2B/LD3W/LD4D, ST2B 등인터리브 데이터 분리/결합
산술ADD/SUB/MUL/MLA/FMLAADD, SUB, MUL, MLA, FMLA, MAD정수와 FP 기본 연산
포화 산술SQADD/UQADD, SQSUB/UQSUBSQADD/UQADD, SQSUB/UQSUB클램프가 필요한 DSP 패턴
고정소수점 곱SQDMULH, SQRDMULHSQDMULH, SQRDMLAH, SQRDMLSHQ-format 오디오/영상 처리
widenUSHLL/SSHLL, UMLAL/SMLALUUNPKLO/HI, SUNPKLO/HI, UMLALB/T, SMLALB/T좁은 입력을 넓혀 누적
narrowSQXTN/UQXTN, FCVTN/FCVTLSQXTNT, UQXTNT, FCVTNT넓은 결과를 저장 폭으로 축소
쌍별/리덕션ADDP, ADDV, SADALP, UADALP, FADDPADDV, FADDV, SMAXV, UMAXV, CLASTA/B합계, 최대/최소, prefix 종료
비교/선택CMEQ/CMGT/CMHI, BSL/BIT/BIFCMP*, SEL, BRKA/BRKB, PTEST조건 생성과 mask merge
비트 조작CNT, CMTST, RBIT, REV64EOR3, BCAX, BDEP/BEXT 유사 비트 조합, CNT 계열암호와 비트셋 처리
셔플/전치ZIP1/2, TRN1/2, UZP1/2, EXTZIP/TRN/UZP/EXT/SPLICElane 재배열과 전치
테이블 룩업TBL/TBXTBL, MATCH, HISTCNT바이트 분류, 문자열 검색, 히스토그램
점곱/복소수SDOT/UDOT, FCMLASDOT/UDOT, BFDOT, FDOT, FCMLAINT8/BF16/복소수 DSP
gather/scatter일반 NEON은 부재LD1W/LD1D gather, ST1W/ST1D scatter희소 인덱스와 테이블 참조
fault-first전용 명령 없음LDFF1B/H/W/D, RDFFR, SETFFRpage 경계 안전 탐색
압축/필터수동 셔플COMPACT, SPLICE활성 요소 pack
predicate 제어해당 없음PTRUE, PFALSE, WHILELT, WHILERW, CNTPVLA 루프와 tail 제어
암호 확장AESE/AESD/AESMC/AESIMC, PMULL, SHA1*, SHA256*SVE2 AES/PMULL, SHA3, SM3, SM4 계열커널 crypto API 가속
SME 행렬해당 없음SMSTART/SMSTOP, FMOPA, SMOPA, BFMOPA, LD1/ST1 ZA타일 행렬 누적

ARM에서 빠지기 쉬운 구분: TBL/TBX는 레지스터 내부 룩업이고, SVE의 gather는 메모리 기반 임의 주소 접근입니다. 또 SVE2의 SDOT/UDOT/BFDOT은 NEON과 비슷해 보여도 프레디케이트와 VL 독립성을 함께 고려해야 합니다.

RISC-V RVV 완전 카탈로그

RVV는 x86처럼 opcode 종류가 많으면서도, 동시에 SVE처럼 벡터 길이 비종속 모델을 갖습니다. 따라서 RVV를 완전히 이해하려면 연산 mnemonic뿐 아니라 vsetvli, mask 정책, load/store addressing mode, ordered/unordered 메모리 의미를 함께 읽어야 합니다.

영역RVV 명령어 패밀리설명x86 / ARM 대응
설정VSETVLI, VSETIVLI, VSETVLSEW, LMUL, VL, tail/mask 정책 결정SVE의 VL + predicate 초기화
연속 로드/스토어VLE8/16/32/64, VSE8/16/32/64unit-stride 메모리 접근VMOVDQU, LD1/ST1
segmentedVLSEG2E*, VLSEG3E*, VLSEG4E*, VSSEG*구조체/인터리브 데이터 분리LD2/LD4
stridedVLSE*, VSSE*고정 stride 접근수동 주소 계산 + load/store
indexed unorderedVLUXEI*, VSUXEI*순서 비보장 임의 주소 접근VGATHER/VSCATTER
indexed orderedVLOXEI*, VSOXEI*프로그램 순서를 보장하는 indexed 접근ordered gather/scatter에 가까움
fault-only-firstVLE8FF.V, VLE16FF.V, VLE32FF.V, VLE64FF.V첫 fault까지만 보장하는 문자열/탐색 로드LDFF1
정수 산술VADD, VSUB, VRSUB, VMUL, VDIV, VREM기본 정수 벡터 산술VPADD*, VMUL*, DIVPS의 정수판
widenVWADD, VWSUB, VWMUL, VWMACC*폭을 넓혀 누적PMADDWD, UMLAL/SMLAL
포화/고정소수점VSADD(U), VSSUB(U), VSMUL, VNCLIP(U), VSSRL/VSSRADSP용 saturating과 rounded shiftPADDS*, SQADD, SQDMULH
비교/mask 생성VMSEQ, VMSNE, VMSLT(U), VMSLE(U), VMSGT(U)mask 벡터 생성VPCMP*, CMEQ/CMGT
mask 조작VMAND.MM, VMOR.MM, VMXOR.MM, VMNOT.Mmask 자체의 Boolean 연산KAND/KOR, predicate logic
mask 소비VCPOP.M, VFIRST.M, VIOTA.M, VID.Vpopcount, first-hit, prefix index, lane idPOPCNT/TZCNT, CNTP
조건부 mergeVMERGE.VVM, VMV.V.Vmask 기반 선택과 moveVPBLEND*, SEL
셔플/슬라이드VRGATHER, VSLIDEUP/DOWN, VSLIDE1UP/DOWN인덱스 재배치와 경계 이동VPERM*, PALIGNR, EXT
압축VCOMPRESS.VM활성 요소를 앞으로 밀착 배치VCOMPRESS*, COMPACT
리덕션VREDSUM, VREDMAX(U), VREDMIN(U), VWREDSUM(U), VFREDUSUM벡터→스칼라 축소HADD/ADDV
부동소수점VFADD, VFSUB, VFMUL, VFDIV, VFSQRT, VFMACC, VFNMACCFP packed arithmeticVADDPS, FMLA
근사 역수VFREC7, VFRSQRT7빠른 reciprocal/rsqrt seedRCPPS, RSQRTPS
부호/병합VFSGNJ, VFSGNJN, VFSGNJX, VFMERGE, VFMVsign injection과 scalar mergeXOR-sign, BLEND
암호 벡터 확장Zvkb, Zvkg, Zvkned, Zvknha, Zvknhb, Zvksed, Zvksh비트 조작, GCM, AES, SHA, SM4/SM3GFNI, AES-NI, SHA-NI

RVV mnemonic 읽기 팁: vwmaccsu.vx라면 vw=widen, macc=multiply-accumulate, su=signed×unsigned 조합, .vx=벡터-스칼라 형태라는 뜻입니다. RVV는 이름만 읽어도 데이터 경로가 상당 부분 드러납니다.

최신 확장 현황 (2025-2026)

SIMD 생태계는 2025~2026년에 AVX10.1/AVX10.2 상용화, SME2/SME2.1 성숙, RISC-V Zicfiss/Zicfilp(CFI) 사용자 공간 지원으로 큰 전환점을 맞이했습니다. x86에서는 Granite Rapids(2024, Xeon)의 AVX-512 구현이 AVX10.1의 기준 서브셋을 정의했고, Panther Lake(2026, 클라이언트)는 AVX10.1-256 프로필로 출시되었습니다. 소비자 CPU의 512비트 SIMD 도입은 Nova Lake(2027 예정, AVX10.2-512) 단계로 유보되었고, Diamond Rapids(2026 예정, Xeon)이 AVX10.2-512로 이어가는 로드맵입니다 (제품별 지원 세대는 업체 로드맵 기준이므로 출시 시점 사양을 확인해야 합니다). 각 아키텍처의 ISA 확장이 병합된 커널 버전과 툴체인 상태를 정리합니다.

x86 — AVX10.2 / AMX / VAES

확장도구체인 상태 (2026-08)커널 연계
AVX10.1 GCC 14, Clang 18에서 공식 지원. Granite Rapids(2024, Xeon)의 AVX-512 구현이 AVX10.1의 기준 서브셋을 정의. Panther Lake(2026, 클라이언트)는 AVX10.1-256 프로필(제품별 지원 세대는 업체 로드맵 기준) v6.11부터 VAES AES-GCM 어셈블리 구현(aes-gcm-aesni / aes-gcm-avx10 분리, Eric Biggers). VAES 경로는 AVX-512·AVX10 지원 CPU에서 동작하며, AVX-512 미지원 CPU는 AES-NI 경로로 폴백합니다. v6.19에서 VAES+AVX2 경로(aes-gcm-vaes-avx2)가 추가되어 AVX-512 없는 CPU도 VAES를 사용할 수 있게 되었습니다
AVX10.2 GCC 15 공식 지원(2025 릴리스). Intel 60th ISA 매뉴얼(2025-11)에서 Nova Lake 지원 공식 확인. Diamond Rapids(Xeon, 2026 예정)에서 AVX10.2-512 최초 상용화 예정 E코어/P코어 공통 256비트 경로 상용화. Nova Lake(2027 예정)에서 P코어·E코어 양측 AVX-512 지원으로 하이브리드 비대칭 문제 해소. 커널 crypto·xor·copy 알고리즘에 점진 적용
AMX (Advanced Matrix Extensions) AMX-INT8·AMX-BF16은 Sapphire Rapids(4세대 Xeon, 2023)에서, AMX-FP16·AMX-COMPLEX는 Granite Rapids(Xeon 6, 2024)에서 도입되었습니다(AMX-COMPLEX는 Granite Rapids-D 한정). Diamond Rapids에서는 AMX-FP8·AMX-AVX512·AMX-TRANSPOSE 계열이 예정되어 있습니다(업체 ISA 매뉴얼·컴파일러 지원 기준) 커널 내 직접 사용은 드물고, 유저스페이스(ML 워크로드)에서 활용. XSAVE 영역 확장으로 컨텍스트 스위치 비용 고려 필요. KVM 게스트 AMX 지원 패치(Patch) 진행 중
FRED + SIMD 상태 전환 Linux 6.9 초기 도입. 현재는 fred=on 부팅 옵션으로 활성화하며, 기본 활성화는 Linux 7.1 대상으로 진행 중입니다. 하드웨어는 Panther Lake(2026, Core Ultra 시리즈 3)가 최초 지원 FRED는 이벤트 진입·복귀용 전용 스택 수준(RSP0~RSP3)과 MSR 기반 전달 경로를 사용합니다(MSR_IA32_FRED_RSP0~RSP3, MSR_IA32_FRED_STKLVLS). AMD Zen 6도 FRED 채택 예정
APX (Advanced Performance Extensions) Diamond Rapids·Nova Lake에서 하드웨어 구현 예정(업체 로드맵 기준) Linux 6.16에서 커널 지원 추가(x86/fpu/apx: Define APX state component, x86/cpufeatures: Add X86_FEATURE_APX, 2025-04-16). 범용 레지스터를 16개에서 32개로 확장(r16~r31)하고, XSAVE 상태 컴포넌트 19번(XFEATURE_APX, apx_state.egpr[16])을 사용합니다. SIMD와 직접 관련은 없으나 레지스터 파일 확장으로 컴파일러 레지스터 할당 여유가 늘어 간접적으로 SIMD 성능에 도움이 됩니다

AVX10 버전 체계: AVX10은 AVX-512의 후속으로 설계된 통합 벡터 ISA입니다. 버전별 차이는 다음과 같습니다.

  • AVX10.1 — Granite Rapids(2024 Xeon)·Panther Lake(2026 클라이언트)에서 구현. AVX-512 Foundation 기능을 버전 기반 열거(version-based enumeration)로 재구성. 256비트 프로필(클라이언트 P코어/E코어 공통)과 512비트 프로필(서버) 선택 가능
  • AVX10.2 — Diamond Rapids(2026 예정 Xeon)·Nova Lake(2027 예정 클라이언트)에서 구현 예정. AVX-512의 모든 서브셋(AVX-512F/BW/CD/DQ/VL 등)을 통합하고 128/256/512비트 세 폭을 하나의 모델로 정리. Nova Lake에서는 P코어와 E코어 양측 모두 512비트를 지원하여 하이브리드 비대칭 문제를 근본적으로 해소할 예정

핵심 변화는 "CPUID 비트를 수십 개 개별 확인"에서 "AVX10 버전 번호 하나로 기능 묶음 확인"으로 전환된 것입니다. 이는 커널 cpufeatures.h 탐지 로직을 단순화하고, 소프트웨어 호환성 문제를 줄입니다.

여기서 언급한 제품 세대(Granite Rapids·Panther Lake·Diamond Rapids·Nova Lake)와 프로필 조합은 업체 로드맵 기준이며, 출시 시점의 실제 CPUID 지원 여부와 다를 수 있습니다.

ARM — SVE2 / SME / SME2 / SME2.1

확장핵심 특징병합 버전
SVE / SVE2 가변 길이 벡터, predicate 레지스터 메인라인 병합 완료. sve는 HWCAP_SVE, sve2는 HWCAP2_SVE2로 노출(sve와 sve2의 노출 비트가 서로 다른 네임스페이스(Namespace)에 위치)
SME (v1) 행렬 연산 ZA 배열, AT/AS state, streaming 모드 Linux 5.19부터 메인라인
SME2 ZT0 레지스터 추가, multi-vector 명령 군 Linux 6.3에서 HWCAP2_SME2 노출로 초기 지원. v6.16(2025-07)에 ARM64 SME·lazy preemption 관련 코드가 대거 병합
SME2.1 벡터 처리량 확대, ZA·ZT0 결합 Linux 6.3에서 HWCAP2_SME2P1 노출로 초기 지원, 이후 SME_SBITPERM·SME_AES·SME_SMOP4 등 세부 기능이 점진 추가
GCS (Guarded Control Stack) ROP 대응 하드웨어 리턴 주소 스택 Linux 6.13에서 사용자 공간 지원
AMX ↔ SME 매핑: x86-64 AMX 커널을 ARM으로 이식할 때 타깃은 SME입니다. AMX는 주로 내적(dot product)을 표현하지만 SME는 외적(outer product) 중심이므로, 알고리즘을 outer-product 구조로 재배치해야 성능이 나옵니다. 두 확장 모두 행렬 연산 가속이라는 목표는 같지만 ISA 철학이 다르다는 점에 주의합니다.

RISC-V — RVV / Zicfiss / Zicfilp / Zicbop

확장역할커널 병합
RVV 1.0 (vector) 가변 길이 벡터, LMUL/SEW/VL 제어 메인라인 안정화
Zaamo / Zalrsc 원자 메모리 연산 분리(AMO / LR-SC) Linux 6.15 (2025-05)
Zicbom / Zicntr / Zihpm 캐시 관리, 카운터, 성능 모니터 Linux 6.15
BFloat16 AI/ML 부동소수점 Linux 6.15
Zicbop (프리페치 힌트) 비준 상태, 유저스페이스 노출 Linux 6.19 (2026-02) — hwprobe()의 RISCV_HWPROBE_KEY_ZICBOP_BLOCK_SIZE로 감지, 같은 릴리스에서 tools/testing/selftests/riscv/hwprobe/cbo.c에 test_zicbop() 추가
Zicfiss / Zicfilp Shadow Stack / Landing Pad (CFI) Linux 7.0 (2026-04) — x86 CET/ARM64 GCS와 역할 동등
Zacas 원자 compare-and-swap(Zaamo 의존) Linux 6.8에서 hwprobe로 노출, 6.13에서 arch_cmpxchg128() 구현
T-Head xtheadvector T-Head 벤더 벡터 확장 Linux 6.14

커널 6.10~6.19 x86 암호 서브시스템 SIMD 변경사항

커널 6.10부터 6.19 사이에 x86 암호 서브시스템의 SIMD 활용 방식이 크게 개선되었습니다. 가장 핵심적인 변화는 소프트 인터럽트(softirq) 컨텍스트에서의 FPU 사용이 6.15에서 안정화된 것입니다. 아래 표의 커널 버전과 커밋 날짜는 커널 소스 트리(git.kernel.org)의 태그·이력으로 확인한 값입니다.

커널 버전변경 내용성능 영향
6.10 (2024-07) AES-XTS 어셈블리 구현(aes-xts-avx-x86_64.S)이 병합되었습니다(Eric Biggers, Google). AESNI+AVX 경로와 함께 VAES+AVX2, VAES+AVX10/256, VAES+AVX10/512 세 가지 가속 경로가 배선되었습니다 VAES 지원 CPU에서 AES-XTS 가속 경로 사용
6.11 (2024-09) AES-GCM 어셈블리 재작성(2024-06-07 커밋). 하나의 파일이던 구현이 AES-NI 경로(aes-gcm-aesni-x86_64.S)와 VAES·AVX-512/AVX10 경로(aes-gcm-avx10-x86_64.S) 두 파일로 분리되었습니다 VAES 지원 CPU에서 AES-GCM 가속
6.14 (2025-03) AES-XTS 어셈블리 추가 최적화 6건(2024-12-21 커밋): 명령 수 감소, 코드 크기 축소, 레지스터 별칭(alias)의 함수별 분리, .irp 활용 정리 코드 크기·명령 수 감소
6.15 (2025-05) 커널 모드 FPU를 softirq에서 안정적으로 사용 가능하도록 x86/fpu를 개선 ("x86/fpu: Improve crypto performance by making kernel-mode FPU reliably usable in softirqs", 2025-03-06 커밋). 개별 x86 암호 드라이버가 cryptd 워크큐 지연 없이 softirq에서 직접 SIMD 경로를 실행할 수 있게 되었습니다. 같은 시기에 AES-CTR 어셈블리 재작성과 VAES 지원 (aes-ctr-avx-x86_64.S, 2025-02-22 커밋)이 포함되었습니다 softirq 컨텍스트에서 직접 SIMD 경로 실행
6.16 (2025-07) AES-XTS·AES-CTR에서 avx10_256(256비트) 코드를 제거 ("crypto: x86/aes - drop the avx10_256 AES-XTS and AES-CTR code")하고, AES-XTS의 첫 tweak 계산(_compute_first_set_of_tweaks)을 AVX-512로 최적화했습니다 (모두 2025-04-07 커밋) AVX10 가속 경로 단순화, tweak 계산 최적화
6.16 (2025-07) Intel APX 지원 추가: 범용 레지스터를 16개에서 32개로 확장 (r16~r31), XSAVE 상태 컴포넌트 19번(XFEATURE_APX)과 X86_FEATURE_APX를 정의했습니다 (2025-04-16 커밋, arch/x86/include/asm/fpu/types.h·cpufeatures.h). 로드/저장 명령 감소로 코드 밀도와 전력 효율 개선 레지스터 압박 완화로 컴파일러 생성 코드 품질 개선
6.18 (LTS) 2025년 LTS(Long Term Support) 커널로 지정되었습니다(kernel.org longterm 브랜치 6.18.56 유지, 2026-10 기준) 장기 지원 기준선
6.19 (2026-02) AES-GCM에 VAES+AVX2 경로를 추가(aes-gcm-vaes-avx2.S)하고, aes-gcm-avx10-x86_64.S를 aes-gcm-vaes-avx512.S로 개명했습니다. 어셈블리 내부 명칭도 avx10/avx10_512 → avx512로 통일했습니다(2025-10-26 커밋) AVX-512 미지원 CPU에서도 VAES 가속 사용
softirq FPU 해제 배경 (6.15): 기존에는 소프트 IRQ 컨텍스트에서 kernel_fpu_begin()을 호출할 수 없었기 때문에 crypto/simd.c가 SIMD 알고리즘을 프로세스 컨텍스트(cryptd 워크큐)로 지연시키는 래퍼를 제공했습니다. 6.15부터는 x86/fpu 레이어가 softirq에서도 FPU 상태를 안전하게 저장·복원하므로, AES-NI 등 개별 드라이버가 cryptd 지연 없이 직접 SIMD 경로를 실행할 수 있게 되었습니다. crypto/simd.c 래퍼 자체는 이후에도 유지되어 드라이버가 선택적으로 사용합니다. arm, arm64, RISC-V는 이미 softirq에서 FPU/NEON/벡터를 허용하고 있었으므로 x86이 다른 아키텍처와 동일한 기준이 되었습니다.

커널 6.15부터: may_use_simd()가 softirq 컨텍스트에서도 true를 반환할 수 있습니다. x86 암호 드라이버에서 crypto/simd.c 래퍼 의존도가 낮아집니다.

포터블 SIMD 집필 전략 (2025-2026)

AVX10.2와 RVV, SVE2가 동시에 성숙하면서 "한 소스로 여러 ISA 대응"이 현실 과제가 되었습니다. 2026년 기준 권장되는 접근:

문서 완전성 가이드

이 문서는 이제 단순한 SIMD 소개가 아니라, 명령어 패밀리별 레퍼런스로 읽히도록 구성되어 있습니다. 개별 예제는 성능과 커널 사용 규칙을 보여 주고, 완전 카탈로그는 빠진 mnemonic 없이 계열을 찾도록 돕습니다. 새로운 ISA 확장 문서를 읽을 때도 아래 순서로 접근하면 빠르게 구조를 잡을 수 있습니다.

  1. 확장 비트 확인 — x86은 CPUID/XCR0, ARM은 HWCAP/HWCAP2, RISC-V는 ISA 문자열과 HWCAP을 먼저 봅니다.
  2. 패밀리 확인 — move, compare, shuffle, widen, dot, reduce, gather 중 어느 영역인지 먼저 정합니다.
  3. 접미사 해독 — 데이터 폭, 부호, scalar/packed, mask 정책, indexed 형태를 읽습니다.
  4. upper-lane 규칙 확인 — x86 scalar VEX, SVE predicate merge, RVV tail/mask undisturbed처럼 결과 보존 규칙을 반드시 점검합니다.
  5. 커널 제약 확인 — FPU 상태, preemption, IRQ 컨텍스트, fault 가능 메모리 여부를 점검합니다.

참고자료

SIMD를 실전 구현 관점에서 공부할 때는 먼저 커널 공식 문서로 사용 규칙을 확인하고, 다음으로 실제 커널 소스와 ISA 레퍼런스를 보는 순서가 효율적입니다. 아래 목록은 그 흐름에 맞춰 정리했습니다.

커널 공식 문서

커널 소스 코드

ISA와 벤더 레퍼런스

SIMD와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.

필수 관련 문서: