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) 절차까지 실전 커널 최적화 포인트를 다룹니다.
핵심 요약
- 레지스터 계층 — XMM(128-bit) ⊂ YMM(256-bit) ⊂ ZMM(512-bit), SSE→AVX→AVX-512 순서로 확장.
- 커널 SIMD 제한 — XMM/YMM/ZMM은 유저 프로세스도 쓰는 실제 CPU 레지스터이므로, 커널이 잠깐 빌려 쓸 때도
kernel_fpu_begin()/kernel_fpu_end()로 보호해야 안전합니다. - FPU 상태 저장 — context switch 시 XSAVE/XRSTOR로 FPU/SIMD 상태를 task 구조에 저장.
- ARM NEON/SVE — ARM64 커널에서도 동일 원칙 적용, fpsimd_save/load 사용.
- CPUID 확인 — 런타임에 SIMD 기능 존재 여부를 확인한 후 사용.
단계별 이해
- 레지스터 구조 파악
XMM/YMM/ZMM 계층 구조와 각 확장(SSE/AVX/AVX-512) 관계를 먼저 이해합니다. - 커널 사용 규칙 확인
kernel_fpu_begin()/kernel_fpu_end()는 단순 관례가 아니라, 유저 프로세스의 계산 상태를 보존하기 위한 필수 보호 장치입니다. - ARM 대응 확인
x86 SIMD가 ARM에서 어떻게 매핑(Mapping)되는지(NEON, SVE) 비교합니다. - 실제 커널 활용 사례
crypto, CRC32, memcpy 최적화에서 SIMD 사용 패턴을 분석합니다.
x86 SIMD 확장 역사
| 확장 | 레지스터 | 비트 폭 | 도입 | 주요 기능 |
|---|---|---|---|---|
| MMX | MM0-MM7 | 64-bit | Pentium MMX (1997) | 정수 SIMD, FPU 레지스터 공유 |
| SSE | XMM0-XMM7 | 128-bit | Pentium III (1999) | 단정밀도 부동소수점 4개 병렬 |
| SSE2 | XMM0-XMM15 (64-bit) | 128-bit | Pentium 4 (2001) | 배정밀도, 정수 128-bit. x86-64 기본 지원 |
| SSE3/SSSE3/SSE4 | XMM | 128-bit | 2004-2008 | 수평 연산, 문자열 비교, CRC32 |
| AVX | YMM0-YMM15 | 256-bit | Sandy Bridge (2011) | 256-bit 부동소수점, VEX 인코딩 |
| AVX2 | YMM0-YMM15 | 256-bit | Haswell (2013) | 256-bit 정수, Gather, FMA3 |
| AVX-512 | ZMM0-ZMM31, k0-k7 | 512-bit | Xeon Phi / Skylake-X (2016) | 512-bit, 마스크 레지스터, scatter/gather |
| AMX | TMM0-TMM7 | 타일 (최대 1KB) | Sapphire Rapids (2023) | 행렬 곱셈 가속 (INT8/BF16) |
| AVX10 | ZMM/YMM/XMM | 128~512-bit | Granite Rapids (2024+) | AVX-512 통합 후속, 벡터 길이 프로필 |
x86 SIMD 레지스터 레이아웃
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) */
암호화(Encryption) 전용 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-bit | ARMv7 / ARMv8 | 정수+부동소수점 SIMD, 암호 확장(CE) |
| SVE (Scalable Vector) | Z0-Z31, P0-P15 | 128~2048-bit | ARMv8.2-A | 가변 벡터 길이, 프레디케이트 레지스터 |
| SVE2 | Z0-Z31 | 128~2048-bit | ARMv9-A | NEON 명령어 세트 포괄, 암호 확장 |
| SME | ZA 타일 | SVE VL × SVE VL | ARMv9.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)을 하드웨어에서 직접 수행합니다.
| 구분 | SVE2 | SME / 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 함수 실전 가이드
x86 Intrinsic 함수는 인라인 어셈블리 없이 C/C++ 코드에서 SIMD 명령어를 직접 호출하는 인터페이스입니다. 커널은 주로 어셈블리를 선호하지만, Intrinsic은 유저 공간 최적화와 컴파일러 생성 SIMD 이해에 필수적입니다.
헤더 체계와 네이밍 규칙
| 헤더 | 제공 Intrinsic 접두사 | 레지스터 | ISA |
|---|---|---|---|
<xmmintrin.h> | _mm_* (float) | XMM0-XMM7 | SSE |
<emmintrin.h> | _mm_* (int/double) | XMM0-XMM15 | SSE2 |
<tmmintrin.h> | _mm_shuffle_* | XMM | SSSE3 |
<smmintrin.h> | _mm_blend_* | XMM | SSE4.1 |
<wmmintrin.h> | _mm_aes* | XMM | AES-NI |
<immintrin.h> | _mm256_*, _mm512_* | YMM / ZMM | AVX+ |
네이밍 규칙 예시:
_mm256_add_epi32→vpaddd ymm0, ymm1, ymm2(어셈블리 변환)_mm512_mask_add_epi32→vpaddd zmm0{k1}, zmm1, zmm2
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 파일 */
커널에서의 SIMD 사용 — 핵심 제약
왜 kernel_fpu_begin()과 kernel_fpu_end()가 꼭 필요한가
핵심은 XMM/YMM/ZMM 레지스터가 함수마다 새로 생기는 개인 메모장이 아니라, 현재 CPU가 실제로 들고 있는 공용 작업 공간이라는 점입니다. 유저 프로세스가 이미지 처리, 브라우저 엔진, 데이터베이스 계산처럼 SIMD를 많이 쓰는 작업을 하다가 시스템 콜(System Call)이나 인터럽트(Interrupt)로 커널에 들어올 수 있습니다. 그 순간 커널이 같은 레지스터를 곧바로 덮어쓰면, 유저 프로세스는 자신이 계산하던 중간값을 잃어버린 채 다시 실행됩니다.
이 문제를 더 정확히 표현하면, 커널은 FP/SIMD 레지스터를 일반 커널 문맥의 일부로 항상 저장·복원하도록 설계되지 않았습니다는 뜻입니다. 유저 태스크의 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 레지스터에 색 변환 계수와 누산값을 담아 둔 상태에서 파일 입출력 시스템 콜을 호출했다고 가정하겠습니다. 이때 커널이 체크섬 계산이나 암호화 가속을 위해 AVX 명령을 직접 실행하면서 상태를 보존하지 않으면, 시스템 콜이 끝난 뒤 유저 프로그램은 손상된 레지스터 값으로 계산을 이어 가게 됩니다. 소스 코드 관점에서는 서로 다른 함수처럼 보여도, 실제 하드웨어 레지스터는 같은 CPU 위에서 번갈아 사용되므로 보호 절차가 필요합니다.
보호 구간이 필요한 직접적인 이유 중 하나는 선점과 스케줄링(Scheduling)입니다. 커널이 SIMD 레지스터를 잡은 채 다른 태스크로 전환되면 어떤 상태가 누구 것인지 관리가 어려워지고, 복원 시점도 복잡해집니다. 그래서 kernel_fpu_begin()은 보통 선점을 막고, 환경에 따라서는 softirq 개입도 제한하며, 개발자에게 "이 구간에서는 자지 말고 아주 짧게 끝내라"는 제약을 함께 부여합니다. 따라서 이 API는 단순 저장 함수가 아니라, 짧고 엄격한 임계 구간을 여는 장치로 이해하는 편이 정확합니다.
/* ⚠ 커널 코드에서 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();
- kernel_fpu_begin/end 필수 — 이 쌍 없이 SIMD 레지스터를 사용하면 유저 프로세스(Process)의 FPU 상태가 손상됨. 결과: 프로세스의 부동소수점 연산이 오염되어 무작위 연산 오류
- 컨텍스트 제한 —
kernel_fpu_begin()은preempt_disable()을 포함. sleep 불가. IRQ 핸들러(Handler), softirq에서도 사용 가능하지만 중첩 비용 큼 - 최소 구간 사용 — FPU begin/end 구간이 길면 preempt 지연(Latency)이 증가하여 시스템 응답성 저하. 가능한 짧게 유지
- 대안: SIMD 전용 함수 — crypto API는 별도 SIMD 최적화 함수를
*-aesni,*-neon등으로 등록하여 자동 관리 - CONFIG_CRYPTO_SIMD — 비동기 crypto에서 SIMD를 안전하게 사용하기 위한 래퍼. softirq에서 자동으로 generic fallback
XSAVE/XRSTOR 메커니즘 상세
XSAVE는 FPU/SIMD 상태를 확장 가능한 방식으로 저장/복원하는 x86 메커니즘입니다. kernel_fpu_begin()의 핵심 구현이며, 새로운 SIMD 확장이 추가될 때마다 자동으로 대응합니다.
/* 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 BNDCFG 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 플래그가 이 최적화의 핵심입니다.
/* arch/x86/kernel/fpu/core.c — switch_fpu_finish() */
static inline void switch_fpu_finish(void)
{
/* 컨텍스트 스위치 직후 (새 태스크로 전환) */
if (!test_thread_flag(TIF_NEED_FPU_LOAD))
return; /* FPU가 이미 올바른 상태 — 복원 불필요 */
/*
* FPU 상태가 레지스터에 없음 → 유저 복귀 전에 복원 필요
* 이 시점에서는 아직 복원하지 않음!
* 유저 복귀 경로(exit_to_user_mode)에서 실제 XRSTOR 수행
*
* 최적화 이유:
* 커널 코드 실행 중에는 FPU 레지스터 불필요
* → 유저 복귀 직전까지 복원을 지연하면
* → 같은 태스크가 연속 컨텍스트 스위치되는 경우 복원 생략 가능
*/
}
/* arch/x86/kernel/fpu/core.c — fpregs_restore_userregs() */
void fpregs_restore_userregs(void)
{
struct fpu *fpu = ¤t->thread.fpu;
if (!test_thread_flag(TIF_NEED_FPU_LOAD))
return;
/* 실제 XRSTOR 수행 — 유저 복귀 직전에만 호출됨 */
restore_fpregs_from_fpstate(fpu->fpstate, XFEATURE_MASK_FPSTATE);
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 모델, 캐시(Cache) 상태, 메모리 대역폭(Bandwidth)에 따라 달라집니다. 핵심 판단 기준: kernel_fpu_begin/end의 오버헤드가 SIMD 가속으로 얻는 이득보다 작아야 합니다. 일반적으로 1KB 이상의 데이터를 처리할 때만 SIMD가 유리합니다. crypto API의 simd_skcipher 래퍼는 이 판단을 자동화합니다.
데이터 정렬 — #GP 예외와 진단
SIMD 정렬 명령어(movaps, vmovaps 등)는 데이터가 정해진 바이트 경계에 맞지 않으면 #GP(General Protection Fault)를 발생시킵니다. 커널에서 이를 잘못 처리하면 oops로 이어집니다.
정렬 요구사항 상세 테이블
| ISA | 정렬 명령어 | 비정렬 명령어 | 정렬 바이트 | #GP 조건 |
|---|---|---|---|---|
| SSE | movaps, movdqa | movups, movdqu | 16B | addr % 16 ≠ 0 |
| AVX / AVX2 | vmovaps (256-bit) | vmovups, vmovdqu | 32B | addr % 32 ≠ 0 |
| AVX-512 | vmovaps (512-bit) | vmovdqu32/64 | 64B | addr % 64 ≠ 0 |
| Non-Temporal | movntps, 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 |
| 체크섬(Checksum) (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 루프가 아닌, CPU 마이크로아키텍처와 크기에 따른 계층적 결정 트리를 사용합니다.
arch/x86/lib/memcpy_64.S 구조
/* arch/x86/lib/memcpy_64.S — 단순화된 구조 */
/* 실제 구현은 CPU 특성 플래그를 런타임에 패치(alternatives)함 */
/* 소형 복사 (len < 64바이트): 레지스터 직접 이동 */
/* 1~7B: rep movsb (또는 개별 mov 명령어) */
/* 8~15B: mov + 겹침 허용 (end 기준 마지막 8B) */
/* 16~31B: movdqu × 2 (겹침 허용) */
/* 32~63B: vmovdqu × 2 (겹침 허용) */
/* 중형 복사 (64B ≤ len < 2KB): AVX2 루프 */
.memcpy_avx2_loop:
vmovdqu (%rsi), %ymm0
vmovdqu 32(%rsi), %ymm1
vmovdqu %ymm0, (%rdi)
vmovdqu %ymm1, 32(%rdi)
add $64, %rsi
add $64, %rdi
sub $64, %rcx
jg .memcpy_avx2_loop
vzeroupper /* AVX→SSE 전환 페널티 방지 */
/* 대형 복사 (len ≥ 2KB): Non-Temporal 스토어 */
.memcpy_nt_loop:
vmovdqu (%rsi), %ymm0 /* 소스: 일반 로드 (캐시 경유) */
vmovdqu 32(%rsi), %ymm1
vmovntdq %ymm0, (%rdi) /* 목적지: NT 스토어 (캐시 우회) */
vmovntdq %ymm1, 32(%rdi) /* Write-Combining Buffer 사용 */
add $64, %rsi
add $64, %rdi
sub $64, %rcx
jg .memcpy_nt_loop
sfence /* NT 스토어 가시성 보장 (필수!) */
vzeroupper
ERMS / FSRM vs AVX 수동 루프 선택 로직
| 기능 | 도입 CPU | 특징 | 커널 사용 조건 |
|---|---|---|---|
| ERMS (Enhanced REP MOVSB) | IvyBridge (2012) | rep movsb가 대용량에서도 SIMD 수준 성능 | X86_FEATURE_ERMS 플래그 |
| FSRM (Fast Short REP MOVSB) | IceLake (2019) | 소형(<128B)에서도 rep movsb가 최속 | X86_FEATURE_FSRM 플래그 |
| AVX2 루프 | Haswell (2013) | 중형 범위에서 최적, FSRM 없는 시스템 기본 | X86_FEATURE_AVX2 플래그 |
| NT 스토어 | Pentium III (SSE) | 대형 복사에서 캐시 오염 방지, 대역폭 향상 | len ≥ 2KB 임계값 |
/* 커널이 런타임에 memcpy 전략 선택 — alternatives 메커니즘 */
/* arch/x86/lib/memcpy_64.S에서 alternatives 패치로 결정 */
/* FSRM 검사 예시 (arch/x86/include/asm/cpufeatures.h) */
#define X86_FEATURE_ERMS (0*32+9) /* Enhanced REP MOVSB/STOSB */
#define X86_FEATURE_FSRM (3*32+4) /* Fast Short REP MOVSB */
/* 런타임 기능 검사 */
if (static_cpu_has(X86_FEATURE_FSRM)) {
/* rep movsb → 하드웨어가 최적화 수행 */
asm volatile("rep movsb"
: "+D"(dst), "+S"(src), "+c"(len) :: "memory");
} else if (static_cpu_has(X86_FEATURE_AVX2)) {
__memcpy_avx2(dst, src, len); /* AVX2 수동 루프 */
} else {
__memcpy_sse2(dst, src, len); /* SSE2 fallback */
}
Non-Temporal 스토어와 sfence
/* Non-Temporal 스토어 사용 패턴 — 커널 대형 복사 핵심 */
/*
* 일반 스토어: 데이터를 캐시에 올리고 → 메모리에 쓰기
* → 대형 복사 시 캐시를 오염시켜 다른 데이터 축출
*
* NT 스토어: Write-Combining Buffer(WCB)에 누적 → 메모리 직접 기록
* → 캐시 오염 없음, 메모리 대역폭 집중 활용
* → 단, sfence로 WCB 플러시 보장 필수
*/
void large_memcpy_nt(void *dst, const void *src, size_t len)
{
kernel_fpu_begin();
asm volatile(
".Lnt_loop:\\n"
"vmovdqu (%[s]), %%ymm0\\n" /* 소스: 일반 로드 */
"vmovdqu 32(%[s]), %%ymm1\\n"
"vmovntdq %%ymm0, (%[d])\\n" /* 목적지: NT 스토어 */
"vmovntdq %%ymm1, 32(%[d])\\n"
"add $64, %[s]\\n"
"add $64, %[d]\\n"
"sub $64, %[n]\\n"
"jg .Lnt_loop\\n"
"sfence\\n" /* NT 스토어 완료 보장 */
"vzeroupper\\n" /* AVX→SSE 전환 페널티 방지 */
: [d] "+r"(dst), [s] "+r"(src), [n] "+r"(len)
:: "ymm0", "ymm1", "memory"
);
kernel_fpu_end();
}
/* 경고: sfence 없이 NT 스토어를 사용하면 다른 CPU 코어에서
* 데이터가 보이지 않을 수 있음 (Write-Combining Buffer에 잔류).
* 특히 DMA, 크로스-CPU 공유 메모리 시나리오에서 치명적. */
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-v2 | SSE4.2, POPCNT, SSSE3, CMPXCHG16B | Nehalem / K10 이상 |
| x86-64-v3 | AVX2, FMA, BMI1/BMI2, MOVBE | Haswell / Excavator 이상 |
| x86-64-v4 | AVX-512F/BW/CD/DQ/VL | Skylake-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 코드 디버깅(Debugging)과 성능 분석
# 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 ymm | 1 | 0.33 | 정수 덧셈, 3개 포트 분산 |
VMULPS ymm | 4 | 0.5 | float 곱셈, 2 포트 |
VDIVPS ymm | 11~14 | 5~14 | 나눗셈, 매우 느림 |
VGATHERDPS ymm | ~24 | ~24 | Gather, 캐시 미스 시 더 느림 |
VAESENC ymm | 4 | 1 | AES 라운드, 높은 처리량 |
VPSHUFB ymm | 1 | 0.5 | 바이트 셔플, SSSE3 |
SIMD 명령어는 objdump으로 확인할 수 있습니다:
objdump -d vmlinux | grep -E 'vmov|vpxor|vaes|vpshufb|vpadd'
커널 SIMD 코드의 일반적인 실수와 디버깅:
kernel_fpu_begin/end누락- 증상: 유저 프로세스의 부동소수점 결과가 무작위로 틀립니다.
- 진단: KASAN/UBSAN으로는 감지할 수 없습니다.
CONFIG_X86_DEBUG_FPU를 활성화해야 합니다.
- clobber 리스트 누락
- 증상: 최적화 수준에 따라 간헐적 오류가 발생합니다.
- 진단:
-O0에서 정상이고-O2에서 비정상이면 clobber를 확인합니다.
- 정렬 문제
- 증상:
movaps에서 #GP (General Protection) 예외가 발생합니다. - 해결:
movdqu/vmovdqu(비정렬 변종) 사용 또는__attribute__((aligned(32)))으로 정렬을 보장합니다.
- 증상:
- 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-512 | E-core AVX-512 |
|---|---|---|---|---|
| Alder Lake (2021) | Golden Cove | Gracemont | 하드웨어 지원 | 미지원 |
| Raptor Lake (2022) | Raptor Cove | Gracemont | 하드웨어 지원 | 미지원 |
| Meteor Lake (2023) | Redwood Cove | Crestmont | 미지원 | 미지원 |
| Arrow Lake (2024) | Lion Cove | Skymont | 미지원 | 미지원 |
| Lunar Lake (2024) | Lion Cove | Skymont | 미지원 | 미지원 |
/* 커널의 하이브리드 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 → 항목 없으면 비활성화됨
*/
- 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 효과 |
|---|---|---|---|
| memcpy | 0 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 ~ 2KB | AVX2 vmovdqu 루프 | L1/L2 캐시 히트, SIMD 유리 |
| 2KB ~ 256KB | REP MOVSB (FSRM) | L2/L3 최적화된 마이크로코드 |
| > 256KB | NT 스토어 (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 비활성화)을 따릅니다.
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.7+에서 arch/riscv/crypto/에 SIMD 가속 알고리즘이 등록됩니다.
AMX — 행렬 연산 가속
Intel AMX(Advanced Matrix Extensions)는 Sapphire Rapids(2023)에서 도입된 타일 기반 행렬 곱셈 가속기입니다. 8개의 타일 레지스터(TMM0-TMM7, 각 최대 1KB)를 사용하여 INT8/BF16 행렬 외적(Outer Product)을 하드웨어에서 수행합니다. 커널에서는 직접 사용하지 않지만, 유저 공간 상태 관리가 커널의 핵심 역할입니다.
/* 커널의 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 = ¤t->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)
*/
/* XSAVE 크기 비교 (태스크당 FPU 상태 메모리) */
/*
* SSE만 사용: ~576 바이트
* + AVX: ~832 바이트 (+256)
* + AVX-512: ~2,688 바이트 (+1,856)
* + AMX: ~10,880 바이트 (+8,192)
*
* → AMX 활성 프로세스는 컨텍스트 스위치 시 ~10KB 저장/복원
* → XFD Lazy Allocation으로 실제 사용 프로세스만 부담
*/
- 컨텍스트 스위치 비용 — AMX 활성 프로세스의 XSAVE/XRSTOR는 ~10KB 메모리 접근. 빈번한 스위칭 시 성능 영향 큼
- 커널에서 미사용 — 커널 자체는 AMX를 사용하지 않음. XFD/#NM 핸들러와 XSAVE 상태 관리만 담당
- prctl(ARCH_REQ_XCOMP_PERM) — 유저 공간에서 AMX 사용 전에 명시적 권한 요청 필요 (Linux 5.16+)
- KVM 가상화(Virtualization) — 게스트 VM의 AMX 상태도 커널이 관리.
IA32_XFD_ERRMSR VMEXIT 처리 필요
아키텍처별 SIMD 비교
x86, ARM, RISC-V 세 아키텍처의 SIMD 확장을 직접 비교합니다. 커널 개발자가 크로스 플랫폼 SIMD 코드를 작성할 때 참고할 수 있습니다.
| 특성 | x86 SSE/AVX/AVX-512 | ARM NEON/SVE/SVE2 | RISC-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 가속의 혜택을 받습니다.
dm-crypt SIMD 가속 경로
dm-crypt 디스크 암호화의 SIMD 가속 데이터 흐름:
- 유저
write()호출 - VFS → Block Layer → dm-crypt
- dm-crypt가 crypto API를 호출합니다:
skcipher_request_set_crypt(req, src_sg, dst_sg, nbytes, iv); crypto_skcipher_encrypt(req);
- 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-genericfallback을 사용합니다.
- process context (kcryptd 워크큐):
- 암호화된 데이터 → Block Layer → 디스크 기록
dm-crypt 성능 최적화 포인트:
kernel_fpu_begin/end호출 빈도 최소화: dm-crypt는 여러 섹터를 한 번의 FPU 구간에서 처리합니다.crypto_skcipher_encrypt()가 scatterlist 전체를 처리하며, FPU begin/end는 알고리즘 내부에서 한 번만 호출됩니다.- 워커 스레드 수 조정:
kcryptd_crypt워크큐는 unbound workqueue로 CPU별로 분산됩니다. - AES-XTS 모드 선택 근거: XTS는 블록 단위 독립적이므로 병렬화에 유리합니다. VAES + AVX-512로 4블록(64B) 동시 암호화가 가능하며, 이전 블록 스토어와 현재 블록 연산을 동시에 수행하는 파이프라이닝을 활용합니다.
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을 사용 중입니다.
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]"으로 높은 우선순위(300+) 알고리즘을 찾으면 SIMD 가속 구현입니다. cryptsetup benchmark 명령으로 현재 시스템의 실제 crypto 처리량을 측정할 수 있습니다. dm-crypt가 어떤 알고리즘을 사용하는지는 dmsetup table --showkeys로 확인합니다.
x86 데이터 이동 명령어 시각화
SIMD 연산의 첫 단계는 데이터를 메모리에서 레지스터로, 또는 레지스터 간에 이동하는 것입니다. 정렬 여부, 논템포럴 힌트, 브로드캐스트 패턴에 따라 명령어 선택이 성능에 큰 영향을 미칩니다.
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으로 지워질 수 있습니다.
| 명령어 | 형식 | low lane | 나머지 low 128비트 | 128비트 초과 상위 |
|---|---|---|---|---|
| MOVSS xmm, xmm | legacy reg→reg | src의 32비트 복사 | dst의 상위 96비트 유지 | YMM/ZMM 상위는 legacy 상태 유지 |
| MOVSS xmm, m32 | legacy mem→reg | 메모리 32비트 로드 | xmm[127:32] = 0 | YMM/ZMM 상위는 legacy 상태 유지 |
| VMOVSS xmm, xmm, xmm | VEX reg,reg,reg | 세 번째 오퍼랜드의 32비트 | 두 번째 오퍼랜드의 상위 96비트 복사 | 0으로 클리어 |
| VMOVSS xmm, m32 | VEX mem→reg | 메모리 32비트 로드 | xmm[127:32] = 0 | 0으로 클리어 |
| MOVSD / VMOVSD | 64-bit scalar double | 64비트 low lane 이동 | legacy는 upper 64 유지, VEX reg-form은 merge, mem-form은 0 | VEX는 0, legacy는 기존 상태 유지 |
| 명령어 | 오퍼랜드 | 동작 | 지연(cycle) | 처리량(CPI) |
|---|---|---|---|---|
| MOVAPS | xmm, m128 | 정렬 128b 로드 | 4–5 | 0.25–0.5 |
| VMOVUPS | ymm, m256 | 비정렬 256b 로드 | 4–5 | 0.25–0.5 |
| VMOVDQU32 | zmm{k}, m512 | 마스크 512b 로드 | 5–7 | 0.5 |
| MOVSS | xmm, m32 | 스칼라 float 로드, xmm[127:32]=0 | 4–5 | 0.5 |
| VMOVSS | xmm,xmm,xmm / xmm,m32 | low32 merge 또는 load, VEX 상위 비트 정리 | 1–5 | 0.5 |
| MOVSD | xmm, m64 | 스칼라 double 로드, xmm[127:64]=0 | 4–5 | 0.5 |
| VMOVSD | xmm,xmm,xmm / xmm,m64 | low64 merge 또는 load, VEX 상위 비트 정리 | 1–5 | 0.5 |
| VBROADCASTSS | ymm, m32 | 스칼라→8레인 복제 | 5–7 | 0.5 |
| VBROADCASTSD | ymm, m64 | 스칼라→4레인 복제 | 5–7 | 0.5 |
| VINSERTF128 | ymm,ymm,xmm,i | 128b 레인 삽입 | 3 | 1 |
| VEXTRACTF128 | xmm,ymm,i | 128b 레인 추출 | 3 | 1 |
| VMOVNTPS | m256, ymm | NT 스토어 (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 추론 가속의 핵심입니다.
| 명령어 | 동작 | 데이터 폭 | 용도 |
|---|---|---|---|
| PADDB/W/D/Q | 레인별 덧셈 (랩어라운드) | i8/i16/i32/i64 | 일반 정수 연산 |
| PADDSB/SW | 부호있는 포화 덧셈 | i8/i16 | 오디오, 이미지 클램핑 |
| PADDUSB/USW | 부호없는 포화 덧셈 | u8/u16 | 픽셀 합산 |
| PMULLW | i16×i16→i16 (하위 16비트) | i16 | 소규모 곱셈 |
| PMULHW | i16×i16→i16 (상위 16비트) | i16 | 고정소수점 곱셈 |
| PMULLD | i32×i32→i32 (하위 32비트) | i32 | 일반 정수 곱셈 |
| PMADDWD | i16×i16→i32 쌍별 합 | i16→i32 | FIR, 컨볼루션, 닷프로덕트 |
| PMADDUBSW | u8×i8→i16 쌍별 합 | u8×i8→i16 | INT8 양자화 곱셈 |
| PSADBW | |a-b| 절대차 합 (8바이트) | u8→u64 | 모션 추정 SAD |
| PAVGB/W | (a+b+1)>>1 평균 | u8/u16 | 이미지 보간 |
| VPDPBUSD | u8×i8→i32 4쌍 누적 | u8×i8→i32 | VNNI 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 명령어 상세
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 / VADDSS | ADDPS / VADDPS | float 1개 덧셈 | legacy는 dst 유지, VEX는 merge source 복사 | 잔여 원소 누적, 스칼라 보정 |
| MULSS / VMULSS | MULPS / VMULPS | float 1개 곱셈 | legacy는 dst 유지, VEX는 merge source 복사 | gain 적용, 단일 샘플 처리 |
| DIVSS / VDIVSS | DIVPS / VDIVPS | float 1개 나눗셈 | 상위 lane 유지 또는 merge | 정확도 우선 scalar 경로 |
| SQRTSS / VSQRTSS | SQRTPS / VSQRTPS | float 1개 제곱근 | 상위 lane 유지 또는 merge | 길이 계산, 정규화 tail |
| VFMADDSS / VFMADDSD | VFMADDPS / VFMADDPD | low lane FMA | merge source의 상위 lane 복사 | 닷프로덕트 tail, Horner 다항식 |
| MAXSS/MINSS | MAXPS/MINPS | float 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 제약을 그대로 적용해야 합니다.
| 명령어 | 동작 | 지연(c) | 처리량(CPI) | 비고 |
|---|---|---|---|---|
| VADDPS/VSUBPS | 레인별 덧셈/뺄셈 | 4 | 0.5 | 2포트 동시 발행 |
| VMULPS | 레인별 곱셈 | 4 | 0.5 | FMA 포트 공유 |
| VFMADD231PS | a×b+c (단일 반올림) | 4 | 0.5 | FMA3, Haswell+ |
| VDIVPS (256b) | 레인별 나눗셈 | 11 | 5 | divider 독점 |
| VSQRTPS (256b) | 레인별 제곱근 | 12 | 6 | divider 독점 |
| VRCPPS | 근사 역수 (12-bit) | 4 | 1 | NR 반복 필요 |
| VRSQRTPS | 근사 역제곱근 (12-bit) | 4 | 1 | NR 반복 필요 |
| VMAXPS/VMINPS | 레인별 최대/최소 | 4 | 0.5 | NaN 전파 주의 |
| VROUNDPS | 반올림 모드 선택 | 8 | 2 | floor/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 변환, 전치, 엔디안(Endianness) 변환의 핵심이며, 명령어마다 고유한 선택 패턴을 가집니다.
| 명령어 | 단위 | 크로스레인 | 제어 | 용도 |
|---|---|---|---|---|
| PSHUFB | byte | ✗ (레인 내) | 레지스터(16인덱스) | 엔디안 변환, LUT |
| PSHUFD | dword | ✗ | imm8 (2bit×4) | broadcast, 역순 |
| PSHUFHW/LW | word | ✗ (상위/하위 half) | imm8 | 16비트 셔플 |
| PALIGNR | byte | ✗ | imm8 (shift량) | 바이트 시프트 결합 |
| VPERMILPS | float | ✗ (레인 내) | imm8/reg | 레인 내 float 셔플 |
| VPERMD/Q | dword/qword | ✓ | 레지스터(8인덱스) | 크로스레인 재배치 |
| VPERM2I128 | 128b 레인 | ✓ | imm8 | 레인 교환/복제 |
| VPERMB | byte | ✓ | 레지스터 | 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로 전체 바이트 퍼뮤트. 크로스레인은 1사이클 추가 지연이 있으므로 가능하면 레인 내 셔플로 해결합니다.
x86 패킹/언패킹/확장 명령어
데이터 폭 변환은 이미지/비디오 파이프라인(Pipeline)의 핵심입니다. uint8 픽셀을 float로 올려 연산하고, 결과를 다시 uint8로 내려야 합니다. 인터리브(PUNPCK)와 패킹(PACK) 명령어가 이 "깔때기"를 형성합니다.
| 명령어 | 동작 | 입력→출력 | 포화 |
|---|---|---|---|
| PUNPCKLBW | 하위 바이트 인터리브 | 2×16B→16B | — |
| PUNPCKHBW | 상위 바이트 인터리브 | 2×16B→16B | — |
| PUNPCKLWD | 하위 워드 인터리브 | 2×8W→8W | — |
| PACKSSWB | i16→i8 축소 | 2×8W→16B | 부호있는 |
| PACKUSWB | i16→u8 축소 | 2×8W→16B | 부호없는 |
| PACKSSDW | i32→i16 축소 | 2×4D→8W | 부호있는 |
| PACKUSDW | i32→u16 축소 | 2×4D→8W | 부호없는 (SSE4.1) |
| VPMOVZXBW | u8→u16 제로확장 | 8B→8W | — |
| VPMOVZXBD | u8→u32 제로확장 | 4B→4D | — |
| VPMOVSXBW | i8→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 명령에 프레디케이트를 부여해, 분기 없는 조건부 처리와 루프 테일 마스킹을 가능하게 합니다.
| 명령어 | 동작 | 용도 |
|---|---|---|
| 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)하거나 인접 쌍을 합산하는 "수평" 연산이 필요한 경우가 있습니다. 수평 명령은 셔플+수직 연산의 조합이므로, 처리량이 낮고 주의가 필요합니다.
| 명령어 | 동작 | µops | 지연(c) | 권장 여부 |
|---|---|---|---|---|
| HADDPS | 인접 쌍 합 (2소스) | 3 | 6 | △ 느림, 셔플+add 조합 권장 |
| HSUBPS | 인접 쌍 차 | 3 | 6 | △ |
| DPPS | 닷프로덕트 (마스크 선택) | 4 | 9 | △ SSE4.1, FMA가 빠름 |
| DPPD | 2×double 닷프로덕트 | 3 | 9 | △ |
| MPSADBW | 다중 위치 SAD (8위치) | 2 | 5 | ○ 모션 추정 특화 |
| 셔플+수직 리덕션 | 캐스케이드 add | 5–7 | 12–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에 내장되어 있어, 데이터 재배치에서 독보적 효율을 보입니다.
| 명령어 | 동작 | x86 대응 | 용도 |
|---|---|---|---|
| TBL/TBX | 바이트 테이블 룩업 (1–4 reg) | PSHUFB (16B 제한) | LUT, 변환 테이블 |
| TRN1/TRN2 | 짝/홀수 쌍 전치 | — | 2×2 행렬 전치 |
| ZIP1/ZIP2 | 하위/상위 인터리브 | PUNPCKLXX/PUNPCKHXX | SoA→AoS |
| UZP1/UZP2 | 짝수/홀수 디인터리브 | — | AoS→SoA |
| EXT | 두 벡터 연결 후 바이트 추출 | PALIGNR | 슬라이딩 윈도우 |
| REV16/32/64 | 16/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 축소 */
}
ARM SVE 프레디케이트 연산 시각화
SVE(Scalable Vector Extension)는 하드웨어 벡터 길이에 무관한 코드를 작성할 수 있게 합니다. 핵심은 프레디케이트 레지스터(p0–p15)로, WHILELT로 루프 테일을 자동 처리하고, BRKA/COMPACT로 조건부 데이터 조작을 수행합니다.
/* 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;
}
RISC-V RVV 명령어 시각화
RVV(RISC-V Vector Extension)는 SVE처럼 구현 정의 벡터 길이(VLEN)를 지원하되, vsetvli로 동적으로 SEW(요소 폭)와 LMUL(레지스터 그룹 배수)을 설정하는 독특한 모델입니다. 이 명령어가 VL(실제 처리 요소 수)을 반환하므로, 루프 제어가 자동화됩니다.
| 명령어 | 동작 | x86 대응 | SVE 대응 |
|---|---|---|---|
| VSETVLI/VSETIVLI | VL/SEW/LMUL 설정 | — | —(VL 고정) |
| VLE/VSE | 단위 스트라이드 로드/스토어 | VMOVDQU | LD1/ST1 |
| VLSE/VSSE | 스트라이드 로드/스토어 | VPGATHERDD(유사) | LD1(stride) |
| VLUXEI/VSUXEI | 인덱스(gather/scatter) | VPGATHERD/VPSCATTERD | LD1(gather) |
| VRGATHER | 인덱스 기반 재배치 | VPERMD | TBL |
| VSLIDEDOWN/UP | 요소 시프트 | VPALIGNR(유사) | EXT |
| VCOMPRESS | 마스크 압축 | VCOMPRESSPS | COMPACT |
| VREDSUM/MAX/MIN | 벡터→스칼라 리덕션 | 셔플 캐스케이드 | FADDV/SMAXV |
| VWMUL/VWMACC | 확장 곱/곱-누적 | PMADDWD(유사) | SMULL/SMLAL |
| VFMACC | FP 곱-누적 | VFMADD | FMLA |
/* 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를 동일한 관점으로 정리한 실전 지도입니다.
| 패밀리 | x86 | ARM NEON / SVE | RISC-V RVV | 커널 활용 |
|---|---|---|---|---|
| 연속 로드/스토어 | VMOVDQU8/16/32/64, VMOVAPS, VMOVUPS | LD1/ST1, LD1B/ST1B, LD1W/ST1W | VLE/VSE | memcpy, XOR, checksum |
| 구조화/분리 로드 | UNPCK + SHUF 조합 | LD2/LD3/LD4, ST2/ST4 | VLSEG/VSSEG | 인터리브 패킷, RGB, SoA 변환 |
| 비교 → 마스크 | VPCMPEQ, VPCMP*, VPMOVMSKB, KORTEST | CMEQ/CMGT, PTEST, CNTP | VMSEQ/VMSLT, VMAND.MM, VCPOP.M | memchr, 필터링, 경계 탐색 |
| 조건부 선택 | VPBLEND*, VPTERNLOG*, VSELECT(mask) | BSL/BIT/BIF, SEL | VMERGE.VM | 마스크 기반 합성, 분기 제거 |
| 셔플/테이블 룩업 | PSHUFB, VPERMB, VPERMD, VPERMT2* | TBL/TBX, ZIP/TRN/UZP, EXT | VRGATHER, VSLIDEUP/DOWN | 바이트 재배열, 해시, 문자열 정규화 |
| widen / narrow | VPMOVZX*, VPMOVSX*, PACKSS*, PACKUS* | USHLL/SSHLL, SQXTN/UQXTN, FCVTL/FCVTN | VWADD/VWMUL, VNCLIP/VNCLIPU | 양자화, 픽셀 변환, 샘플 확장 |
| 포화 산술 | PADDS*, PADDUS*, PSUBS* | SQADD/UQADD, SQSUB/UQSUB | VSADD/VSADDU, VSSUB/VSSUBU | 오디오, 이미지, clamp |
| 곱-누적 / 점곱 | PMADDWD, PMADDUBSW, VPDPBUSD, VDPBF16PS | SMLAL/UMLAL, SDOT/UDOT, BFDOT | VWMACC/VWMACCSU, VFMACC | 암호, FIR, INT8 추론, 행렬 곱 |
| 리덕션 | HADD*, DPPS, 셔플 캐스케이드 | ADDV, SMAXV, FADDV | VREDSUM, VWREDSUM, VFREDUSUM | 합계, 최대/최소, 통계 |
| Gather / Scatter | VGATHER*, VSCATTER*, VPSCATTER* | NEON은 일반 메모리 gather 부재, SVE는 LD1/ST1 gather 지원 | VLUXEI/VSUXEI, VLOXEI/VSOXEI | 희소 인덱스, 룩업 테이블 |
| 압축 / 컴팩트 | VCOMPRESS*, VEXPAND* | COMPACT, SPLICE | VCOMPRESS.VM | 필터링, pack-active, sparse write |
| Fault-first / 탐색 | 문자열 비교 + MOVEMASK 조합 | LDFF1, BRKA, MATCH, WHILELT | VLEFF, VFIRST.M, VIOTA.M | strlen, copy_to_user 전 검사 |
| 논템포럴 / 스트리밍 | MOVNT*, PREFETCH*, MOVNTDQA | PRFM, STNP 계열 보조 사용 | 일반 prefetch 힌트 + 스트라이드 루프 | 대용량 복사, 버퍼 초기화 |
| 암호 전용 | AESENC, PCLMULQDQ, SHA*, GFNI | AESE, PMULL, SHA256H, SM4E | Zvkned, Zvkg, Zvkb, Zvknh* | IPsec, dm-crypt, WireGuard |
실전 분류 기준: 커널 코드는 보통 load → compare/select → shuffle → accumulate → reduce/store 흐름으로 읽으면 구조가 빠르게 보입니다. 명령어 이름이 달라도 이 다섯 단계 중 어디에 속하는지만 먼저 파악하면 x86용 알고리즘을 NEON, SVE, RVV로 옮길 때 훨씬 수월합니다. 특히 우선순위(Priority)가 높은 핫패스가 해시(Hash), 복호화(Decryption), 패킷(Packet) 처리, 대형 버퍼(Buffer) 이동 중 어디에 있는지 먼저 판단해야 SIMD 투자 대비 효과를 정확히 계산할 수 있습니다.
x86 논리·비교·선택 명령어 상세
문자열 검색, 패킷 분류, 비트마스크 생성, 조건부 merge는 커널 SIMD 코드에서 매우 자주 등장합니다. 핵심 패턴은 비교(compare) → 마스크(mask) 추출 → 첫 hit 계산 또는 마스크로 선택(blend/select)입니다. AVX2까지는 movemask로 벡터 상태를 스칼라 비트마스크로 내리고, AVX-512에서는 k-mask 레지스터를 유지한 채 kortest, compress, 마스크 스토어로 이어갑니다.
| 패밀리 | 대표 명령어 | 핵심 의미 | 커널 활용 포인트 |
|---|---|---|---|
| 동등/대소 비교 | PCMPEQB/W/D, PCMPGTB/W/D, VPCMPB/D/UQ | 레인별 비교 결과를 all-ones 또는 mask 비트로 생성 | 문자열 검색, 패킷 필터, 경계 조건 검사 |
| 문자열 비교 | PCMPESTRI/M, PCMPISTRI/M | SSE4.2 문자열 검색 전용 비교 + 인덱스 반환 | memchr, strcmp, delimiter 탐색 |
| 논리 연산 | PAND, POR, PXOR, PANDN, VPANDD/Q | 타입과 무관한 비트 연산 | 암호, RAID XOR, 비트마스크 조합 |
| 테스트 | PTEST, VTESTPS/PD, VPTESTMB/MD | AND 결과의 zero/all-one 여부를 플래그 또는 mask로 전달 | 빠른 early-exit, zero-block 탐지 |
| 마스크 추출 | PMOVMSKB, VPMOVMSKB, KMOVW/Q | 벡터 비교 결과를 스칼라 비트필드로 변환 | first-hit 탐색, scalar branch 연결 |
| 블렌드 | BLENDVPS, VPBLENDVB, VPBLENDMD | 조건에 따라 두 소스 중 한쪽을 선택 | 분기 제거, scatter 전 필터링 |
| 삼항 논리 | VPTERNLOGD/Q | 3입력 Boolean 식을 imm8 truth table로 한 번에 계산 | AND/OR/XOR/NOT 조합 축약, 암호 비트슬라이스 |
| k-mask 조작 | KAND, KOR, KXOR, KNOT, KSHIFTL/R, KORTEST | AVX-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로 끝까지 유지하는 편이 더 낫습니다.
x86 Gather·Scatter·Prefetch·Streaming 상세
연속 메모리라면 일반 로드/스토어가 가장 빠릅니다. 하지만 라우팅(Routing) 테이블(Routing Table), 희소 인덱스, 압축 블록 메타데이터처럼 주소가 흩어진 자료구조에서는 gather/scatter가 코드 복잡도를 크게 줄여줍니다. 다만 성능은 주소 분산도(index entropy)와 캐시 히트율에 극단적으로 민감합니다. 연속 접근으로 재구성할 수 있다면 대부분 gather보다 낫습니다.
| 패밀리 | 대표 명령어 | 설명 | 주의점 |
|---|---|---|---|
| Gather FP | VGATHERDPS/DPD/QPS/QPD | 인덱스 벡터로 불연속 주소를 벡터로 모아 읽기 | 캐시라인이 분산되면 거의 스칼라 다중 로드 수준 |
| Gather INT | VPGATHERDD/DQ/QD/QQ | 정수형 불연속 로드 | 주소 계산 µop와 TLB miss 비용 주의 |
| Scatter | VSCATTERDPS/DPD, VPSCATTERDD/DQ | 벡터 값을 불연속 주소로 저장 | read-for-ownership와 write combining 불리 |
| Mask gather/scatter | VGATHER*{%k}, VSCATTER*{%k} | 활성 레인만 접근 | fault가 레인 단위로 발생 가능 |
| Prefetch | PREFETCHT0/T1/T2, PREFETCHNTA | 예상 접근 데이터를 미리 cache 계층으로 당기기 | 과도하면 오히려 cache pollution |
| PF gather hint | VGATHERPF0DPS, VSCATTERPF1DPD | Xeon Phi 계열의 gather/scatter prefetch | 일반 서버 CPU 지원 범위가 좁음 |
| Streaming load | MOVNTDQA | WC 메모리에서 streaming load | 일반 WB 메모리에는 효과 제한 |
| Streaming store | MOVNTPS, MOVNTDQ, VMOVNTDQ | write-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가 보장된 커널 메모리에서만 사용하는 편이 낫습니다.
x86 변환·혼합 정밀도·VNNI·BF16·FP16
현대 커널 SIMD는 단순 산술보다 형 변환과 데이터 재해석에서 더 많은 시간을 씁니다. 압축된 바이트 배열을 넓혀 계산하고, 계산 결과를 다시 좁혀 저장하며, INT8/BF16/FP16 같은 혼합 정밀도 데이터 경로를 연결합니다. 이미지, 오디오, 암호, 네트워크 분류기, AI 추론 오프로드 모두 이 경로 위에 있습니다.
| 패밀리 | 대표 명령어 | 설명 | 대표 용도 |
|---|---|---|---|
| zero/sign extend | VPMOVZXBW/BD/WD/DQ, VPMOVSXBW/BD/WD/DQ | 좁은 정수를 넓은 정수로 확장 | u8 샘플 처리, 양자화 해제 |
| pack / narrow | PACKSSWB, PACKUSWB, VPMOVWB, VPMOVUSDB | 넓은 결과를 포화 또는 truncate 방식으로 좁힘 | 픽셀 저장, clamp |
| 정수↔실수 | VCVTDQ2PS, VCVTPS2DQ, VCVTTPS2DQ | i32와 fp32 사이 변환 | DSP, 통계, 양자화/역양자화 |
| FP16 | VCVTPH2PS, VCVTPS2PH | half precision과 fp32 사이 변환 | 압축 모델, 네트워크 오프로드 |
| BF16 변환 | VCVTNEPS2BF16, VCVTNE2PS2BF16 | fp32를 BF16으로 반올림 압축 | 추론/학습 데이터 전송량 절감 |
| BF16 점곱 | VDPBF16PS | BF16 × BF16 → FP32 누적 | 추론 가속, 행렬 곱 |
| INT8 점곱 | VPDPBUSD, VPDPBUSDS, VPDPWSSD, VPDPWSSDS | u8/i8 또는 i16 쌍을 i32로 누적 | VNNI, 필터, 분류기 |
| 고정소수점 반올림 곱 | PMULHRSW | Q15 고정소수점에 적합한 곱 + 반올림 | 오디오, 영상 DSP |
| 큰 정수 누적 | VPMADD52LUQ/HUQ | 52-bit limb 곱셈 누적 | RSA, ECC, 다정밀도 정수 |
| 클래스 판정 | VFPCLASSPS/PD/PH | NaN, 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 누적 */
ARM NEON·SVE·SVE2 명령어 패밀리 확장
ARM에서는 NEON이 128비트 고정 폭 벡터, SVE/SVE2가 벡터 길이 비종속(VLA) 모델을 담당합니다. 커널 관점에서 중요한 것은 widen/narrow, dot product, predicate 기반 select, fault-first load입니다. NEON은 짧고 예측 가능한 벡터 코드에 강하고, SVE/SVE2는 문자열/메모리 함수처럼 VL에 독립적인 루프에 강합니다.
| 패밀리 | NEON | SVE / SVE2 | 실전 의미 |
|---|---|---|---|
| widen | USHLL, SSHLL, UMLAL, SMLAL | UUNPKLO/HI, SUNPKLO/HI, UMLALB/T | u8/i8 데이터를 넓혀 누적 |
| narrow / saturating narrow | SQXTN, UQXTN, FCVTN | TRN + pack 조합, SQXTNT | 결과를 저장 폭으로 다시 줄임 |
| 포화 산술 | SQADD, UQADD, SQSUB, UQSUB | SQADD, UQADD, SQSUB | 오디오/이미지 clamp |
| 고정소수점 곱 | SQDMULH, SQRDMULH | SQDMULH, SQRDMLAH | Q-format DSP |
| 점곱 | SDOT, UDOT | SDOT, UDOT, BFDOT | INT8/BF16 추론 |
| 쌍별 누적 | SADALP, UADALP, ADDV | SADDLV, UADDV, FADDV | 체크섬, 통계, reduce |
| 테이블 룩업 | TBL, TBX, EXT | TBL, EXT, SPLICE | 바이트 분류, 셔플 |
| 비트 선택 | BSL, BIT, BIF, CMTST, CNT | SEL, EOR, AND, PTEST | mask merge, bit class |
| 구조화 로드/스토어 | LD2/LD3/LD4, ST2/ST4 | LD2B/LD3W/LD4D, ST2B | 인터리브 데이터 분해 |
| gather / scatter | 일반 메모리 gather 부재 | LD1W/LD1D gather, ST1W scatter | 희소 테이블 탐색 |
| fault-first / 탐색 | 별도 전용 명령 부재 | LDFF1, WHILELT, BRKA, CNTP, MATCH | strlen, 안전 탐색 루프 |
| 압축 / 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, VSETIVLI | SVE의 VL 고정과 대비 | 동적 VL 루프 제어 |
| segmented load/store | VLSEG2E*, VLSEG4E*, VSSEG* | LD2/LD4, 구조 분리 로드 | 패킷 헤더, RGB, 복합 구조 |
| strided / indexed | VLSE/VSSE, VLUXEI/VSUXEI, VLOXEI/VSOXEI | Gather / scatter | 희소 행렬, 테이블 참조 |
| fault-first | VLE8FF.V, VLE32FF.V | LDFF1, 문자열 탐색 | page boundary 안전 탐색 |
| 비교 / mask | VMSEQ, VMSLT(U), VMAND.MM, VMXOR.MM | VPCMP, CMEQ | 조건 생성, 필터링 |
| mask 소비 | VCPOP.M, VFIRST.M, VIOTA.M | POPCNT, TZCNT, prefix index | hit 개수, 첫 hit, 압축 인덱스 |
| 조건부 merge | VMERGE.VVM | BLENDVPS, SEL | 분기 제거 |
| 압축 | VCOMPRESS.VM | VCOMPRESSPS, COMPACT | 활성 요소 pack |
| widen / narrow | VWADD, VWMUL, VWMACC, VNCLIP(U) | PMADDWD, SQXTN | 양자화, 누적, clamp |
| slide / gather | VSLIDEUP/DOWN, VSLIDE1UP/DOWN, VRGATHER | VPALIGNR, TBL, VPERMD | 경계 맞춤, 셔플 |
| 리덕션 | VREDSUM, VWREDSUMU, VFREDUSUM | HADD/ADDV | 합계, 체크섬, 통계 |
| 포화 산술 | VSADD(U), VSSUB(U) | PADDS*, SQADD | 이미지, DSP |
| 암호 벡터 확장 | Zvkb, Zvkg, Zvkned, Zvknha | GFNI, PCLMUL, AES, SHA | RISC-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 / PD | x86 | packed single / packed double | ADDPS, MULPD |
| SS / SD | x86 | scalar single / scalar double, low lane만 연산 | VMOVSS, VADDSD |
| B / W / D / Q | x86 | 8/16/32/64비트 정수 요소 폭 | VPADDB, VPCMPEQD |
| U / S | x86, ARM, RVV | 부호 없음 / 부호 있음 또는 zero/sign extension 문맥 | PADDUSB, VMSLTU |
| V / VP 접두사 | x86 | VEX/EVEX 인코딩. 대개 3-operand, 상위 비트 정리, mask 수식 가능 | VADDPS, VPTERNLOGD |
| {k}{z} | x86 AVX-512 | mask 적용과 zeroing merge 정책 | VMOVDQU32 zmm0{k1}{z} |
| .16b / .8h / .4s / .2d | ARM NEON | 레지스터 해석 폭과 레인 수 | add v0.4s, v1.4s, v2.4s |
| p0/z, p0/m | ARM SVE | 프레디케이트 기반 zeroing / merging | ld1w z0.s, p0/z, [x0] |
| .vv / .vx / .vi / .vm | RVV | 벡터-벡터, 벡터-스칼라, 벡터-즉시값, mask 소비 형태 | vadd.vv, vmseq.vi |
| e8 / e16 / e32 / e64 | RVV | SEW(Standard Element Width) 설정 | vsetvli t0, a0, e32, m4 |
| m1 / m2 / m4 / m8 | RVV | LMUL(Register Group Multiplier) | vsetvli t0, a0, e8, m8 |
| ta/tu, ma/mu | RVV | tail/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) | 대표 명령어 패밀리 | 핵심 의미 |
|---|---|---|
| MMX | PADD*, PSUB*, PUNPCK* | 64비트 정수 SIMD의 출발점 |
| SSE | MOVAPS, ADDPS, MULPS, SHUFPS | 128비트 packed float |
| SSE2 | MOVDQA, PADDQ, PMULLW, PSHUFD, MOVSD | 128비트 정수와 double, scalar double |
| SSE3 / SSSE3 | HADDPS, ADDSUBPS, PSHUFB, PALIGNR, PABS* | 수평 연산, 바이트 셔플, 정렬 보조 |
| SSE4.1 / SSE4.2 | PBLEND*, PMULLD, PTEST, PCMPESTR*, PCMPISTR* | blend, test, 문자열 비교 |
| AES-NI / PCLMULQDQ | AESENC, AESDEC, AESKEYGENASSIST, PCLMULQDQ | 대칭키와 GHASH/CRC 가속 |
| AVX | VADDPS, VMULPS, VBROADCAST*, VINSERTF128 | VEX 3-operand, YMM, scalar merge 규칙 |
| F16C / FMA | VCVTPS2PH, VCVTPH2PS, VFMADD* | FP16 변환과 FMA3 |
| AVX2 | VPADDD, VPMASKMOV*, VPGATHER*, VPERMD, PMADDUBSW | 256비트 정수, gather, 정수 셔플 확장 |
| SHA / GFNI / VAES / VPCLMULQDQ | SHA256RNDS2, GF2P8AFFINEQB, VAESENC, VPCLMULQDQ | 현대 암호/비트 행렬 가속 |
| AVX-VNNI / AVX-512 VNNI | VPDPBUSD, VPDPBUSDS, VPDPWSSD, VPDPWSSDS | INT8/INT16 점곱 누적 |
| AVX-512F/BW/DQ/VL | VPCMP*, VCOMPRESS*, VEXPAND*, KAND/KOR/KTEST | 512비트, mask 레지스터, compress/expand |
| AVX-512 CD/VBMI/VBMI2/BITALG/VPOPCNTDQ | VPCONFLICT*, VPERMB, VPSHUFBITQMB, VPOPCNT* | 충돌 탐지, 바이트 셔플, 비트 알고리즘 |
| AVX-512 BF16 / FP16 / IFMA / VP2INTERSECT | VDPBF16PS, VCVTNEPS2BF16, VCVTPH2PS, VPMADD52*, VP2INTERSECT* | 혼합 정밀도, 다정밀도 정수, 집합 교차 |
| AMX | TILELOADD, TILESTORED, TDPBUSD, TDPBF16PS | 행렬 타일 연산 |
| 영역 | 명령어 패밀리 | 세부 mnemonic 묶음 | 설명 |
|---|---|---|---|
| 연속 이동 | MOV* | MOVAPS/APD/UPS/UPD/DQA/DQU, VMOVDQU8/16/32/64 | 정렬/비정렬 로드와 스토어 |
| 스칼라 이동 | MOVSS/MOVSD | MOVSS, VMOVSS, MOVSD, VMOVSD | low 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 입출력(I/O) |
| 정수 산술 | PADD*/PSUB* | PADDB/W/D/Q, PSUBB/W/D/Q, VPADD*, VPSUB* | 기본 레인별 정수 산술 |
| 포화/평균/절댓값 | PADDS*/PAVG/PABS/PSIGN | PADDSB/SW, PADDUSB/USW, PAVGB/W, PABSB/W/D, PSIGNB/W/D | DSP와 이미지 처리 핵심 |
| min/max/비교 선택 | PMIN*/PMAX*, MINPS/MAXPS | PMINSB/PMAXUB, VMINPS, VMAXPD, VMINSS/VMAXSD | 클램프, 임계값 분기 제거 |
| 곱셈과 점곱 | PMUL*/PMADD*/VPDP* | PMULLW/HW, PMULDQ, PMADDWD, PMADDUBSW, VPDPBUSD, VDPBF16PS | 고정소수점, INT8, BF16 추론 |
| 큰 정수 | VPMADD52* | VPMADD52LUQ, VPMADD52HUQ | RSA/ECC limb 누적 |
| 논리/비트 | PAND/POR/PXOR/VPTERNLOG | PAND, PANDN, POR, PXOR, VPTERNLOGD/Q, KAND/KOR/KXOR/KNOT | 비트슬라이스, mask 조합 |
| 시프트/회전 | PSLL/PSRL/PSRA/PROL/PROR | PSLLW/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/PTEST | PCMPEQB/W/D/Q, PCMPGT*, VPCMP*, PTEST, VTESTPS/PD | 조건 생성과 빠른 zero/all 검사 |
| 문자열/텍스트 | PCMPESTR*/PCMPISTR* | PCMPESTRI/M, PCMPISTRI/M, MOVMSK | memchr, strcmp, delimiter 탐색 |
| 수평/축소 | HADD/HSUB/DPPS | HADDPS/PD, HSUBPS/PD, DPPS/DPPD, PHADDW/D | 리덕션 보조 연산 |
| FP 기본 연산 | ADD/MUL/SUB/DIV/SQRT/RCP/RSQRT | ADDPS/PD/SS/SD, MUL*, DIV*, SQRT*, RCPPS, RSQRTPS | packed/scalar FP 전 범위 |
| FMA와 반올림 | VFMADD*/ROUND*/VFPCLASS | VFMADD132/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/scatter | VGATHER*/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/GFNI | AESENC/DEC, VAESENC, PCLMULQDQ, SHA1*/SHA256*, CRC32, GFNI | dm-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는 행렬 타일을 별도 레지스터 공간으로 분리합니다.
| 영역 | NEON | SVE / SVE2 / SME | 설명 |
|---|---|---|---|
| 연속 로드/스토어 | LD1/ST1 | LD1B/H/W/D, ST1B/H/W/D | 기본 unit-stride 벡터 접근 |
| 구조화 로드/스토어 | LD2/LD3/LD4, ST2/3/4 | LD2B/LD3W/LD4D, ST2B 등 | 인터리브 데이터 분리/결합 |
| 산술 | ADD/SUB/MUL/MLA/FMLA | ADD, SUB, MUL, MLA, FMLA, MAD | 정수와 FP 기본 연산 |
| 포화 산술 | SQADD/UQADD, SQSUB/UQSUB | SQADD/UQADD, SQSUB/UQSUB | 클램프가 필요한 DSP 패턴 |
| 고정소수점 곱 | SQDMULH, SQRDMULH | SQDMULH, SQRDMLAH, SQRDMLSH | Q-format 오디오/영상 처리 |
| widen | USHLL/SSHLL, UMLAL/SMLAL | UUNPKLO/HI, SUNPKLO/HI, UMLALB/T, SMLALB/T | 좁은 입력을 넓혀 누적 |
| narrow | SQXTN/UQXTN, FCVTN/FCVTL | SQXTNT, UQXTNT, FCVTNT | 넓은 결과를 저장 폭으로 축소 |
| 쌍별/리덕션 | ADDP, ADDV, SADALP, UADALP, FADDP | ADDV, FADDV, SMAXV, UMAXV, CLASTA/B | 합계, 최대/최소, prefix 종료 |
| 비교/선택 | CMEQ/CMGT/CMHI, BSL/BIT/BIF | CMP*, SEL, BRKA/BRKB, PTEST | 조건 생성과 mask merge |
| 비트 조작 | CNT, CMTST, RBIT, REV64 | EOR3, BCAX, BDEP/BEXT 유사 비트 조합, CNT 계열 | 암호와 비트셋 처리 |
| 셔플/전치 | ZIP1/2, TRN1/2, UZP1/2, EXT | ZIP/TRN/UZP/EXT/SPLICE | lane 재배열과 전치 |
| 테이블 룩업 | TBL/TBX | TBL, MATCH, HISTCNT | 바이트 분류, 문자열 검색, 히스토그램 |
| 점곱/복소수 | SDOT/UDOT, FCMLA | SDOT/UDOT, BFDOT, FDOT, FCMLA | INT8/BF16/복소수 DSP |
| gather/scatter | 일반 NEON은 부재 | LD1W/LD1D gather, ST1W/ST1D scatter | 희소 인덱스와 테이블 참조 |
| fault-first | 전용 명령 없음 | LDFF1B/H/W/D, RDFFR, SETFFR | page 경계 안전 탐색 |
| 압축/필터 | 수동 셔플 | COMPACT, SPLICE | 활성 요소 pack |
| predicate 제어 | 해당 없음 | PTRUE, PFALSE, WHILELT, WHILERW, CNTP | VLA 루프와 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, VSETVL | SEW, LMUL, VL, tail/mask 정책 결정 | SVE의 VL + predicate 초기화 |
| 연속 로드/스토어 | VLE8/16/32/64, VSE8/16/32/64 | unit-stride 메모리 접근 | VMOVDQU, LD1/ST1 |
| segmented | VLSEG2E*, VLSEG3E*, VLSEG4E*, VSSEG* | 구조체(Struct)/인터리브 데이터 분리 | LD2/LD4 |
| strided | VLSE*, VSSE* | 고정 stride 접근 | 수동 주소 계산 + load/store |
| indexed unordered | VLUXEI*, VSUXEI* | 순서 비보장 임의 주소 접근 | VGATHER/VSCATTER |
| indexed ordered | VLOXEI*, VSOXEI* | 프로그램 순서를 보장하는 indexed 접근 | ordered gather/scatter에 가까움 |
| fault-only-first | VLE8FF.V, VLE16FF.V, VLE32FF.V, VLE64FF.V | 첫 fault까지만 보장하는 문자열/탐색 로드 | LDFF1 |
| 정수 산술 | VADD, VSUB, VRSUB, VMUL, VDIV, VREM | 기본 정수 벡터 산술 | VPADD*, VMUL*, DIVPS의 정수판 |
| widen | VWADD, VWSUB, VWMUL, VWMACC* | 폭을 넓혀 누적 | PMADDWD, UMLAL/SMLAL |
| 포화/고정소수점 | VSADD(U), VSSUB(U), VSMUL, VNCLIP(U), VSSRL/VSSRA | DSP용 saturating과 rounded shift | PADDS*, SQADD, SQDMULH |
| 비교/mask 생성 | VMSEQ, VMSNE, VMSLT(U), VMSLE(U), VMSGT(U) | mask 벡터 생성 | VPCMP*, CMEQ/CMGT |
| mask 조작 | VMAND.MM, VMOR.MM, VMXOR.MM, VMNOT.M | mask 자체의 Boolean 연산 | KAND/KOR, predicate logic |
| mask 소비 | VCPOP.M, VFIRST.M, VIOTA.M, VID.V | popcount, first-hit, prefix index, lane id | POPCNT/TZCNT, CNTP |
| 조건부 merge | VMERGE.VVM, VMV.V.V | mask 기반 선택과 move | VPBLEND*, 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, VFNMACC | FP packed arithmetic | VADDPS, FMLA |
| 근사 역수 | VFREC7, VFRSQRT7 | 빠른 reciprocal/rsqrt seed | RCPPS, RSQRTPS |
| 부호/병합 | VFSGNJ, VFSGNJN, VFSGNJX, VFMERGE, VFMV | sign injection과 scalar merge | XOR-sign, BLEND |
| 암호 벡터 확장 | Zvkb, Zvkg, Zvkned, Zvknha, Zvknhb, Zvksed, Zvksh | 비트 조작, GCM, AES, SHA, SM4/SM3 | GFNI, AES-NI, SHA-NI |
RVV mnemonic 읽기 팁: vwmaccsu.vx라면 vw=widen, macc=multiply-accumulate, su=signed×unsigned 조합, .vx=벡터-스칼라 형태라는 뜻입니다. RVV는 이름만 읽어도 데이터 경로가 상당 부분 드러납니다.
문서 완전성 가이드
이 문서는 이제 단순한 SIMD 소개가 아니라, 명령어 패밀리별 레퍼런스로 읽히도록 구성되어 있습니다. 개별 예제는 성능과 커널 사용 규칙을 보여 주고, 완전 카탈로그는 빠진 mnemonic 없이 계열을 찾도록 돕습니다. 새로운 ISA 확장 문서를 읽을 때도 아래 순서로 접근하면 빠르게 구조를 잡을 수 있습니다.
- 확장 비트 확인 — x86은 CPUID/XCR0, ARM은 HWCAP/HWCAP2, RISC-V는 ISA 문자열과 HWCAP을 먼저 봅니다.
- 패밀리 확인 — move, compare, shuffle, widen, dot, reduce, gather 중 어느 영역인지 먼저 정합니다.
- 접미사 해독 — 데이터 폭, 부호, scalar/packed, mask 정책, indexed 형태를 읽습니다.
- upper-lane 규칙 확인 — x86 scalar VEX, SVE predicate merge, RVV tail/mask undisturbed처럼 결과 보존 규칙을 반드시 점검합니다.
- 커널 제약 확인 — FPU 상태, preemption, IRQ 컨텍스트, fault 가능 메모리 여부를 점검합니다.
참고 자료
SIMD를 실전 구현 관점에서 공부할 때는 먼저 커널 공식 문서로 사용 규칙을 확인하고, 다음으로 실제 커널 소스와 ISA 레퍼런스를 보는 순서가 효율적입니다. 아래 목록은 그 흐름에 맞춰 정리했습니다.
커널 공식 문서
- 커널 공식 문서 — Floating-point API: kernel_fpu_begin()/kernel_fpu_end(), 빌드 플래그, FP 임계 구간 규칙
- 커널 공식 문서 — Proper Locking Under a Preemptible Kernel: preemption과 FPU 보호 규칙
- 커널 공식 문서 — Porting an architecture to support PREEMPT_RT: RT 커널에서의 FPU/SIMD 접근 규칙
- 커널 공식 문서 — x86 XSTATE: XSAVE, AVX-512, AMX 상태 관리
- 커널 공식 문서 — Kernel mode NEON: ARM 커널 모드 NEON/VFP 사용 규칙
- 커널 공식 문서 — Scalable Vector Extension support for AArch64 Linux
- 커널 공식 문서 — Scalable Matrix Extension support for AArch64 Linux
커널 소스 코드
- 커널 소스 — arch/x86/include/asm/fpu/api.h FPU API 헤더 (Bootlin Elixir)
- 커널 소스 — arch/x86/kernel/fpu/core.c x86 FPU/XSTATE 핵심 구현 (Bootlin Elixir)
- 커널 소스 — arch/x86/crypto x86 SIMD 암호화 구현 모음 (Bootlin Elixir)
- 커널 소스 — arch/arm64/crypto ARM64 NEON/SVE 암호화 구현 모음 (Bootlin Elixir)
- 커널 소스 — lib/crypto/aegis128-neon-inner.c NEON 최적화 예제 (Bootlin Elixir)
ISA와 벤더 레퍼런스
- Intel Intrinsics Guide — SSE/AVX/AVX-512 인트린직 함수 레퍼런스
- Intel SDM — Intel 64 and IA-32 Architectures Software Developer's Manual
- ARM NEON 공식 문서 — Arm Neon Intrinsics Reference
- ARM SVE 프로그래밍 가이드 — Arm Scalable Vector Extension Programming Guide
- RISC-V 공식 사양 — "V" Standard Extension for Vector Operations, Version 1.0
해설과 배경 자료
- LWN.net — Kernel FPU state handling
- LWN.net — AMX support for the kernel
- LWN.net — A new direction for SIMD in the kernel
관련 문서
SIMD와 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.