解决应用程序瓶颈:微架构优化
立即解锁
发布时间: 2025-08-21 02:07:55 阅读量: 2 订阅数: 4 


高性能计算系统优化与应用设计
# 解决应用程序瓶颈:微架构优化
## 1. 报告过滤与向量优化概述
在进行程序性能分析时,生成的报告可能包含大量信息。为了便于查看,我们可以对报告进行过滤。例如,对于一个只有单个函数调用的短程序,报告级别为 5 时可能会有大约 200 行内容,而我们可能只对文件中的某个函数或报告的特定阶段感兴趣。这时可以使用过滤器,如指定函数名:
```plaintext
-opt-report-filter="squaredgemm"
```
或者指定报告阶段,如向量化阶段:
```plaintext
-opt-report-phase=vec
```
阶段参数必须是 CG、IPO、LOOP、OFFLOAD、OPENMP、PAR、PGO、TCOLLECT、VEC 或 all 中的一个。
### 1.1 向量优化方法
SIMD 向量化是提升英特尔 CPU 性能的主要方式之一,支持向量化的方法有:
- **自动向量化**:以编译器易于识别可向量化代码的方式编写代码。
- **用户辅助向量化**:向编译器指明向量化机会,甚至通过编译器指令注释强制编译器进行向量化。
- **语言扩展**:在高级语言中显式表达向量化。
其中,循环向量化最为常见,但并非唯一的向量化来源。
### 1.2 AVX 指令集
Sandy Bridge 架构引入了一组新的指令集——高级向量扩展(AVX),它操作 256 位向量寄存器,取代了奔腾处理器系列中引入的 128 位 SSE 指令集扩展,使 CPU 的浮点性能提升了一倍。
AVX 指令可以操作十六个 256 位向量寄存器 ymm0 - ymm15,与 SSE 不同,AVX 引入了三参数指令格式:
```plaintext
<instruction> <destination>, <source1>, <source2>
```
这种格式允许进行非破坏性操作,避免了为保存向量寄存器内容而进行的频繁保存操作,同时减少了寄存器压力。AVX 对于 256 位向量的功能包括:
- **数据加载和存储**:可进行对齐和非对齐数据的加载和存储,操作可以被掩码,避免加载到未分配内存范围时出错。
- **广播**:将一个内存元素复制到向量的所有元素中。
- **基本算术运算**:加法、减法、乘法、除法、加减混合运算、平方根、倒数等。
- **比较、取最小/最大值和舍入**。
- **元素置换**:包括车道内元素置换和车道置换。
## 2. 代码无法向量化的原因
### 2.1 数据依赖
在流水线执行中,数据依赖会阻止指令以流水线、乱序或超标量的方式并行执行,对于向量操作同样如此。向量依赖更为可控且易于解决,常见的数据冲突有:
- **流依赖(RAW)**:写后读,即一个变量在一次迭代中被写入,在后续迭代中被读取。例如:
```c
for(int i=0;i<length-1;i++){
a[i+1]=a[i];
}
```
展开该循环可得:
```plaintext
a[1]=a[0]; a[2]=a[1]; a[3]=a[2]; ...
```
正确执行循环后,所有元素应被设置为 a[0] 的值。但如果对该循环进行两元素向量化,会得到错误结果,因此编译器必须阻止该循环向量化。不过,编译器有时会假设存在未经证实的向量依赖,而实际上这种依赖可能并不存在。
我们可以通过以下练习来验证:
```c
// 原始循环
for(int i=0;i<length-1;i++){
a[i+1]=a[i];
}
// 强制向量化
#pragma simd
for(int i=0;i<length-1;i++){
a[i+1]=a[i];
}
```
尝试不同的偏移量(i + 1, i + 2, ...),观察哪些能得到正确结果,哪些会得到错误结果。
### 2.2 数据别名
代码无法向量化的另一个相关原因是别名问题,即两个变量(指针或引用)关联到同一内存区域。例如一个简单的复制函数:
```c
void mycopy(double* a, double* b, int length){
for(int i=0;i<length-1;i++){
a[i]=b[i];
}
}
```
理论上编译器可以轻松对其进行向量化,但编译器无法确定数组 a 和 b 是否重叠,因为 C/C++ 明确允许这种情况。以下是调用该函数的示例:
```c
int main(void){
int length=100;
int copylength=50;
double* a;
double* b;
double* data = (double*) malloc(sizeof(double)*length);
a=&data[1];
b=&data[0];
mycopy(a,b,copylength);
}
```
这种情况下,编译器必须假设存在依赖关系。
### 2.3 数组表示法(AN)
Intel Cilk Plus 引入的数组表示法(AN)是 C/C++ 的特定语言扩展,允许直接表达数据级并行性。AN 减轻了编译器对依赖和别名分析的负担,提供了一种编写正确、高效代码的简单方法。
AN 引入了数组段表示法,允许指定特定元素,格式为:
```plaintext
<array base>[<lower bound>:<length>[:<stride>]]
```
语法类似于 Fortran 语法,但语义要求是 start:length 而不是 start:end。示例如下:
```plaintext
a[:] // 整个数组
a[0:10] // 元素 0 到 9
a[0:5:2] // 元素 0, 2, 4, 6, 8
```
基于此表示法,运算符将按元素操作:
```c
c[0:10]=a[0:10]*b[0:10]; // 10 个元素的逐元素乘法
a[0:10]++; // 递增所有元素
m[0:10]=a[0:10]<b[0:10]; // 如果 a[i]<b[i],m[i] 为 1,否则为 0
```
AN 还支持高维字段和不同秩的操作,唯一要求是秩的数量和大小必须匹配。此外,AN 提供了归约内建函数,例如:
```plaintext
_sec_reduce_add(a[:]); // 返回数组所有元素的和
_sec_reduce_add(a[:]*b[:]); // 返回向量内积
```
以一维声波方程为例,在 C/C++ 中可以这样实现:
```c
for(int i=0;i<iterations;i++){ // 时间步数
for(int n=1;n<size-1;n++){ // 空间维度迭代
f_next[n]= prefac*(f_curr[n-1]+f_curr[n+1]
-2.0*f_curr[n])-f_prev[n]+2.0*f_curr[n];
}
tmp=f_prev;
f_prev=f_curr; // 迭代中,下一个场变为当前场
f_curr=f_next; // 当前场变为前一个场
f_next=tmp; // 旧的前一个场用于存储新的下一个场
}
```
使用 AN 表示则为:
```c
for(int i=0;i<iterations;i++){
f_next[1:size-2]=prefac*(f_curr[0:size-2]+f_curr[2:size-2]
-2.0*f_curr[1:size-2])-f_prev[1:size-2]+2.0*f_curr[1:size-2];
tmp=f_prev;
f_prev=f_curr;
f_curr=f_next;
f_next=tmp;
}
```
虽然编译器可能会对简单的 C/C++ 示例进行向量化,但对于更复杂的问题,如三维波动方程的高阶有限差分方程,AN 能使向量化代码更加明确。
## 3. 向量化指令
### 3.1 指令概述
在 C/C++ 或 Fortran 中,有些信息难以直接表达,这时可以使用编译指示(pragma)向编译器提供提示。编译指示如果编译器不识别,会被当作注释或未知预处理器指令处理,最终会被忽略,因此代码
0
0
复制全文
相关推荐








