2536c937e3
翻译自英文原版 maths-cs-ai-compendium,共 20 章全部完成。 第01章 向量 | 第02章 矩阵 | 第03章 微积分 第04章 统计学 | 第05章 概率论 | 第06章 机器学习 第07章 计算语言学 | 第08章 计算机视觉 | 第09章 音频与语音 第10章 多模态学习 | 第11章 自主系统 | 第12章 图神经网络 第13章 计算与操作系统 | 第14章 数据结构与算法 第15章 生产级软件工程 | 第16章 SIMD与GPU编程 第17章 AI推理 | 第18章 ML系统设计 第19章 应用人工智能 | 第20章 前沿人工智能 翻译说明: - 所有数学公式 $...$ / $$...$$、代码块、图片引用完整保留 - mkdocs.yml 配置中文导航 + language: zh - README.md 已翻译为中文(兼 docs/index.md) - docs/ 目录包含指向各章文件的 symlink - 约 29,000 行中文内容,排除 .cache/ 构建缓存
451 lines
18 KiB
Markdown
451 lines
18 KiB
Markdown
# x86与AVX
|
||
|
||
*x86处理器来自Intel和AMD,主导着大多数ML训练所在的数据中心服务器。本文涵盖x86 SIMD的演进、AVX/AVX2内联函数编程、AVX-512、用于矩阵操作的Intel AMX、内存对齐、性能陷阱以及性能分析——在全球最常见的服务器CPU上榨取最大性能的工具。*
|
||
|
||
- 如果你的训练在云虚拟机(AWS、GCP、Azure)上运行,它几乎肯定运行在x86上。即使是GPU密集训练也有CPU瓶颈:数据加载、预处理、梯度聚合和检查点保存都在CPU上运行。使用x86 SIMD优化这些环节可以有意义地减少端到端训练时间。
|
||
|
||
## x86 SIMD演进
|
||
|
||
- x86 SIMD经历了越来越宽的向量寄存器:
|
||
|
||
| 代次 | 年份 | 寄存器宽度 | 寄存器数量 | 关键特性 |
|
||
|------|------|---------|----------|----------|
|
||
| MMX | 1997 | 64位 | 8(mm0-7) | 仅整数,与FPU共享 |
|
||
| SSE | 1999 | 128位 | 8(xmm0-7) | 4个浮点数,专用寄存器 |
|
||
| SSE2 | 2001 | 128位 | 8/16 | 2个双精度浮点数,整数操作 |
|
||
| AVX | 2011 | 256位 | 16(ymm0-15) | 8个浮点数,三操作数指令 |
|
||
| AVX2 | 2013 | 256位 | 16 | 整数256位,FMA,收集 |
|
||
| AVX-512 | 2017 | 512位 | 32(zmm0-31) | 16个浮点数,掩码寄存器,分散 |
|
||
| AMX | 2023 | 瓦片寄存器 | 8个瓦片 | 矩阵乘法(BF16,INT8) |
|
||
|
||
- 每一代都将向量化代码的吞吐量翻倍。用SSE内联函数编写的代码可以在2001年以来制造的每一个x86 CPU上运行。AVX2需要2013年以后的CPU。AVX-512需要Intel Xeon和一些消费级芯片。AMX是最新的(Sapphire Rapids及以后)。
|
||
|
||
- **向后兼容性**:x86 SSE寄存器(xmm)是AVX寄存器(ymm)的低128位,后者是AVX-512寄存器(zmm)的低256位。旧的SSE代码无需修改即可在新的CPU上运行。
|
||
|
||
## AVX2编程
|
||
|
||
- AVX2操作256位寄存器(YMM),同时处理8个浮点数或4个双精度浮点数。它是可移植高性能代码的甜点区域:在几乎所有现代x86 CPU(2013+)上可用。
|
||
|
||
### 内联函数命名约定
|
||
|
||
- 所有x86内联函数遵循模式:`_mm[宽度]_[操作]_[类型]`
|
||
|
||
- `_mm` = MMX/SSE(128位),`_mm256` = AVX(256位),`_mm512` = AVX-512(512位)
|
||
- 操作:`add`、`mul`、`fmadd`、`load`、`store`、`set` 等
|
||
- 类型:`ps` = 打包单精度(float32),`pd` = 打包双精度(float64),`epi32` = 打包int32,`si256` = 256位整数
|
||
|
||
```cpp
|
||
#include <immintrin.h> // 所有x86 SIMD内联函数
|
||
|
||
// 数据类型
|
||
__m256 a; // 256位寄存器,保存8个float32
|
||
__m256d b; // 256位寄存器,保存4个float64
|
||
__m256i c; // 256位寄存器,保存整数(8x32、16x16或32x8)
|
||
```
|
||
|
||
### 加载和存储数据
|
||
|
||
```cpp
|
||
// 从内存加载8个浮点数
|
||
__m256 v = _mm256_loadu_ps(ptr); // 非对齐加载(适用于任何地址)
|
||
__m256 v = _mm256_load_ps(ptr); // 对齐加载(ptr必须32字节对齐,更快)
|
||
|
||
// 存储8个浮点数到内存
|
||
_mm256_storeu_ps(out_ptr, v); // 非对齐存储
|
||
_mm256_store_ps(out_ptr, v); // 对齐存储
|
||
|
||
// 将单个值广播到所有8个通道
|
||
__m256 ones = _mm256_set1_ps(1.0f); // [1, 1, 1, 1, 1, 1, 1, 1]
|
||
|
||
// 设置各个值(很少需要)
|
||
__m256 v = _mm256_set_ps(7,6,5,4,3,2,1,0); // 注意:逆序!
|
||
|
||
// 零寄存器
|
||
__m256 z = _mm256_setzero_ps();
|
||
```
|
||
|
||
### 算术运算
|
||
|
||
```cpp
|
||
__m256 c = _mm256_add_ps(a, b); // c[i] = a[i] + b[i]
|
||
__m256 d = _mm256_mul_ps(a, b); // d[i] = a[i] * b[i]
|
||
__m256 e = _mm256_sub_ps(a, b); // e[i] = a[i] - b[i]
|
||
__m256 f = _mm256_div_ps(a, b); // f[i] = a[i] / b[i](比乘法慢)
|
||
|
||
// 融合乘加:r = a * b + c(一条指令,一次舍入)
|
||
__m256 r = _mm256_fmadd_ps(a, b, c); // ML最重要的指令
|
||
|
||
// 最小值和最大值
|
||
__m256 mn = _mm256_min_ps(a, b); // min(a[i], b[i]) — 用于裁剪
|
||
__m256 mx = _mm256_max_ps(a, b); // max(a[i], b[i]) — 用于ReLU
|
||
```
|
||
|
||
### 实践示例:AVX2点积
|
||
|
||
```cpp
|
||
#include <immintrin.h>
|
||
|
||
float dot_avx2(const float* a, const float* b, int n) {
|
||
__m256 sum = _mm256_setzero_ps(); // 8个累加器初始化为0
|
||
|
||
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
|
||
}
|
||
|
||
// 水平归约:将sum的8个元素相加
|
||
// 步骤1:将上128位加到下128位
|
||
__m128 hi = _mm256_extractf128_ps(sum, 1);
|
||
__m128 lo = _mm256_castps256_ps128(sum);
|
||
__m128 sum128 = _mm_add_ps(hi, lo); // 4个部分和
|
||
|
||
// 步骤2:在128位寄存器内水平相加
|
||
sum128 = _mm_hadd_ps(sum128, sum128); // [a+b, c+d, a+b, c+d]
|
||
sum128 = _mm_hadd_ps(sum128, sum128); // [a+b+c+d, ...]
|
||
|
||
float result = _mm_cvtss_f32(sum128); // 提取标量
|
||
|
||
// 标量清理
|
||
for (; i < n; i++) {
|
||
result += a[i] * b[i];
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
- **为什么水平归约如此丑陋**:SIMD是为垂直操作设计的(通道0与通道0,通道1与通道1)。水平操作(跨通道求和)与硬件对抗。这就是点积在末尾有尴尬归约代码的原因。向量化循环是简洁的;归约是样板代码。
|
||
|
||
- **性能**:与NEON版本(文件02)相比,AVX2每次迭代处理8个浮点数,而NEON处理4个。对于长向量,这比NEON快2倍(忽略内存带宽限制)。
|
||
|
||
### 实践示例:AVX2 Softmax(简化版)
|
||
|
||
- Softmax需要:找到最大值,减去它,求指数,求和,除法。以下是最值查找步骤:
|
||
|
||
```cpp
|
||
float vector_max_avx2(const float* data, int n) {
|
||
__m256 max_vec = _mm256_set1_ps(-INFINITY);
|
||
|
||
int i = 0;
|
||
for (; i + 8 <= n; i += 8) {
|
||
__m256 v = _mm256_loadu_ps(data + i);
|
||
max_vec = _mm256_max_ps(max_vec, v);
|
||
}
|
||
|
||
// 将8个最大值归约为1个
|
||
__m128 hi = _mm256_extractf128_ps(max_vec, 1);
|
||
__m128 lo = _mm256_castps256_ps128(max_vec);
|
||
__m128 max128 = _mm_max_ps(hi, lo);
|
||
|
||
// 通过混洗和取最大值找到单一最大值
|
||
max128 = _mm_max_ps(max128, _mm_shuffle_ps(max128, max128, 0b01001110));
|
||
max128 = _mm_max_ps(max128, _mm_shuffle_ps(max128, max128, 0b10110001));
|
||
|
||
float result = _mm_cvtss_f32(max128);
|
||
|
||
for (; i < n; i++) {
|
||
result = result > data[i] ? result : data[i];
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
- `_mm_shuffle_ps` 指令在寄存器内重排元素。二进制常量 `0b01001110` 控制哪些元素去哪里。这称为**置换**,它直接连接到置换矩阵(第2章):打乱SIMD通道是做硬件级别的乘以置换矩阵。
|
||
|
||
## AVX-512
|
||
|
||
- AVX-512再次加倍宽度:512位寄存器(ZMM),同时处理16个浮点数。
|
||
|
||
```cpp
|
||
__m512 a = _mm512_loadu_ps(ptr); // 加载16个浮点数
|
||
__m512 c = _mm512_fmadd_ps(a, b, c); // 16个FMA同时进行
|
||
float sum = _mm512_reduce_add_ps(a); // 内置水平求和(无需手动归约!)
|
||
|
||
// 掩码操作:操作通道子集
|
||
__mmask16 mask = _mm512_cmpgt_ps_mask(a, zero); // 哪些通道 > 0?
|
||
__m512 relu = _mm512_maskz_mov_ps(mask, a); // 负通道置零 = ReLU
|
||
```
|
||
|
||
- **掩码寄存器**(`__mmask16`)是AVX-512最强大的功能。每个位控制一个通道是否参与操作。这取代了标量清理循环:最后一次迭代使用掩码,只有有效通道是激活的,处理任何向量长度而无需单独标量循环。
|
||
|
||
- **AVX-512频率降频**:在许多Intel CPU上,使用AVX-512指令会导致CPU暂时降低时钟频率(以保持在热限制内)。这意味着对于短时爆发,AVX-512并不总是比AVX2快——频率惩罚可能抵消更宽向量的优势。对于持续工作负载(如矩阵乘法),AVX-512胜出。对于混合代码(部分SIMD、部分标量),频率转换可能造成损失。
|
||
|
||
## Intel AMX:矩阵乘法硬件
|
||
|
||
- **AMX**(高级矩阵扩展)增加了专用矩阵乘法单元。AMX操作的不是SIMD向量,而是**瓦片**:2D数据块(最多16行 × 每行64字节)。
|
||
|
||
```cpp
|
||
#include <immintrin.h>
|
||
|
||
// AMX瓦片乘法:C += A * B(BF16格式)
|
||
// A为16x32 BF16,B为32x16 BF16,C为16x16 FP32
|
||
_tile_loadd(0, a_ptr, stride_a); // 从A加载瓦片0
|
||
_tile_loadd(1, b_ptr, stride_b); // 从B加载瓦片1
|
||
_tile_dpbf16ps(2, 0, 1); // 瓦片2 += 瓦片0 * 瓦片1(BF16矩阵乘法,FP32累加)
|
||
_tile_stored(2, c_ptr, stride_c); // 存储瓦片2到C
|
||
```
|
||
|
||
- AMX在一条指令中执行完整的16×32 × 32×16矩阵乘法。这是数百次FMA操作同时进行,专门为Transformer推理中主导的小矩阵乘法设计(注意力得分计算、MLP层)。
|
||
|
||
- AMX支持BF16(bfloat16)和INT8,匹配ML推理中使用的精度。结合用于其他操作的AVX-512,配备AMX的CPU(Intel Sapphire Rapids、Emerald Rapids)可以在Transformer推理中与入门级GPU竞争。
|
||
|
||
## 内存对齐
|
||
|
||
- **对齐内存访问**是指数据地址是向量寄存器宽度的倍数(SSE为16字节、AVX为32字节、AVX-512为64字节)。对齐访问在某些CPU上更快,并且是 `_mm256_load_ps`(相对于 `_mm256_loadu_ps`)的要求。
|
||
|
||
```cpp
|
||
// 分配对齐内存
|
||
float* data = (float*)aligned_alloc(32, n * sizeof(float)); // AVX的32字节对齐
|
||
|
||
// C++对齐分配
|
||
#include <new>
|
||
float* data = new (std::align_val_t(32)) float[n];
|
||
|
||
// 或者使用编译器属性
|
||
alignas(32) float data[1024];
|
||
```
|
||
|
||
- **实际上**:在现代CPU(Haswell及以后)上,当数据不跨越缓存行边界时,非对齐加载(`loadu`)几乎与对齐加载一样快。非对齐访问的性能惩罚已基本消失,但缓存行分割(数据跨越两个64字节缓存行)仍可能使特定加载变慢约2倍。对齐分配完全避免了这种情况。
|
||
|
||
## 性能陷阱
|
||
|
||
- **AVX-SSE转换惩罚**:在较旧的Intel CPU(Skylake之前)上,在AVX(256位)和SSE(128位)指令之间切换会造成惩罚(约70周期)。这就是为什么你应该在从使用AVX的函数返回之前使用 `_mm256_zeroupper()`(或 `vzeroupper` 指令)清除YMM寄存器的上128位。现代CPU(Skylake+)没有此惩罚。
|
||
|
||
- **寄存器压力**:AVX2有16个YMM寄存器。如果你的核函数使用太多变量,编译器会将寄存器溢出到栈(内存),从而破坏性能。保持内循环简单,活变量少。
|
||
|
||
- **数据依赖**:`sum = _mm256_fmadd_ps(a, b, sum)` 对 `sum` 有依赖:每次迭代必须等待前一个FMA完成(约4-5个周期的延迟)。解决方案:使用多个独立累加器并在结束时归约:
|
||
|
||
```cpp
|
||
// 单累加器:受FMA延迟限制(4-5个周期)
|
||
__m256 sum = _mm256_setzero_ps();
|
||
for (...) {
|
||
sum = _mm256_fmadd_ps(a, b, sum); // 每个依赖前一个
|
||
}
|
||
|
||
// 四个累加器:4倍吞吐量(隐藏延迟)
|
||
__m256 sum0 = _mm256_setzero_ps();
|
||
__m256 sum1 = _mm256_setzero_ps();
|
||
__m256 sum2 = _mm256_setzero_ps();
|
||
__m256 sum3 = _mm256_setzero_ps();
|
||
for (...) {
|
||
sum0 = _mm256_fmadd_ps(a0, b0, sum0); // 独立
|
||
sum1 = _mm256_fmadd_ps(a1, b1, sum1); // 独立
|
||
sum2 = _mm256_fmadd_ps(a2, b2, sum2); // 独立
|
||
sum3 = _mm256_fmadd_ps(a3, b3, sum3); // 独立
|
||
}
|
||
sum0 = _mm256_add_ps(sum0, sum1);
|
||
sum2 = _mm256_add_ps(sum2, sum3);
|
||
sum0 = _mm256_add_ps(sum0, sum2);
|
||
```
|
||
|
||
- 这是**循环展开**以隐藏延迟。CPU可以背靠背发出FMAs,因为它们写入不同的寄存器。这是数值代码中最有影响力的微优化之一。
|
||
|
||
## 性能分析
|
||
|
||
- **性能计数器**提供硬件级测量:
|
||
|
||
```bash
|
||
# Linux perf(需要内核支持)
|
||
perf stat ./my_program # 基本计数器:周期、指令、IPC
|
||
perf stat -e cache-misses,cache-references ./my_program # 缓存行为
|
||
perf record -g ./my_program && perf report # 调用图分析
|
||
|
||
# Intel VTune(详细的x86性能分析)
|
||
vtune -collect hotspots -- ./my_program
|
||
vtune -collect memory-access -- ./my_program # 内存带宽分析
|
||
```
|
||
|
||
- **需要关注什么**:
|
||
- **IPC**(每周期指令数):CPU被使用的效率。IPC > 2 良好。IPC < 1 表明内存停顿或分支预测错误。
|
||
- **缓存缺失率**:高L1/L2缺失率表明数据局部性差。需重构数据访问模式。
|
||
- **分支预测错误率**:> 5% 表明分支不可预测。如可能,转换为无分支代码(SIMD比较+混合)。
|
||
- **实际FLOPS vs 屋顶线**:将你的实测FLOPS与屋顶线模型(文件01)比较。如果你低于屋顶线,还有改进空间。
|
||
|
||
## 编程任务(在x86——Intel/AMD上用g++或clang++编译)
|
||
|
||
1. 编写标量点积和AVX2点积。对两者进行基准测试并测量8路SIMD带来的加速比。
|
||
```cpp
|
||
// task1_avx_dot.cpp
|
||
// 编译:g++ -O3 -mavx2 -mfma -o task1 task1_avx_dot.cpp
|
||
|
||
#include <iostream>
|
||
#include <chrono>
|
||
#include <vector>
|
||
#include <immintrin.h>
|
||
|
||
float dot_scalar(const float* a, const float* b, int n) {
|
||
float sum = 0.0f;
|
||
for (int i = 0; i < n; i++) sum += a[i] * b[i];
|
||
return sum;
|
||
}
|
||
|
||
float dot_avx2(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);
|
||
}
|
||
// 归约:上128加到下128,然后水平相加
|
||
__m128 hi = _mm256_extractf128_ps(sum, 1);
|
||
__m128 lo = _mm256_castps256_ps128(sum);
|
||
__m128 r = _mm_add_ps(hi, lo);
|
||
r = _mm_hadd_ps(r, r);
|
||
r = _mm_hadd_ps(r, r);
|
||
float result = _mm_cvtss_f32(r);
|
||
for (; i < n; i++) result += a[i] * b[i];
|
||
return result;
|
||
}
|
||
|
||
int main() {
|
||
const int N = 10'000'000;
|
||
std::vector<float> a(N, 1.0f), b(N, 2.0f);
|
||
|
||
volatile float s1 = dot_scalar(a.data(), b.data(), N);
|
||
volatile float s2 = dot_avx2(a.data(), b.data(), N);
|
||
|
||
auto bench = [&](auto fn, const char* name) {
|
||
auto start = std::chrono::high_resolution_clock::now();
|
||
volatile float s;
|
||
for (int t = 0; t < 100; t++) s = fn(a.data(), b.data(), N);
|
||
auto end = std::chrono::high_resolution_clock::now();
|
||
double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
|
||
std::cout << name << ": " << ms << " ms(结果: " << s << ")\n";
|
||
return ms;
|
||
};
|
||
|
||
double t1 = bench(dot_scalar, "标量");
|
||
double t2 = bench(dot_avx2, "AVX2 ");
|
||
std::cout << "加速比: " << t1 / t2 << "x\n";
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
2. 使用 `_mm256_max_ps` 实现AVX2 ReLU并与标量循环比较。然后尝试使用多累加器(循环展开)以隐藏FMA延迟。
|
||
```cpp
|
||
// task2_avx_relu.cpp
|
||
// 编译:g++ -O3 -mavx2 -o task2 task2_avx_relu.cpp
|
||
|
||
#include <iostream>
|
||
#include <chrono>
|
||
#include <vector>
|
||
#include <immintrin.h>
|
||
|
||
void relu_scalar(const float* in, float* out, int n) {
|
||
for (int i = 0; i < n; i++) {
|
||
out[i] = in[i] > 0.0f ? in[i] : 0.0f;
|
||
}
|
||
}
|
||
|
||
void relu_avx2(const float* in, float* out, int n) {
|
||
__m256 zero = _mm256_setzero_ps();
|
||
int i = 0;
|
||
for (; i + 8 <= n; i += 8) {
|
||
__m256 x = _mm256_loadu_ps(in + i);
|
||
_mm256_storeu_ps(out + i, _mm256_max_ps(x, zero));
|
||
}
|
||
for (; i < n; i++) out[i] = in[i] > 0.0f ? in[i] : 0.0f;
|
||
}
|
||
|
||
// 展开:每次迭代处理32个浮点数(4 x 8)
|
||
void relu_avx2_unrolled(const float* in, float* out, int n) {
|
||
__m256 zero = _mm256_setzero_ps();
|
||
int i = 0;
|
||
for (; i + 32 <= n; i += 32) {
|
||
__m256 x0 = _mm256_loadu_ps(in + i);
|
||
__m256 x1 = _mm256_loadu_ps(in + i + 8);
|
||
__m256 x2 = _mm256_loadu_ps(in + i + 16);
|
||
__m256 x3 = _mm256_loadu_ps(in + i + 24);
|
||
_mm256_storeu_ps(out + i, _mm256_max_ps(x0, zero));
|
||
_mm256_storeu_ps(out + i + 8, _mm256_max_ps(x1, zero));
|
||
_mm256_storeu_ps(out + i + 16, _mm256_max_ps(x2, zero));
|
||
_mm256_storeu_ps(out + i + 24, _mm256_max_ps(x3, zero));
|
||
}
|
||
for (; i + 8 <= n; i += 8) {
|
||
_mm256_storeu_ps(out + i, _mm256_max_ps(_mm256_loadu_ps(in + i), zero));
|
||
}
|
||
for (; i < n; i++) out[i] = in[i] > 0.0f ? in[i] : 0.0f;
|
||
}
|
||
|
||
int main() {
|
||
const int N = 16'000'000;
|
||
std::vector<float> in(N), out(N);
|
||
for (int i = 0; i < N; i++) in[i] = (float)(i % 200) - 100.0f;
|
||
|
||
auto bench = [&](auto fn, const char* name) {
|
||
fn(in.data(), out.data(), N); // 预热
|
||
auto start = std::chrono::high_resolution_clock::now();
|
||
for (int t = 0; t < 100; t++) fn(in.data(), out.data(), N);
|
||
auto end = std::chrono::high_resolution_clock::now();
|
||
double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
|
||
double bw = 2.0 * N * sizeof(float) / ms / 1e6; // 读取+写入
|
||
std::cout << name << ": " << ms << " ms(" << bw << " GB/s)\n";
|
||
};
|
||
|
||
bench(relu_scalar, "标量 ");
|
||
bench(relu_avx2, "AVX2 ");
|
||
bench(relu_avx2_unrolled, "AVX2展开 ");
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
3. 测量内存对齐的效果。比较在大数组上的对齐加载与非对齐加载。
|
||
```cpp
|
||
// task3_alignment.cpp
|
||
// 编译:g++ -O3 -mavx2 -o task3 task3_alignment.cpp
|
||
|
||
#include <iostream>
|
||
#include <chrono>
|
||
#include <cstdlib>
|
||
#include <immintrin.h>
|
||
|
||
int main() {
|
||
const int N = 16'000'000;
|
||
|
||
// 对齐分配(AVX2为32字节)
|
||
float* aligned = (float*)aligned_alloc(32, N * sizeof(float));
|
||
|
||
// 非对齐:从对齐边界偏移4字节(1个浮点数)
|
||
float* raw = (float*)malloc((N + 1) * sizeof(float));
|
||
float* unaligned = raw + 1; // 保证未对齐
|
||
|
||
for (int i = 0; i < N; i++) {
|
||
aligned[i] = 1.0f;
|
||
unaligned[i] = 1.0f;
|
||
}
|
||
|
||
auto bench = [&](float* ptr, bool use_aligned, const char* name) {
|
||
__m256 sum = _mm256_setzero_ps();
|
||
// 预热
|
||
for (int i = 0; i + 8 <= N; i += 8) {
|
||
__m256 v = use_aligned ? _mm256_load_ps(ptr + i) : _mm256_loadu_ps(ptr + i);
|
||
sum = _mm256_add_ps(sum, v);
|
||
}
|
||
|
||
auto start = std::chrono::high_resolution_clock::now();
|
||
for (int t = 0; t < 100; t++) {
|
||
sum = _mm256_setzero_ps();
|
||
for (int i = 0; i + 8 <= N; i += 8) {
|
||
__m256 v = use_aligned ? _mm256_load_ps(ptr + i) : _mm256_loadu_ps(ptr + i);
|
||
sum = _mm256_add_ps(sum, v);
|
||
}
|
||
}
|
||
auto end = std::chrono::high_resolution_clock::now();
|
||
double ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
|
||
double bw = (double)N * sizeof(float) / ms / 1e6;
|
||
std::cout << name << ": " << ms << " ms(" << bw << " GB/s)\n";
|
||
};
|
||
|
||
bench(aligned, true, "对齐加载 ");
|
||
bench(unaligned, false, "非对齐加载");
|
||
|
||
free(aligned);
|
||
free(raw);
|
||
return 0;
|
||
}
|
||
```
|