原文
三个月前,我使用LDC
按配置文件优化(PGO)
测量了D编译器前端
代码,有7%
的性能提升.现在,部分PGO
工作于2016
年1
月20日合并到LDC
主分支中!
LDC
使用PGO
,类似Clang
使用PGO
这里,大部分内容也适合Clang
.
(PGO)
PGO
构建配置
文件可用不同
的方式获得,但(目前)LDC
仅支持指令分析.编译器在第一个
编译阶段中添加"指令
"代码.然后,使用要优化
代码的用例
的输入
,运行程序,并在退出
时,输出
配置文件数据到单独
文件中.编译器在第二个
编译阶段使用此配置文件数据文件来编译和优化
.
1,使用指令编译:ldc2 -fprofile-instr-generate=profile.raw yourprogram.d -of=instrumented_program
2,用常见工作
负载(要优化的工作
负载类型)运行instrumented_program
.这应该创建profile.raw
文件.
3,转换
原始配置文件为LDC
可用格式:ldc-profdata merge -output=profile.data profile.raw
4,使用配置
文件数据重新编译
程序:ldc2 -O3 -fprofile-instr-use=profile.data yourprogram.d -of=optimized_program
指令
会使程序
明显变慢,但不必
经常生成新的配置文件
.在重新
编译有更改
程序时,可重用
配置文件数据;LDC
会检测
控制流的变化
,并简单
忽略过时
的配置文件数据.
可在编译时
指定,配置文件输出文件名,如上所示(-fprofile-instr-generate=
).如果未指定文件名,则改用LLVM_PROFILE_FILE
环境变量.如果未定义LLVM_PROFILE_FILE
,则使用默认default.profraw
文件名.
一般,单次运行程序不能创建有统计
代表的配置文件.想使用不同的输入
来分析多趟
指令程序.每次运行程序,可设置LLVM_PROFILE_FILE
为不同文件名,但配置文件运行时还支持配置文件名中的替换
说明符来帮助你.分别用进程pid
和主机名
替换文件名中的%p
和%h
说明符.(%h
需要LLVM>=3.9
).
如果使用-fprofile-instr-generate=test.%p.raw
编译并运行
几次程序,最终会得到一组test.*.raw
配置文件数据文件.然后,可如常使用ldc-profdata
合并它们:
profdata merge -o test.profdata test.*.raw
ldc-profdata
工具(与构建ldc2
时使用的LLVM
版本匹配的llvm-profdata
的重命名版本)可来检查
配置文件数据
文件的内容.看看
如下:
extern(C)
void foo(int x) {
if (x > 0) {
if (x > 500) {
// ...
}
}
}
void main() {
foreach (i; 0 .. 1000) {
foo(i);
}
}
使用
ldc2 -fprofile-instr-generate=default.profraw -run test_foo.d
编译并运行它.然后,
ldc-profdata show -all-functions default.profraw
输出:
Counters:
foo:
Hash: 0x00000000000002cb
Counters: 3
Function count: 1000
_Dmain:
Hash: 0x0000000000000004
Counters: 2
Function count: 1
Functions shown: 2
Total functions: 2
Maximum function count: 1000
Maximum internal block count: 1000
配置
文件数据文件包含有每个函数
计数器和其他信息
的数据结构.在此,看到程序只有两个
指令函数:foo
和_Dmain
.
foo
函数包含3个执行计数器(每个if
语句,函数项加上计数器),该函数被调用1000
次(“函数计数
”).在-fprofile-instr-use
编译步骤中,LDC
使用"hash"
字段来检查是否可使用配置文件数据
.
LDC
可重用
旧版本程序获得的配置
文件数据.在源码稍有更改
时允许,重用
配置文件,在配置文件中存储
函数控制流的哈希
.在-fprofile-instr-use
编译步骤中,LDC
确保配置文件中的函数哈希
等于正在编译代码的哈希.
如果没有匹配项
,LDC
忽略该函数
的配置文件数据.哈希
并不完美:不同代码段
可能产生相同的哈希,因此对新代码
可应用旧版本
代码获得的配置文件
.如:
void foo(int x) {
if (x > 0) { // 1000x 真, 4x 假
// ...
}
}
foo
函数几乎总是用正x
调用,if
语句的配置文件数据是"是分支有1000
次,否分支有4次".如果代码现在从x>0
更改为x<0
,则函数
哈希不变(当前LDC
行为),且错误地使用
了旧的配置文件数据
.
void foo(int x) {
if (x < 0) { // 用旧数据:1000x真, 4x假.错了
// ...
}
}
哈希
可依赖更多代码
来区分>和<
式.目前,哈希主要是为了避免在配置
文件信息不足或过多
时出现编译错误,
如,只有if
语句的函数才放入
哈希中.哈希
不完善是设计
使然,因为某些更改
不太可能更改
旧配置文件数据的有效性
.考虑将代码从x>1000
更改为x>1001
.此时,旧的
配置文件数据可能仍足够好,丢弃它
,性能会更糟.
无需苦恼:PGO
不会改变
D的语义
,它只是个优化技术
.因此,如果配置
文件数据不再与修改
后的代码匹配
,则可能的最坏情况是程序性能下降
.因为你已在(正确
)测量有和没有PGO
的程序性能,所以可轻松知道配置
文件何时需要更新.
ldc.profile
模块包含对接配置
文件指令的函数
.唯一真正有用的函数是resetAll()
.其他ldc.profile
函数公开的函数
是非常低级的,且在编译器/LLVM
未来版本中可能不可用.resetAll()
重置所有性能分析
信息,并可从配置
文件中删除
程序启动行为:
import ldc.profile : resetAll;
void main()
{
initializeProgram();
resetAll();
operate();
}
这样,PGO
优化器,为了获得最佳操作()
性能,会降低initializeProgram()
的性能.
在函数级
分析实现,因此用新的(LDC_profile_instr,{true|false})
指示打开/关闭
指定函数的指令
非常简单.指示
与指示(inline,...)
有相同的语义.
void instrumented() {}
void not_instrumented() {
pragma(LDC_profile_instr, false);
}
pragma(LDC_profile_instr, false) {
void not_instrumented_2() {}
void instrumented2_override() {
pragma(LDC_profile_instr, true);
}
pragma(LDC_profile_instr, true):
void instrumented3() { }
void instrumented4() { }
}
pragma(LDC_profile_instr, false)
struct Strukt {
void not_instrumented() {}
void instrumented_method() {
pragma(LDC_profile_instr, true);
}
}
指令默认
为打开
状态.因此,对非常可选
指令,需要在每个
文件的开头放pragma(LDC_profile_instr,false):
.
但即便如此,也会指令导入
函数.如果有足够
的需求,则很容易实现默认关闭
指令的命令行开关
,因此仅指令
一组指定
函数就不会那么麻烦了.