清单 20. GET_HIGH_WORD 宏定义1
2
3
4
5
6
| #define GET_HIGH_WORD(i,d) \
do { \
ieee_double_shape_type gh_u; \
gh_u.value = (d); \
(i) = gh_u.parts.msw; \
} while (0)
|
在这个宏中,造成 LHS 停滞的罪魁祸首可能是将 float 的属性读取到了内部 value,然后将它读取到变量 i 的操作。POWER7 处理器没有将浮点寄存器的内容逐位转移到定点寄存器的原生指令。在 POWER 上实现该操作的方式是,使用一个存储操作将浮点寄存器中的 FP 数字存储到内存,然后将相同的内存位置加载到一个定点寄存器(通用)。由于内存访问比寄存器操作慢(即使在访问 L1 数据缓存时也如此),在存储过程中,CPU 会停滞,以便完成后续的加载。
注:文档 "POWER ISA 2.06 (POWER7)"包含更多的信息。
最常见的性能计数器事件触发中断,保留一个与执行指令接近的指令 PC 地址。这可能会导致不完全准确的程序集注释。为了缓解这种行为 POWER4,并在以后有一组有限的名称为 marked 的性能计数器。标记的指令在每个时间框架内将产生较少的事件;然而,PC 指令会是准确的,导致准确的程序集注释。标记的事件在 opcontrol -l 获得的 OProfile 计数器列表中带有 PM_MRK 前缀。
仔细检查分析,查看 PM_MRK_LSU_REJECT_LHS 计数器。PM_MRK_LSU_REJECT_LHS 和 PM_LSU_REJECT_LHS 这两个计数器监视相同的性能事件。不过,标记的计数器 (PM_MRK_LSU_REJECT_LHS) 在每个时间框架内会产生较少的事件,但带有更准确的程序集注释。
清单 21. PM_MRK_LSU_REJECT_LHS POWER7 事件的 perf record1
2
3
4
5
| $ perf record -C 0 -e rd082 taskset -c 0 ./hypot_bench_glibc
$ perf report
Events: 256K raw 0xd082
64.61% hypot_bench_gli libm-2.12.so [.] __ieee754_hypot
35.33% hypot_bench_gli libm-2.12.so [.] __GI___finite
|
这会生成 中的程序集注释。
清单 22. PM_MRK_LSU_REJECT_LHS POWER7 事件的 perf report1
2
3
4
5
6
7
8
9
10
11
| : 00000080fc38b730 <.__ieee754_hypot>:
[...]
1.23 : 80fc38b7a8: c9 a1 00 70 lfd f13,112(r1)
0.00 : 80fc38b7ac: f8 01 00 70 std r0,112(r1)
32.66 : 80fc38b7b0: c8 01 00 70 lfd f0,112(r1)
[...]
0.00 : 80fc38b954: f8 01 00 70 std r0,112(r1)
0.00 : 80fc38b958: e8 0b 00 00 ld r0,0(r11)
0.00 : 80fc38b95c: 79 00 00 0e rldimi r0,r8,32,0
61.72 : 80fc38b960: c9 61 00 70 lfd f11,112(r1
[...]
|
另一个符号显示大约有 35% 的生成事件有类似的行为,如
清单 23. perf report 的更多重点1
2
3
| : 00000080fc3a2610 <.__finitel>>
0.00 : 80fc3a2610: d8 21 ff f0 stfd f1,-16(r1)
100.00 : 80fc3a2614: e8 01 ff f0 ld r0,-16(r1)
|
根据这些信息,优化工作可能是,通过删除 FP 到 INT 的转换,消除这些停滞。POWER 处理器具有快速、高效的 Float-Point 执行单元,所以不需要使用 Fixed-Point 指令来执行这些计算。POWER 目前在 GLIBC 中使用的算法 (sysdeps/powerpc/fpu/e_hypot.c) 只使用 FP 操作就可以删除所有 LHS 停滞。结果是获得了更简单的算法,如 所示。
清单 24. PowerPC GLIBC hypot 源代码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
| double
__ieee754_hypot (double x, double y)
{
x = fabs (x);
y = fabs (y);
TEST_INF_NAN (x, y);
if (y > x)
{
double t = x;
x = y;
y = t;
}
if (y == 0.0 || (x / y) > two60)
{
return x + y;
}
if (x > two500)
{
x *= twoM600;
y *= twoM600;
return __ieee754_sqrt (x * x + y * y) / twoM600;
}
if (y < twoM500)
{
if (y <= pdnum)
{
x *= two1022;
y *= two1022;
return __ieee754_sqrt (x * x + y * y) / two1022;
}
else
{
x *= two600;
y *= two600;
return __ieee754_sqrt (x * x + y * y) / two600;
}
}
return __ieee754_sqrt (x * x + y * y);
}
|
TEST_INF_NAN 宏是更进一步的小优化,在开始更进一步的 FP 操作之前,它会测试某个数是 NaN 还是 INFINITY(这是由于针对 NaN 和 INFINITY 的操作可能会增加 FP 异常,而函数规范不允许这一点)。在 POWER7 上,isinf 和 isnan 函数调用被编译器优化为 FP 指令,并且不会产生额外的函数调用,而在较旧的处理器(POWER6 和更旧的版本)上,它将对各函数分别生成一个调用。优化基本上是相同的实现,但是内联的,以避免函数调用。
最后,比较这两种实现,执行下面的简单测试。在使用和不使用新算法的情况下重新编译 GLIBC,比较每个基准运行的总时间。默认 GLIBC 实现结果位于 中:
1
2
3
4
5
6
7
8
9
| $ /usr/bin/time ./hypot_bench_glibc
INF_CASE : elapsed time: 14:994339
NAN_CASE : elapsed time: 14:707085
TWO60_CASE : elapsed time: 12:983906
TWO500_CASE : elapsed time: 10:589746
TWOM500_CASE : elapsed time: 11:215079
NORMAL_CASE : elapsed time: 15:325237
79.80user 0.01system 1:19.81elapsed 99%CPU (0avgtext+0avgdata 151552maxresident)k
0inputs+0outputs (0major+48minor)pagefaults 0swaps
|
优化版本的结果如 中所示:
清单 26. Benchmark with optimized GLIBC hypot1
2
3
4
5
6
7
8
9
| $ /usr/bin/time ./hypot_bench_glibc
INF_CASE : elapsed time: 4:667043
NAN_CASE : elapsed time: 5:100940
TWO60_CASE : elapsed time: 6:245313
TWO500_CASE : elapsed time: 4:838627
TWOM500_CASE : elapsed time: 8:946053
NORMAL_CASE : elapsed time: 6:245218
36.03user 0.00system 0:36.04elapsed 99%CPU (0avgtext+0avgdata 163840maxresident)k
0inputs+0outputs (0major+50minor)pagefaults 0swaps
|
这最终将获得超过 100% 的性能提高,使基准时间减少一半。 |