Port Utilization

description:" "

背景

计算 \(itn\) 次 logistic 映射, 具有如下形式:

\[ x_{n+1} = r x_n (1 - x_n) \]

Baseline 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
double it(double r, double x, int64_t itn) {
for (int64_t i = 0; i < itn; i++) {
x = r * x * (1.0 - x);
}
return x;
}

void itv(double r, double* x, int64_t n, int64_t itn) {
for (int64_t i = 0; i < n; i++) {
x[i] = it(r, x[i], itn);
}
}

OpenMP + SIMD

简单的想法是将 itv 中的 for 循环并行化,每次 it 并行计算 8 个 x. 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__m512d r_vec;
__m512d one_vec = _mm512_set1_pd(1.0);
__m512d it_512(__m512d x, int64_t itn) {

for (int64_t i = 0; i < itn; i++) {
x = _mm512_mul_pd(_mm512_mul_pd(r_vec, x), _mm512_sub_pd(one_vec, x));
}

return x;
}

void itv(double* x, int64_t n, int64_t itn) {
#pragma omp parallel for
for (int64_t i = 0; i < n; i+=8) {
__m512d x_vec = it_512(_mm512_loadu_pd(&x[i]), itn);
_mm512_storeu_pd(&x[i], x_vec);
}

}

编译指令为:

1
g++ -O3 -fopenmp -avx512f 

Port Utilization

这个度量指标表示由于核心与除法器无关的问题而导致应用程序停滞的周期的比例。例如,附近指令之间存在大量数据依赖,或者一系列指令过度加载特定端口。下面子项代表, CPU 在所有执行端口上每个周期执行了微操作的周期的占比。

但是这样的优化还是不够,丢尽 Vtune 中发现 microarchitecture Exploration 指标有些可优化的地方,比如:Port Utilization 在 Cycle of 1 Port Utilized. 20240201142003 但感觉还是没看懂,但是大概是 pipline 的程度不够,因为每个线程的每次迭代只有一个 SIMD 指令再跑,并行化程度不够.

进一步

最终,就是将 itv 中的 for 循环展开,每次迭代并行计算 8 个 向量. 这样流水线至少可以有两个 SIMD 指令在跑,实测 BLOCK_SIZE = 16 性能差不多. 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define BLOCK_SIZE 64

void it(double* x, double r, int64_t itn) {

for (int64_t i = 0; i < itn; i++) {
for (int j = 0; j < BLOCK_SIZE; j++) {
x[j] = r * x[j] * (1.0 - x[j]);
}
}
}

void itv(double* x, double r, int64_t n, int64_t itn) {
#pragma omp parallel num_threads(16)
#pragma omp for
for (int64_t i = 0; i < n; i+=BLOCK_SIZE) {
it(&x[i], r, itn);
}

}
没有手写 SIMD 是因为编译器已经自动优化了. 如果想查看编译器的优化报告,编译时加上参数 -fopt-info-optimized 可查看优化报告.