Intrinsics and Vector Types
Intrinsics and Vector Types
Intrinsics and Vector Types
Tập trung vào thiết lập môi trường, các thanh ghi SIMD, cú pháp intrinsics, và cách dùng vector extensions trong GCC để viết mã ngắn gọn, dễ hiểu hơn.
Chuẩn bị môi trường
-
Xác định CPU hỗ trợ SIMD:
- Trên Linux: cat /proc/cpuinfo để xem các cờ (flags).
- Trong C++ có thể dùng __builtin_cpu_supports(“avx2”) để kiểm tra.
-
Thêm header:
- Dùng <x86intrin.h> để có tất cả intrinsics.
- Khai báo compiler flags:
#pragma GCC target("avx2")-march=nativeđể tự động nhận kiến trúc CPU.
-
Các kiểu dữ liệu: __m128 (__m128d, __m128i), __m256, __m512
-
Intrinsics: Intrinsics là hàm C ánh xạ trực tiếp tới lệnh assembly, ví dụ cộng hai mảng double bằng AVX2:
for (int i = 0; i < 100; i += 4) {__m256d x = _mm256_loadu_pd(&a[i]);__m256d y = _mm256_loadu_pd(&b[i]);__m256d z = _mm256_add_pd(x, y);_mm256_storeu_pd(&c[i], z);}
Vấn đề thường gặp: mảng không chia hết cho block size.
- Giải pháp 1: pad thêm phần tử “trung tính” (ví dụ 0).
- Giải pháp 2: xử lý phần dư bằng vòng lặp thường.
Con người thích lựa chọn số 1, vì đơn giản hơn và giúp giảm code. Còn các compiler thì thích lựa chọn số 2, vì không có lựa chọn nào khác.
Instruction References
Cấu trúc: _mm<size>_<action>_<type>.
- _mm_add_epi16: cộng vector 128-bit gồm các số nguyên 16-bit.
- _mm256_ceil_pd: làm tròn lên 4 số double.
- _mm256_blendv_ps: trộn hai vector theo mask.
Bài tập
Câu hỏi tư duy
- Phân biệt SSE, AVX, AVX2, AVX-512 về vector width, số lượng register, instruction set khác biệt.
- Tại sao
_mm256_load_pd(aligned) tồn tại bên cạnh_mm256_loadu_pd(unaligned)? Khác biệt thực tế trên CPU hiện đại? - Auto-vectorize của compiler vs viết intrinsics tay — khi nào nên chọn cái nào?
- AVX-512 trên Intel có “downclocking” issue. Giải thích và khi nào nên/không nên dùng AVX-512.
- Horizontal sum (cộng tất cả lane trong vector) tại sao chậm hơn vertical add? Ví dụ: dùng
_mm256_add_ps7 lần để sum 8 float vs cách reduction tree.
Bài tập code
Bài 1: Viết AVX2 vector dot product:
float dot_avx(const float* a, const float* b, int n);Handle phần dư (n % 8 ≠ 0). Compare với scalar version.
Bài 2: SAXPY: y[i] = a*x[i] + y[i]. Implement dùng FMA intrinsic _mm256_fmadd_ps. Đo speedup vs scalar.
Bài 3: Implement int min(int* arr, int n) dùng AVX2. Tip: dùng _mm256_min_epi32 cho lane-wise min, sau đó reduction.
Đáp án
Câu hỏi tư duy
-
SSE (128-bit, 4 float): từ Pentium III. AVX (256-bit, 8 float): Sandy Bridge 2011, chỉ FP. AVX2 (256-bit integer): Haswell 2013, FMA. AVX-512 (512-bit, 16 float): Skylake-X 2017, mask register
k0-k7. Số ZMM register = 32 trên AVX-512 (vs 16 YMM trên AVX2). AVX-512 cũng có gather/scatter, nhiều specialized instruction. -
Trên Haswell+,
loadvàloaducùng latency/throughput nếu data thực sự aligned. Dùngloaduluôn an toàn hơn (no SIGSEGV nếu data lệch).loadaligned crash khi data không align 32 byte → AVX. Best practice 2020+: dùngloaduluôn. -
Auto-vectorize:
- Pros: portable, tự update khi CPU mới, ngắn gọn.
- Cons: phụ thuộc compiler, có thể fail vì aliasing/alignment unknown, không control được layout. Intrinsics:
- Pros: control hoàn toàn, predictable performance, tận dụng instruction đặc biệt (mask, gather).
- Cons: verbose, không portable across arch (x86 vs ARM NEON), phải viết lại khi đổi width.
Quy tắc: thử auto-vectorize trước (
-O3 -march=native), check assembly. Nếu compiler fail hoặc cần kỹ thuật đặc biệt → intrinsics.
-
AVX-512 trên Intel Skylake-X/Cascade Lake: khi execute heavy AVX-512 instruction, CPU giảm clock 100-300MHz để giữ TDP. Khởi động lại lên full clock mất ~700µs. → Nếu chỉ một vài AVX-512 instruction lẻ tẻ, lợi không bù lỗ. Dùng AVX-512 khi: hot loop chạy hàng triệu iter, không có code khác xen kẽ. Trên Ice Lake/Tiger Lake: downclock giảm mạnh, AVX-512 thực dụng hơn.
-
Vertical add (
add_ps): mỗi lane độc lập → 1 instruction handle 8 lane song song. Horizontal sum: cần lane shuffle giữa các lane →hadd_ps,extractf128_ps,add→ 5–7 instruction tổng. Reduction tree:v = [a,b,c,d,e,f,g,h]v_hi + v_lo = [a+e,b+f,c+g,d+h] (extractf128 + add)shuffle + add → [a+c+e+g, b+d+f+h, ...]shuffle + add → [a+b+c+...+h, ...]Tổng ~3 add + 3 shuffle = 6 instruction cho 8-element sum. Nếu loop có nhiều element, nên accumulate vector, horizontal sum chỉ ở cuối.
Bài tập code
Bài 1 — dot product:
#include <immintrin.h>
float dot_avx(const float* a, const float* b, int n) { __m256 sum = _mm256_setzero_ps(); int i = 0; for (; i + 8 <= n; i += 8) { __m256 va = _mm256_loadu_ps(a + i); __m256 vb = _mm256_loadu_ps(b + i); sum = _mm256_fmadd_ps(va, vb, sum); // sum += va*vb } // Horizontal reduce __m128 lo = _mm256_castps256_ps128(sum); __m128 hi = _mm256_extractf128_ps(sum, 1); __m128 v4 = _mm_add_ps(lo, hi); // 4 lanes __m128 v2 = _mm_add_ps(v4, _mm_movehl_ps(v4, v4)); __m128 v1 = _mm_add_ss(v2, _mm_shuffle_ps(v2, v2, 1)); float total = _mm_cvtss_f32(v1); // Tail for (; i < n; i++) total += a[i] * b[i]; return total;}Speedup vs scalar: 4–8x. Compiler -O3 -mavx2 -mfma thường auto-vectorize dot product tốt → diff < 2x. Intrinsics thắng khi: pattern không match auto-vec, hoặc cần xử lý mask/gather.
Bài 2 — SAXPY:
void saxpy_avx(float a, const float* x, float* y, int n) { __m256 va = _mm256_set1_ps(a); int i = 0; for (; i + 8 <= n; i += 8) { __m256 vx = _mm256_loadu_ps(x + i); __m256 vy = _mm256_loadu_ps(y + i); vy = _mm256_fmadd_ps(va, vx, vy); _mm256_storeu_ps(y + i, vy); } for (; i < n; i++) y[i] = a*x[i] + y[i];}FMA: 1 instruction = multiply + add (kèm rounding chỉ 1 lần — chính xác hơn). Speedup vs scalar without FMA: 8x SIMD × 2x FMA = 16x lý thuyết. Thực tế memory-bound → ~5–8x. Khi n lớn, bottleneck là memory bandwidth.
Bài 3 — vector min:
int min_avx(const int* arr, int n) { __m256i vmin = _mm256_set1_epi32(INT_MAX); int i = 0; for (; i + 8 <= n; i += 8) { __m256i v = _mm256_loadu_si256((__m256i*)(arr + i)); vmin = _mm256_min_epi32(vmin, v); } // Reduce 8 lanes -> 1 int tmp[8]; _mm256_storeu_si256((__m256i*)tmp, vmin); int m = INT_MAX; for (int j = 0; j < 8; j++) if (tmp[j] < m) m = tmp[j]; for (; i < n; i++) if (arr[i] < m) m = arr[i]; return m;}Hoặc reduction tree thuần SIMD:
__m128i lo = _mm256_castsi256_si128(vmin);__m128i hi = _mm256_extracti128_si256(vmin, 1);__m128i m4 = _mm_min_epi32(lo, hi);__m128i m2 = _mm_min_epi32(m4, _mm_shuffle_epi32(m4, _MM_SHUFFLE(1,0,3,2)));__m128i m1 = _mm_min_epi32(m2, _mm_shuffle_epi32(m2, _MM_SHUFFLE(0,0,0,1)));int result = _mm_cvtsi128_si32(m1);
Discussion
Sign in with GitHub to comment or react. Powered by giscus.