HPC openMP AVX2
Introduction HPC
Sequential programs(串行程序)
Parallel programs(并行程序)
OpenMP
OpenMP execution model
fork - join 并行模式
只有一个master thread ,程序开始(单线程)- fork - parallel - join
OpenMP memory model
All threads have access to the same **shared memory space **所有线程共享同意内存空间
OpenMP derectives
1 |
- #pragma omp :告诉编译器这是一个 OpenMP 指令。
:具体的并行控制指令。 - clause :附加选项(子句),比如 private、reduction、schedule。
指令
parallel
表是这段代码被多个线程并行执行
1 |
|
for
用于for循环之前,将循环分配到多个线程执行,必须保证每次循环之间无相关性!
1 |
|
parallel for
等价于 parallel + for
1 |
|
sections
把不同的代码段(section)分配给不同的线程去执行,每个 section 只会执行一次。
相当于 任务并行(task parallelism),而不是 数据并行(data parallelism)。
1 |
|
parallel sections
single
在一个并行区域里,只允许一个线程执行这段代码,而其他线程 跳过它,继续往下执行。
1 |
|
barrier
让所有线程在这里集合(同步点),只有当 所有线程都到达这个位置 时,才会一起继续往下执行。
1 |
|
- part A 部分的输出是乱序的(线程独立执行)。
- 但是所有线程必须 等到 barrier,再一起进入 part B。
- 所以 part B 的内容永远不会早于任何一个 part A。
nowait
取消隐式的 barrier,也就是不让线程在循环或指令后等待其他线程完成,而是直接继续执行后续代码。
1 |
|
1 |
|
- 当循环后面的代码不依赖于当前循环的完成结果时,可以加 nowait 提高并行效率。
- 如果后续代码依赖当前循环的结果,不能用 nowait,否则会出现数据竞争或错误。
if
条件性地决定是否在并行区域创建线程。也就是说,可以根据一个布尔条件来选择执行并行版本还是串行版本。
1 |
|
atomic
保证某个内存操作是原子的(不可分割的),防止多个线程同时修改同一个变量时产生 竞争条件 (race condition)。
但只能管一行语句
1 |
|
critical
作用等同于atomic,用于一段代码,保证一段代码 同一时刻只允许一个线程进入
1 | int sum = 0; |
- single VS critical :
- single = “只做一次,随便哪个线程做就行”
- critical = “大家都要做,但得一个一个排队做,不能一起做”
master
指定一段代码由主线程(thread 0)执行,其余线程跳过,不等待
1 |
|
只有主线程会执行里面的printf, 其他线程都执行外面的printf
orderd
用来保证在并行循环中某些操作按循环迭代的顺序执行,即使循环是并行执行的。
- 当你用 #pragma omp for 并行循环时,每个迭代可能被不同线程同时执行。
- 如果某些操作必须严格按照迭代顺序执行(比如输出、累加到共享数组),就可以使用 ordered。
- 必须在 for 循环中加上 ordered 子句,然后在循环体内用 #pragma omp ordered 标记需要按顺序执行的部分。
1 |
|
子句
shared
指定变量在多个线程间共享。
1 | int sum = 0; |
private / firstprivate / lastprivate
- private(var):每个线程有独立的 var 副本,原来的共享变量不会被线程修改。
- firstprivate(var):每个线程有独立副本,并初始化为原来的值。
- lastprivate(var):循环结束后,最后一次迭代(代码理论上的最后一次)的线程的 var 值写回原来的共享变量。
1 | int x = 10; |
1 | // lastprivate |
threadprivate
用于声明每个线程都有自己独立的全局或静态变量副本,每个线程访问的是自己独立的副本,互不干扰。
- 默认情况下,全局变量是所有线程共享的。
- 如果你希望每个线程有自己的独立版本,可以用 threadprivate。
- 和局部变量不同,局部变量天然就是线程私有的;threadprivate 针对的是全局或 static 变量。
1 |
|
default
- 设置并行区域中变量的默认属性:
- default(none):所有变量必须显式声明 shared 或 private,安全
- default(shared):未声明的变量默认共享
- default(private):未声明的变量默认私有
1 | int x, y; |
reduction
- 对共享变量进行归约操作(累加、乘积、最大值、最小值等),保证结果正确。
1 | int sum = 0; |
copyin
用来给 线程私有的全局或静态变量(用 threadprivate 声明) 初始化值。它把 主线程(thread 0)的值复制到所有线程的私有副本。
换句话说,threadprivate 声明的变量每个线程都有自己的副本,而 copyin 可以用来统一初始化这些副本。
1 |
|
- counter 是 threadprivate,每个线程有独立副本
- copyin(counter) 把主线程的值(42)复制给每个线程的副本
- 每个线程修改自己的 counter 不会影响其他线程
- 主线程的 counter 保持原值
copyprivate
- copyprivate 是 OpenMP 的 single 子句的一部分。
- 作用:在 parallel 区域中,single 区块由一个线程执行,但这个线程的变量值可以复制给其他线程。
- 通常配合 #pragma omp single 使用。
1 |
|
collapse
collapse(n) 用来 把 n 层嵌套循环合并为一个大的迭代空间,再分配给线程执行。
1 | // 没有collapse(n) |
1 | // 有collapse |
- collapse = 把多层循环“摊平”成一层迭代空间
- 适合嵌套循环层次少,迭代次数不够多的情况
- 可以与 schedule 结合,提高负载均衡
任务调度
schedule
- schedule 是 OpenMP 的 for 或 parallel for 子句的一部分,用来控制 循环迭代分配给线程的策略。
- 主要作用:决定线程如何分配迭代任务,以及每次分配多少个迭代。
- type:static, dynamic, guided, runtime(真正意义上不算)
- chunck_size:块大小
1 | schedule(type[,chunk_size]) |
| 类型 | 说明 | 示例 |
|---|---|---|
| static | 循环迭代 均匀分块 分配给线程。默认 chunk_size = N / num_threads | #pragma omp for schedule(static) |
| static, chunk_size | 每个线程一次分配 chunk_size 个迭代,按顺序循环分配 | #pragma omp for schedule(static, 2) |
| dynamic | 循环迭代 动态分配,线程完成后再拿新的迭代块。适合负载不均的循环 | #pragma omp for schedule(dynamic, 3) |
| guided | 每次分配迭代块大小 逐渐减小,前期大块,后期小块。如果不指定chunk size,最小可以到1 | #pragma omp for schedule(guided, 2) |
| auto | 让编译器/运行时自动选择调度策略 | #pragma omp for schedule(auto) |
| runtime | 运行时根据环境变量 OMP_SCHEDULE 决定 | #pragma omp for schedule(runtime) |
- static:负载均匀,开销小
- dynamic:负载不均,线程可以“抢活”,开销略大
- guided:负载递减型动态分配
- auto / runtime:交给编译器或环境变量控制
task
- Task 是 OpenMP 的一个并行单位,表示一段可以延迟执行的代码块。
- 可以想象成一个 “工作单元”,由线程池中的线程去执行。
- 与 parallel for 不同,它不一定是循环的迭代,也可以是任意代码块。
- Task 可以有 依赖关系(depend),保证任务之间的执行顺序。
特点:
- 延迟执行:创建 task 时,OpenMP 不会立即执行它,而是放入任务队列等待线程执行。
- 灵活性高:可以表示任意代码,不只是循环。
- 线程复用:多个 task 可以被线程池中的空闲线程执行。
1 |
|
- single 保证任务只被创建一次。
- 任务可以被任意空闲线程执行。
depend
在计算前缀和、Fibonacci 或矩阵块操作中,任务可能需要依赖其他任务完成的结果。
1 |
|
- in: 表示依赖输入,任务必须等待这些数据准备好。
- out: 表示任务会写这个数据。
- inout: 表示任务既读又写。
1 | // 二维前缀和 |
taskwait
用于 等待当前线程生成的所有子任务完成,然后再继续往下执行。
- 它只等待 同一线程创建的任务。
- 不会等待其他线程创建的任务(除非它们是依赖任务)。
- 放在并行区域或单线程区域中。
- 后面不跟任何语句,只是一个同步点。
1 |
|
环境变量与 API 对照表
| 功能类别 | 环境变量 | 对应 API(代码里设置) | 说明 |
|---|---|---|---|
| 线程数 | OMP_NUM_THREADS | omp_set_num_threads(int n) omp_get_num_threads() | 控制并行区域的线程数 |
| 动态线程调整 | OMP_DYNAMIC (TRUE/FALSE) | omp_set_dynamic(int flag) omp_get_dynamic() | 是否允许运行时调整线程数 |
| 线程总上限 | OMP_THREAD_LIMIT | omp_get_thread_limit() | 限制整个程序最多线程数 |
| 嵌套并行 | OMP_NESTED(旧标准) OMP_MAX_ACTIVE_LEVELS | omp_set_max_active_levels(int levels) omp_get_max_active_levels() | 设置并行嵌套层数 |
| 循环调度 | OMP_SCHEDULE | omp_set_schedule(omp_sched_t kind, int chunk) omp_get_schedule(&kind, &chunk) | 控制 for 循环调度策略 |
| 绑定和亲和性 | OMP_PROC_BIND | omp_get_proc_bind() | 控制线程是否绑定到 CPU |
| OMP_PLACES | 无(环境变量指定) | 线程可绑定的位置(cores/threads/sockets) | |
| 任务 | OMP_MAX_TASK_PRIORITY | 无 | 控制任务最大优先级 |
| 调试 & 性能 | OMP_DISPLAY_ENV (TRUE/FALSE) | 无 | 是否显示 OpenMP 环境设置 |
| OMP_WAIT_POLICY (ACTIVE/PASSIVE) | 无 | 控制空闲线程等待策略 | |
| OMP_CANCELLATION (TRUE/FALSE) | omp_get_cancellation() | 是否允许取消任务/循环 |
1 | export OMP_NUM_THREADS=8 |
OpenMP 常用函数
1.线程管理
| 函数 | 说明 |
|---|---|
| omp_set_num_threads(int n) | 设置并行区域中的线程数(仅对后续并行区有效) |
| omp_get_num_threads() | 获取当前并行区域的线程数 |
| omp_get_thread_num() | 获取当前线程编号(0 ~ n-1) |
| omp_get_max_threads() | 返回可能使用的最大线程数 |
| omp_set_dynamic(int flag) | 开启/关闭动态调整线程数 |
| omp_get_dynamic() | 判断是否允许动态调整线程数 |
| omp_get_thread_limit() | 获取程序允许的最大线程数 |
2.并行控制
| 函数 | 说明 |
|---|---|
| omp_in_parallel() | 判断当前是否在并行区域内 |
| omp_set_nested(int flag) (旧) | 设置是否支持嵌套并行(OMP 4.0 前) |
| omp_set_max_active_levels(int levels) | 设置最大并行嵌套层数 |
| omp_get_max_active_levels() | 获取最大嵌套层数 |
| omp_get_level() | 获取当前并行嵌套层数 |
| omp_get_ancestor_thread_num(int level) | 获取某层的祖先线程编号 |
3.调度与任务
| 函数 | 说明 |
|---|---|
| omp_set_schedule(omp_sched_t kind, int chunk) | 设置循环调度策略(static, dynamic, guided, auto) |
| omp_get_schedule(omp_sched_t *kind, int *chunk) | 获取当前调度策略和 chunk 大小 |
| omp_get_wtime() | 获取墙钟时间(高精度计时) |
| omp_get_wtick() | 获取 omp_get_wtime() 的精度 |
| omp_get_num_procs() | 获取可用的处理器数 |
| omp_get_proc_bind() | 获取线程绑定策略 |
4.任务与取消
| 函数 | 说明 |
|---|---|
| omp_get_cancellation() | 是否支持取消任务 |
| omp_get_default_device() | 获取默认设备(比如 GPU 设备 ID) |
| omp_set_default_device(int device_num) | 设置默认设备 |
5.锁(同步机制)
| 函数 | 说明 |
|---|---|
| omp_init_lock(omp_lock_t *lock) | 初始化锁 |
| omp_destroy_lock(omp_lock_t *lock) | 销毁锁 |
| omp_set_lock(omp_lock_t *lock) | 获取锁(阻塞等待) |
| omp_unset_lock(omp_lock_t *lock) | 释放锁 |
| omp_test_lock(omp_lock_t *lock) | 尝试获取锁(不阻塞,立即返回) |
1 |
|
OpenMP 编程常见坑点总结
1.循环迭代变量没声明为 private
写了平行区域 + for 循环,但是忘了加 private(i) → 会发生数据竞争。
1 |
|
- 这里 i 是 共享的(shared),所有线程同时用 同一个变量 i。
- 当多个线程同时修改 i,就会发生 数据竞争 (race condition)。
- English: The loop variable i is shared across threads, so multiple threads update it at the same time, causing a race condition.
解决办法,加上private(i) 或者使用parallel for(会自动把 i 设置成private)
1 |
|
2.数据竞争(race condition)
- 多个线程同时写共享变量 → 结果不确定。
- 常见于计数、累加。
- English: Shared variables modified by multiple threads without synchronization will lead to unpredictable results.
1 | int sum = 0; |
3.reduction
忘记 reduce
- 很多人写并行求和、最值时直接 shared → 结果错误。
- English: Without reduction, all threads write to the same shared variable at the same time, causing wrong results.
1 | int sum = 0; |
reduction 忘记初始化
- reduction 会在每个线程里生成 sum 的副本,但如果 sum 没初始化,结果是随机的。
- English: If the reduction variable is not initialized, each thread’s private copy starts with an undefined value, leading to incorrect results.
1 | int sum; |
1 | // ✅正确写法 |
4.barrier / nowait 使用不当
- 默认 for 和 sections 结束时会有隐式 barrier。
- 如果用 nowait,要小心后续代码是否需要同步。
- English: Using nowait skips synchronization, so some threads may read data before others finish writing.
1 |
|
- 所以有些线程可能还在写 A[i],但别的线程已经跑到 printf 了。
- 结果就可能访问到 未初始化或部分更新的 A,产生 竞态条件 (race condition)。
去掉 nowait(默认会有 barrier,同步所有线程):
1 |
|
保留 nowait,但手动加 barrier:
1 |
|
5.task 忘记加 taskwait
- 如果 task 有依赖关系,必须手动同步。
- English: Without taskwait, the program may continue before all tasks have finished, leading to incomplete results.
1 |
|
6.循环嵌套没有 collapse
- 多重循环并行时,只 parallel 外层 → 内层仍是串行。
- English: Without collapse, only the outer loop is parallelized, so the workload may not be fully distributed.
1 | #pragma omp parallel for |
7. schedule 不当
- schedule(static) 适合工作量均匀的循环;
- schedule(dynamic, chunk) 适合负载不均的循环;
- 乱用可能导致线程负载不平衡。
- English: Using the wrong scheduling strategy can cause load imbalance, where some threads finish early and others are overloaded.
8.乱用 critical / atomic
critical 会严重拖慢性能,如果只是单行更新 → 用 atomic。
English: critical forces threads to serialize access, which is slower; if only a single statement is updated, atomic is better.
1 |
|
9.输出打印错乱
- printf 是共享 I/O,多个线程同时写会交错。
- English: Standard output is shared; multiple threads writing at the same time will interleave their messages.
- 如果要有序输出,用 ordered 或加锁。
1 |
|
10.忘记设置默认共享/私有变量(default 子句)
x 是共享变量(shared),但被多个线程同时修改
x is a shared variable, but it is being modified simultaneously by multiple threads.
1 | int x = 10, y = 20; |
- ✅解决办法,明确写出
1 |
1 | // 每个线程有自己的 x 副本: |
11. PPT
#pragma omp parallel for num_threads(P) for (int i = 0; i < N; i++) { #pragma omp task h(i); }#pragma omp parallel num_threads(P) { #pragma omp single { for (int i = 0; i < N; i++) { #pragma omp task h(i); } #pragma omp taskwait // 等待所有任务完成 } }1
2
3
4
5
6
7
- 在 OpenMP 中,如果没有明确的同步机制(如 `taskwait` 或 `barrier`),父任务(在这里是循环迭代)可以在子任务(h(i))完成前就结束。
- 当所有循环迭代完成后,parallel 区域会立即结束。此时,可能有许多任务(h(i))尚未执行完成。
- Explicit synchronization mechanisms (such as taskwait or barrier) cause the parallel region to end immediately after all loop iterations are completed. At this point, many tasks (h(i)) may not have finished executing.
**在 single 区域中创建任务并等待**:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
## AVX2
SIMD(Single Instruction, Multiple Data)单指令多数据
#### 指令分类体系
AVX2 指令可以按功能和数据类型分为以下几大类:
1. **数据移动指令**
- 加载:从内存到寄存器
- 存储:从寄存器到内存
- 设置:直接设置寄存器值
- 混洗:在寄存器内部重新排列数据
2. **算术运算指令**
- 基本算术:加、减、乘
- 融合乘加:FMA 操作
- 特殊算术:绝对值、最大值、最小值
3. **逻辑运算指令**
- 按位与、或、异或、非
4. **比较指令**
- 等于、大于、小于等比较操作
5. **移位指令**
- 逻辑移位、算术移位
6. **类型转换指令**
- 不同数据类型间的转换
**Intel Intrinsics Guide**:https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html
| **片段** | **英文缩写来源** | **含义** | **例子** |
| --------------------------------- | ---------------- | -------------------------------- | ------------------------- |
| _mm | “multimedia” | 表示 SSE/AVX 指令 | _mm_add_ps, _mm256_mul_pd |
| 128 / 256 / 512 | 位宽 | 使用的寄存器宽度 | __m128, __m256, __m512 |
| add, mul, sub, div, and, or, xor… | 运算名 | 算术或逻辑操作 | _mm256_add_ps(加法) |
| ps | Packed Single | 8 个 float(单精度) | |
| pd | Packed Double | 4 个 double(双精度) | |
| epi8 / epi16 / epi32 / epi64 | Packed Integer | 8 位 / 16 位 / 32 位 / 64 位整数 | |
```c
// 以 _mm256_add_epi32 为例:
__m256i _mm256_add_epi32 (__m256i a, __m256i b)
// 返回类型:__m256i
// 函数名:_mm256_ + 操作(add) + 数据类型(epi32)
// 参数:两个 __m256i 向量具体操作
_mm256_add_epi32
| | | └── 每个元素是 32 位整数
| | └── 操作:加法
| └── 使用 256 位寄存器
└── SIMD 内建函数(多媒体扩展)
| 符号 | 英文全称 | 含义 | 示例 |
|---|---|---|---|
| s | single | 单精度浮点数(float, 32-bit) |
_mm256_add_ps |
| d | double | 双精度浮点数(double, 64-bit) |
_mm256_add_pd |
| i | integer | 整数类型 | _mm256_add_epi32 |
| p | packed | “打包”操作(向量运算,多个元素同时操作) | _mm256_add_ps(8个float) |
| s (第二个) | scalar | 标量操作,只作用于第一个元素 | _mm_add_ss(仅第一个float) |
| u | unaligned | 不对齐内存访问 | _mm256_loadu_ps |
| l | low | 操作低半部分(低位元素) | _mm_unpacklo_ps |
| h | high | 操作高半部分(高位元素) | _mm_unpackhi_ps |
| r | reversed | 按相反顺序排列元素 | _mm256_permute2f128_ps(..., ..., 1) |
具体指令
1.1 加载内存(load、loadu)
- 把内存中的数据读取到CPU 的 SIMD 寄存器中
1 |
|
对齐是什么?
- 内存是怎么存数据的?
我们知道内存的地址是一个一个字节排列的,比如:
1 | 0x1000, 0x1001, 0x1002, 0x1003, 0x1004, ... |
每个地址存放一个字节(byte)。
假设我们有一个 int(4字节),它可能存放在:
1 | 地址 0x1000 ~ 0x1003 |
对齐的核心概念
对齐(alignment) 就是让数据的起始地址是它大小的整数倍。
假设一行内存是 64 字节(两个 32 字节块):
1 | |<-------- 32 字节对齐区域 1 -------->|<-------- 32 字节对齐区域 2 -------->| |
如果数据正好从 0x1000 开始,就是对齐的 ✅
如果数据从 0x1004 开始,就会跨越两个 32 字节块 ❌
- 举例 :AVX 256位寄存器(32 字节)对齐 = 起始地址 % 32 == 0
因为 AVX2 的寄存器一次处理 256 位 = 32 字节,
所以我们希望数据的地址是 32 的倍数。
| 地址 | 是否 32 字节对齐 |
|---|---|
| 0x1000 | ✅ 对齐(1000 mod 32 = 0) |
| 0x1010 | ❌ 不对齐(1010 mod 32 = 16) |
| 0x1020 | ✅ 对齐(1020 mod 32 = 0) |
CPU 取数据时,就得访问两次内存(性能下降,甚至报错)。
- C语言中的内存对齐实现
你可以通过两种方式保证数组对齐:
方式 1:使用 GCC/Clang 属性
1 | int x __attribute__((aligned(32))); |
这行代码告诉编译器:
“请让 aligned_data 的起始地址是 32 的倍数。”
你可以验证:
1 |
|
输出示例:
1 | 地址: 0x7ffeefbff480 |
- 方式 2:使用 _mm_malloc
1 | type* x = (type*) _mm_malloc(size * sizeof(type), 32); |
1 |
|
这样分配的内存一定是 32 字节对齐的。
1.2 创建向量
1 | void set_instructions_demo() { |
1.3 储存指令(store)
- 把 CPU 寄存器中的数据 写回到 内存
1 | void store_instructions_demo() { |
2.1 基本算术运算
1 | void arithmetic_demo() { |
- 数组求和
1 | int32_t sum_array_avx2(int32_t* array, size_t size) { //size得是8的倍数 |
2.2 逻辑运算
命名规则总结
| 结构 | 含义 |
|---|---|
| mm256 | 使用 AVX 256-bit 宽寄存器 |
| cmp | compare 比较 |
| eq / gt / lt | 操作类型(相等、大于、小于) |
| epi32 / ps / pd | 数据类型(int32 / float / double) |
浮点数比较函数族
(_mm256_cmp_ps / _mm256_cmp_pd)
1 | __m256 _mm256_cmp_ps (__m256 A, __m256 B, const int op); // 单精度 float |
- 参数 A, B :要比较的两个向量。
- 参数 op :指定比较操作(见下表)。
- 返回值:一个掩码向量,每个元素要么是 0xFFFFFFFF(真),要么是 0x00000000(假)。
✅ 常用比较操作(op取值)
| 常量名 | 含义 | 说明 |
|---|---|---|
| _CMP_EQ_OQ | A == B | 相等 |
| _CMP_LT_OS | A < B | 小于 |
| _CMP_LE_OS | A ≤ B | 小于等于 |
| _CMP_GT_OS | A > B | 大于 |
| _CMP_GE_OS | A ≥ B | 大于等于 |
| _CMP_NEQ_UQ | A != B | 不相等 |
| _CMP_ORD_Q | 两者都不是 NaN | 有序比较 |
| _CMP_UNORD_Q | 任一为 NaN | 无序比较 |
- OS 表示 Ordered Signaling(有序比较,遇到 NaN 会抛异常)
- OQ 表示 Ordered Quiet(遇到 NaN 不会抛异常)
Exemple:
1 | __m256 A = _mm256_set_ps(8,7,6,5,4,3,2,1); |
结果:
1 | mask = [FFFFFFFF, FFFFFFFF, FFFFFFFF, FFFFFFFF, 00000000, 00000000, 00000000, 00000000] |
代表前四个元素 A > B 为真,后面为假。
整数比较函数族
(_mm256_cmp* epi*)
整数没有第三个参数 op,而是根据函数名来区分操作类型。
✅ 常见函数(只有== 和 > 比较)
| 函数名 | 数据类型 | 含义 |
|---|---|---|
| _mm256_cmpeq_epi8 | 每个元素 8 位 | 相等比较 |
| _mm256_cmpeq_epi16 | 每个元素 16 位 | 相等比较 |
| _mm256_cmpeq_epi32 | 每个元素 32 位 | 相等比较 |
| _mm256_cmpeq_epi64 | 每个元素 64 位 | 相等比较 |
| _mm256_cmpgt_epi8 | 每个元素 8 位 | 大于比较 |
| _mm256_cmpgt_epi16 | 每个元素 16 位 | 大于比较 |
| _mm256_cmpgt_epi32 | 每个元素 32 位 | 大于比较 |
| _mm256_cmpgt_epi64 | 每个元素 64 位 | 大于比较 |
Exemple:
1 | __m256i a = _mm256_set_epi32(1, 2, 3, 4, 5, 6, 7, 8); |
输出(掩码):
1 | eq_mask = [FFFFFFFF, 00000000, FFFFFFFF, 00000000, FFFFFFFF, 00000000, FFFFFFFF, 00000000] |
即 a 与 b 在索引 0、2、4、6 相等。
- 比较两个数组是否相同
1 |
|
2.3 置换/重排 permute
permute 置换函数
在 SIMD(比如 AVX)中,重新排列寄存器中的元素顺序 “打乱 / 调整 向量里的元素顺序”
在 SIMD 中,很多算法(比如矩阵运算、排序、卷积、混洗)需要把向量的不同部分重排。
固定索引(imm8)
- 函数:_mm256_permute4x64_pd / _mm256_permute4x64_epi64
- 元素数:4(每个 64-bit double 或 int64)
- 控制方式:编译期常数 imm8(8 位),每两位控制一个输出元素取自 A 的哪个位置
- 用法示例:
1 | __m256d B = _mm256_permute4x64_pd(A, 0b01_00_11_10); |
- 优点:速度快,编译器可以优化
- 缺点:只能在编译期知道重排模式
1 | // 0b01_00_11_10 ,0b表示二进制 |
1 | imm8[7:6] = 01 -> B[3] = A[1] |
注意顺序是从低位 [1:0] 对应 B[0],高位 [7:6] 对应 B[3]。
可变索引(idx)
- 函数:_mm256_permutevar8x32_ps / _mm256_permutevar8x32_epi32
- 元素数:8(每个 32-bit float 或 int32)
- 控制方式:寄存器 idx,每个元素告诉输出该位置取 A 的哪个元素
- 用法示例:
1 | __m256i idx = _mm256_set_epi32(7,6,5,4,3,2,1,0); |
- 优点:运行期动态决定重排
- 缺点:比固定索引略慢
Exemple:
1 | // 假设有一个寄存器 A *注意 _mm256_set_ps 第一个参数是最高位 A7,最后一个是 A0。 |
2.4 Set
mm256_set*
这些函数用来 初始化 AVX 寄存器(__m256 或 __m256i),手动指定每个元素的值。
| 函数 | 寄存器类型 | 元素类型 | 元素个数 | 功能 |
|---|---|---|---|---|
| _mm256_set_ps(f0,…,f7) | __m256 | float32 | 8 | 设置 8 个 float 元素(f7 → 高位 A7,f0 → 低位 A0) |
| _mm256_set_pd(d0,…,d3) | __m256d | double64 | 4 | 设置 4 个 double 元素(d3 → 高位 A3,d0 → 低位 A0) |
| _mm256_set_epi64x(i0,…,i3) | __m256i | int64 | 4 | 设置 4 个 int64 元素(i3 → 高位 A3,i0 → 低位 A0) |
| _mm256_set_epi32(i0,…,i7) | __m256i | int32 | 8 | 设置 8 个 int32 元素 |
| _mm256_set_epi16(i0,…,i15) | __m256i | int16 | 16 | 设置 16 个 short 元素 |
| _mm256_set_epi8(i0,…,i31) | __m256i | int8 | 32 | 设置 32 个 char 元素 |
- 注意顺序:第一个参数对应 最高位元素,最后一个对应 最低位元素。
mm256_set1*
- 功能:把同一个值填充到寄存器所有元素
1 | __m256 v = _mm256_set1_ps(3.14f); // 所有 8 个 float 都是 3.14 |
2.5 其他指令
1 | // 基本 FMA 操作(乘加) |
1 | // hadd 水平加法 |
3 常见错误
📋 SIMD/AVX 错误类型总结表(双语版)
| 错误类型 | 典型表现 | 修正方法 | 英语错误描述 |
|---|---|---|---|
| 内存对齐Memory Alignment | 使用 _load_ps 但数据未对齐 |
使用 _loadu_ps 或对齐数据 |
Unaligned memory access - _mm256_load_ps requires 32-byte aligned memory, but the data is not properly aligned |
| **数据类型Data Type | __m256 用于 double,_ps 用于 double |
使用正确的类型和函数后缀 | Data type mismatch - Using __m256 (float) for double precision data, should use __m256d |
| 边界处理Boundary Handling | 循环步长导致数组越界 | 添加剩余元素处理逻辑 | Array out-of-bounds access - Loop stride causes reading beyond array boundaries, missing tail processing |
| **类型转换Type Conversion | 错误的指针强制转换 | 使用正确的 intrinsic 函数 | Incorrect pointer casting - Dangerous pointer casting instead of using proper intrinsic functions |
| **编译选项Compilation Flags | 编译时缺少 -mavx2 |
添加编译标志 | Missing compilation flag - AVX/AVX2 intrinsics require -mavx2 or -mavx compilation flag |
| **函数后缀Function Suffix | _ps 用于整数操作,_epi32 用于浮点数 |
使用正确的操作后缀 | Incorrect function suffix - Using _ps (packed single) for integer operations, should use _epi32 |
| **寄存器使用Register Usage | 混合使用不同宽度的寄存器 | 保持寄存器宽度一致 | Register width inconsistency - Mixing different register widths (e.g., __m128 with __m256) |
| 题目5 | 未初始化使用 | Uninitialized variable - using SIMD register without proper initialization |
|---|---|---|
LAB
LAB1
Ex1 HelloWorld
1 |
|
Ex2 计算数组和

1 |
|
Ex3 归并排序


1 |
|
Ex4 计算π
1 |
|
LAB2
Ex1 哥德巴赫猜想

1 |
|
Ex2 斐波那契数列
1 |
|

合理运用depend(in : ) depend(out : )
1 |
|
Ex3 计算矩阵向量乘法

1 |
|
Ex4 归并

1 |
|
LAB3
Ex1 复制数组
1 | /** |
Ex2 计算内积
1 | // 标量版本 |
1 | // AVX向量版本 |
1 | // AVX + FMA 版本 |
1 | // AVX + FMA 的展开版本 |
Ex3 反转一个数组
数组反转:将数组 [0, 1, 2, 3, 4, 5, 6, 7] 变成 [7, 6, 5, 4, 3, 2, 1, 0]
注意:考虑N > 16
1 | // 标量版本 |
1 | // AVX2版本 N是8的倍数且大于16 |
1 | // 完善dim小于16和不是8的整数 |
Ex4 计算矩阵乘向量 B = Ax
$$b[i] = \sum_{j=0}^{N-1} A[i][j] \times x[j]$$
Ann*Xn1=Bn1
1 | // 初始化 |
1 | // 标量版本 |
1 | // AVX2版本 |
LAB4 矩阵转置
1 |
|
LAB5 矩阵乘法

1 | CPU核心 |
1 | /* |





