du_pl">
<d iv id ="article_content" class="article_content clearfix">
<d iv id ="content_views" class="markd own_views prism-atom-one-light">
display: none;">
d" d ="M5,0 0,2.5 5,5z" id ="raphael-marker-block" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0);">
原文
第1部分介绍de>ld c de>中的按配置文件优化 de>(PGO) de>.一篇关于通过转换de>间接调用 de>为de>直接 de>调用 来,使用de>配置文件 de>数据de>优化 de>虚(类)调用 函数,并介绍了如何使用de>llvm de>在de>LDC de>中实现.ld c介绍,llvm地址
用de>PGO de>编译时,de>LDC de>的D代码(de>测试用例 de>上)速度提高了de>7% de>.
d ="PGO_7">按配置优化 de>(PGO) de>
本文中,讨论给定de>程序典型执行流 de>细节时,编译器可de>执行 de>优化 的实现.这些细节是de>程序 de>的"de>配置文件 de>":调用 de>函数 de>次数,de>控制流 de>分支的de>频率 de>,de>B函数 de>调用 de>A函数 de>的频率,de>X变量 de>的可能值,一般de>分配 de>多少内存等.
有不同de>方式 de>取配置文件,在此只讨论de>指令分析 de>.编译器在第一个de>编译 de>阶段中添加"de>指令 de>"代码.然后,使用要de>优化 de>代码的de>典型用例 de>的输入de>运行 de>程序,并在退出时,在单独的文件中输出de>配置 de>文件数据.
编译器在de>第二个 de>编译阶段使用此de>配置文件 de>数据文件来编译并优化 .不必经常生成de>新的配置文件 de>.有de>小更改 de>时,de>重新 de>编译时,可重用de>配置 de>文件数据;de>LDC de>控制指令流的变化,并de>相应 de>地忽略de>配置 de>文件数据.
使用de>PGO de>的de>简单 de>构建过程如下.
de class="prism language-cpp">ld c2 fprofileinstrgenerate= profile. raw yourprogram. d of= instrumented _program
. / instrumented _program
ld cprofd ata merge output= profile. d ata profile. raw
ld c2 fprofileinstruse= profile. d ata yourprogram. d of= optimized _program
de>
de>指令 de>会使程序明显变慢,使得对de>行为 de>依赖特定de>计时 de>或与de>其他 de>系统交互的程序,不适合de>指令 de>的de>PGO de>.
d ="_25">直接和间接调用
de>直接 de>调用 是对de>地址 de>已知,并已在de>机器 de>指令代码中de>编码 de>函数的调用 .de>间接 de>调用 是对存储在de>内存或寄存器 de>中的de>某个 de>地址的de>调用 de>.
程序早期在变量中de>存储 de>函数的地址,de>间接 de>调用 调用 变量指向de>函数 de>.在de>许多 de>程序中使用de>间接调用 de>:允许de>多态 de>对象,绑定de>动态库 de>,选择de>硬件 de>中断例程等.在D中,调用 de>类方法 de>时可能一直使用它们.
de>间接 de>调用 比de>直接 de>调用 "慢"的原因是de>额外 de>的间接性:de>CPU de>必须先de>读取 de>一段内存,然后才能de>跳转 de>到de>函数 de>以继续执行. 现代de>CPU de>,一般de>间接 de>调用 与de>直接 de>调用 一样快.但是可在编译时内联de>直接调用 de>,并允许de>分析 de>被调函数内部情况及返回值. 可de>帮助 de>生成更快的可执行文件.目前de>llvm de>开发者正在添加"提升de>间接调用 de>"趟来使用de>分析信息 de>,转换间接de>调用 de>为de>分支+直接调用 de>,当然de>LDC de>可利用它.
d ="ICP_35">提升间接调用 de>(ICP) de>
基于配置文件的提升间接调用 .de>ICP de>优化 ,是转换如下代码片
de class="prism language-cpp">
d">void function ( ) fptr = get_function_ptr ( ) ;
fptr ( ) ;
d">void function ( ) fptr = get_function_ptr ( ) ;
d">if ( fptr == & likely_function)
likely_function ( ) ;
d">else
fptr ( ) ;
de>
注意,可de>手动 de>转换代码为de>函数指针比较+直接调用 de>,则无需de>分析 de>,就得到相同de>机器码 de>.如果对de>函数指针 de>内容好奇,可如下重写de>代码 de>,来提高性能.使用模板函数de>助手 de>,很容易完成:
de class="prism language-cpp">d">auto is_likely ( alias Likely, Fptr, Args. . . ) ( Fptr fptr, Args args) {
d">return ( fptr== & Likely) ? Likely ( args) : fptr ( args) ;
}
d">void function ( ) fptr = get_function_ptr ( ) ;
fptr. is_likely! likely_function ( ) ;
de>
建议看下生成的de>llvmIR de>(de>ld c2 outputll ... de>),并看看de>(O3) de>上优化 与内联de>likely_function de>函数有什么区别.
d ="_66">分析间接调用
在de>添加 de>指令的编译趟(de>ld c2 -fprofile-instr-generate -fprofile-ind irect-calls) de>中,此代码:
de class="prism language-cpp">d">void somefunction ( ) {
fptr ( ) ;
}
de>
转换为(有些简化):
de class="prism language-cpp">d">void somefunction ( ) {
increment_counter ( & __profc_somefunction, 0 ) ;
__llvm_profile_instrument_target ( fptr, & __profd _somefunction, 0 ) ;
fptr ( ) ;
}
de>
de>指令 de>基于每个函数工作:de>每个 de>指令函数得到de>执行 de>时存储de>分析信息 de>的一组数据结构. de>llvm de>检测到de>llvm de>分析内部调用 函数时,自动添加它们.de>__profc_... de>数据结构是de>存储 de>执行计数的数组;de>increment_counter de>的de>第二个 de>参数是de>计数器索引 de>.
本文其余部分,会排除de>执行计数器 de>;退出de>指令 de>程序之前,链入的de>分析 de>运行时库会,在de>分析 de>文件中(按原始格式)de>转储 de>这些de>指令数据结构 de>.
de>__profd _... de>最受关注:它存储de>"值" de>配置文件数据.此时,该值是存储在de>fptr de>中的de>原语指针值 de>.de>__llvm_profile_instrument_target de>按de>IPVK_Ind irectCalltarget de>值类型发送de>fptr de>值到de>0 de>值分析槽.
然后,de>llvm de>分析机制取de>该值 de>,并最终向编译器提供de>值 de>列表及每个de>值分析槽 de>中这些值出现的de>频率 de>.这是由编译器rt的分析运行时库,及配置文件处理工具de>ld c-profd ata de>这里和de>llvm::InstrProfRead er de>类这里完成的.但是,每次程序de>运行 de>可能有不同de>函数地址 de>值时,如何使用函数地址值?
de>__profd _... de>数据结构不仅包含de>值 de>配置文件数据信息.还包含de>函数名 de>的引用(de>哈希 de>)及de>函数地址 de>! 如果可在de>__profd _ de>结构中找到de>指针值 de>,de>ld c-profd ata de>读取所有de>__profd _ de>数据结构,并(对de>IPVK_Ind irectCalltarget de>值类型)de>转换 de>地址值为de>函数名哈希 de>.
此机制决定了de>ICP de>仅适合,在de>__profd _ de>结构中存储de>地址+哈希 de>的函数de>调用 目标 de>. 目前,对de>X函数 de>,让de>llvm de>生成(正确)de>__profd _ de>结构的de>唯一 de>方法是,在de>X de>函数中添加de>llvm de>分析内部调用 函数.如果函数指针大量指向de>printf de>很多,de>ICP de>就没法了.
以下是de>LDC de>自身用de>ld c-profd ata show de>输出的de>配置文件 de>的片段,显示了de>extern(C++) de>函数de>checkAccess(AggregateDeclaration*,Loc,域*,Dsymbol*) de>的配置文件数据:
de class="prism language-cpp">_Z11checkAccessP20AggregateDeclaration3LocP5域P7Dsymbol:
< snip>
间接调用 点数: 3
间接目标结果:
[ < 调用 点> , < 符号名> , < 计数> ]
[ 0 , _ZN20AggregateDeclaration22isAggregateDeclarationEv, 196415 ]
[ 0 , _ZN7Dsymbol22isAggregateDeclarationEv, 13102 ]
[ 1 , _ZN11Declaration4protEv, 195211 ]
[ 2 , _ZN7Dsymbol7toCharsEv, 20 ]
de>
为此,函数分析了三个间接调用 de>(0,1,2) de>.在0调用 点处,注册了两个函数指针值;de>94% de>的时间是用de>ICP de>发出de>"内联我!" de>尖叫的de>AggregateDeclaration::isAggregateDeclaration() de>调用 ,其他de>两个 de>调用 点,仅注册了一个函数指针值.1调用 点的de>ICP de>肯定也会内联de>声明::prot() de>.
与de>checkAccess(AggregateDeclaration*,Loc,域*,Dsymbol*) de>中的三个de>间接调用 de>一样,de>D de>中的大多数间接调用 都是de>特殊 de>的:它们是de>虚函数 de>调用 .进入"提升虚调用 ".
d ="VCP_115">提升虚调用 de>(VCP) de>
我一直在努力让de>LDC de>中比de>ICP de>更进一步:把de>虚调用 de>提升为de>直接调用 de>.除了de>分析 de>调用 函数地址之外,还可分析对象的de>虚表 de>地址,来提升查找de>虚表 de>为de>直接调用 函数 de>.这消除了两个de>间接 de>:对象到de>虚表 de>到de>函数指针 de>.考虑:
de class="prism language-cpp">d">class A {
d">int foo ( d">int a) {
d">return a * 2 ;
}
} ;
d">void somefunction ( A a) {
d">auto b = a. foo ( 2 ) ;
}
de>
因为de>某个函数 de>必须,对de>A类型 de>的de>a de>及继承自A类型的de>a de>工作,所以编译器de>"降级" de>,de>foo de>的虚调用 为如下de>伪代码 de>:
de class="prism language-cpp">d">void somefunction ( A a) {
d">auto 虚表 = a. __vptr;
d">int function ( A, d">int ) fptr = 虚表[ ind ex of A. foo] ;
d">auto b = fptr ( a, 2 ) ;
}
de>
de>分析 de>文件数据表明,一般用(继承自de>A de>的)de>B de>类型的de>对象 de>调用 de>某个 de>函数时,可de>优化 de>代码为:
de class="prism language-cpp">d">void somefunction ( A a) {
d">auto b =
( cast ( d">void * ) a. __vptr == cast ( d">void * ) d">typeid ( B) . vtbl. ptr)
b = a. B. foo ( 2 )
: b = a. foo ( 2 ) ;
}
de>
代码转换为,比较de>a对象 de>的de>虚表 de>指针与de>B类型对象 de>的de>虚表 de>指针:如果为de>真 de>,则de>直接 de>调用 ;否则,用de>原虚调用 de>.de>a.B.foo(2) de>是不管de>a de>是否为de>B类型 de>,显式调用 de>B类 de>的de>foo de>函数的de>D语法 de>.
(可惜,de>cast(void *)typeof(B).vtbl.ptr de>不是常数) (相比之下,de>GCC de>有在没有de>配置文件 de>数据时,推测性去虚化的选项.
d ="llvm_163">哄骗de>llvm de>
de>虚调用 de>指令类似de>间接调用 de>.de>虚调用 de>指令的伪代码版本de>ld c2 -fprofile-instr-generate -fprofile-virtual-calls de>:
de class="prism language-cpp">d">void somefunction ( A a) {
d">auto 虚表 = a. __vptr;
__llvm_profile_instrument_target ( 虚表, & __profd _somefunction, 0 ) ;
d">int function ( A, d">int ) fptr = 虚表[ ind ex of A. foo] ;
d">auto b = fptr ( a, 2 ) ;
}
de>
以下是启用de>VCP de>时为de>LDC de>自身取de>配置文件 de>的片段,再次显示了de>extern(C++) de>函数de>checkAccess(AggregateDeclaration*,Loc,域*,Dsymbol*) de>的配置文件数据:
de class="prism language-cpp">_Z11checkAccessP20AggregateDeclaration3LocP5域P7Dsymbol:
< snip>
间接调用 点数: 3
间接目标结果:
[ < 调用 点> , < 符号名> , < 计数> ]
[ 0 , , 181132 ] *
[ 0 , , 13102 ]
[ 0 , , 12316 ] *
[ 0 , , 2921 ] *
[ 0 , , 46 ] *
[ 1 , , 131771 ]
[ 1 , , 53053 ]
[ 1 , , 6526 ]
[ 1 , , 3502 ]
[ 1 , , 296 ]
[ 1 , , 40 ]
[ 1 , , 21 ]
[ 1 , , 2 ]
[ 2 , , 20 ]
de>
为什么没有符号名
为了使de>虚表 de>值分析de>工作 de>,必须稍微欺骗一下de>llvm de>.如上,de>__profd _ de>数据结构用来de>转换 de>原de>指针值 de>为编译器给出的de>哈希 de>(且在程序内不变).
但是de>__profd _X de>仅在,de>X函数 de>内有de>llvm de>分析内置函数时,de>llvm de>才生成它.
而且,de>虚表 de>不是函数!与de>llvm de>对de>函数 de>那样,de>LDC de>必须手动为de>虚表 de>创建de>__profd _ de>数据结构,这样de>llvm de>就可de>哈希查找 de>指针值.(有效的de>__profd _ de>数据结构还包含de>带计数器项 de>的de>__profc_ de>数组指针,所以也必须生成它).
de>llvm de>对它自动生成的de>__profd _ de>,保留所有de>函数名 de>列表,并按de>压缩格式 de>把它们放入de>可执行 de>文件中;
de>__profd _ de>包含索引de>压缩数据 de>的de>函数名哈希 de>.因为我生成自己的de>__profd _ de>数据结构,所以de>符号名 de>不在de>llvm de>的分析名列表中,de>查找 de>哈希失败.
问题不大:de>LDC de>只需要知道de>虚表 de>指针的哈希值,而不是de>实际名 de>.也许需要de>llvm de>补丁,来允许de>指针->哈希->名 de>查找,这会帮助解决未使用de>指令 de>编译函数目标的de>ICP de>不可用的问题.
返回到de>配置 de>文件数据.在0调用 点中,注册了de>五个 de>不同的de>虚表 de>指针值,de>86% de>的时间它是第一个.明显的de>VCP de>机会.看看de>-O3 de>生成的de>llvmIR de>:
de class="prism language-cpp"> % 4 = tail call % d d md . d symbol. Dsymbol* @_ZN7Dsymbol8toParentEv ( % d d md . d symbol. Dsymbol* nonnull % smember_arg) ; [ #uses = 7 ]
; . . .
% 6 = getelementptr inbound s % d d md . d symbol. Dsymbol, % d d md . d symbol. Dsymbol* % 4 , i64 0 , i32 0
% 7 = load % d d md . d symbol. Dsymbol. __vtbl* , % d d md . d symbol. Dsymbol. __vtbl* * % 6 , align 8
% 8 = icmp eq % d d md . d symbol. Dsymbol. __vtbl* % 7 , bitcast ( % d d md . d struct. StructDeclaration. __vtbl* @_D4d d md 7d struct17StructDeclaration6__vtblZ to % d d md . d symbol. Dsymbol. __vtbl* )
br i1 % 8 , label % pgo. 虚表. 真, label % pgo. 虚表. 假, ! prof ! 185
pgo. 虚表. 真:
% 9 = bitcast % d d md . d symbol. Dsymbol* % 4 to % d d md . aggregate. AggregateDeclaration*
br label % pgo. 虚表. end
pgo. 虚表. 假:
% "smemberParent.isAggregateDeclaration@vtbl" = getelementptr inbound s % d d md . d symbol. Dsymbol. __vtbl, % d d md . d symbol. Dsymbol. __vtbl* % 7 , i64 0 , i32 54
% 10 = load % d d md . aggregate. AggregateDeclaration* ( % d d md . d symbol. Dsymbol* ) * , % d d md . aggregate. AggregateDeclaration* ( % d d md . d symbol. Dsymbol* ) * * % "smemberParent.isAggregateDeclaration@vtbl" , align 8
% 11 = tail call % d d md . aggregate. AggregateDeclaration* % 10 ( % d d md . d symbol. Dsymbol* nonnull % 4 )
br label % pgo. 虚表. end
pgo. 虚表. end :
% 12 = phi % d d md . aggregate. AggregateDeclaration* [ % 9 , % pgo. 虚表. 真 ] , [ % 11 , % pgo. 虚表. 假 ]
% 13 = icmp eq % d d md . aggregate. AggregateDeclaration* % 12 , null
; . . .
! 185 = ! { ! "branch_weights" , i32 181133 , i32 28386 }
de>
逐行: 1,de>%4 de>存储de>smemberParent=smember.toParent() de>的值;(de>直接调用 de>); 2,de>%6 de>取de>smemberParent de>的de>__vptr de>,de>虚表 de>指针位置的de>指针 de>; 3,de>%7 de>加载de>虚表 de>指针; 4,de>%8 de>是用de>StructDeclaration de>的链接时间常量de>虚表 de>指针比较de>%7 de>的结果; 5,下面是带额外de>分支 de>权重元数据的de>!prof !185 de>(识别de>(+1)? de>数字)的de>%8 de>条件上的de>分支 de>;
6,de>pgo.虚表.真 de>:如果为de>真 de>,则完全内联(de>中 this; de>)de>smemberParent.StructDeclaration.isAggregateDeclaration() de>调用 ,只剩下个de>转换位 de>;
7,de>pgo.虚表.假 de>:如果为de>假 de>,则索引到de>虚表 de>(de>%"smemb...@vtbl" de>和de>%10 de>),并调用 该de>(%11) de>函数指针来,调用 de>smemberParent.isAggregateDeclaration(); de>.
8,在de>pgo.虚表.end de>合并分支:de>PHI de>节点根据取的执行de>路径 de>,选择de>%9 de>或de>%10 de>返回值,并在de>%12 de>中存储它; 9,最后,de>%13 de>使用de>VCP de>检查de>返回值 de>是否为de>null(!) de>,及是否完成de>!smemberParent.isAggregateDeclaration() de>!
d ="ICPVCP_258">de>ICP de>与de>VCP de>通信
根据de>类层次 de>,不同de>虚表 de>可能引用de>相同 de>函数.
de class="prism language-cpp">d">class B : A {
d">overrid e d">int foo ( d">int a) {
d">return a + 1 ;
}
}
d">class C : A {
d">void bar ( ) { }
}
d">void somefunction ( A a) {
d">auto b = a. foo ( 2 ) ;
}
de>
de>A.foo de>和de>C.foo de>指向相同函数,但de>虚表 de>指针不同.根据de>传递 de>给某个函数的de>对象类型 de>的统计信息,想在查找de>虚表 de>或de>VCP de>后利用de>间接调用 de>的de>ICP de>.
当大多数时候传递de>C de>时,你会想要de>VCP de>(假设它比de>ICP de>更快).主要是de>A和C de>的混合时,你会想要de>ICP de>.
为了在de>ICP de>和de>VCP de>之间良好de>交互 de>,应分析de>间接 de>调用 和de>虚表 de>查找(de>-fprofile-ind irect-calls -fprofile-virtual-calls de>). 目前实现了个de>简单 de>的方案,当de>VCP de>有利时,de>忽略 de>间接调用 de>配置文件 de>数据(de>次数>1000 de>,de>虚表 de>指针值的概率为de>80% de>或更高).
上面给出的de>配置 de>文件数据中,带de>* de>的de>虚表 de>指针,都会导致调用 de>AggregateDeclaration::isAggregateDeclaration() de>(结合给定的de>配置文件 de>数据并计算). 问题是,de>VCP de>(de>86% de>的命中率)还是de>ICP de>(de>96% de>的命中率,但额外的de>虚表 de>查找"de>有代价 de>")更好.
d iv>
d iv>
<d iv id ="treeSkill">d iv>
<d iv id ="blogExtensionBox" style="wid th:400px;margin:auto;margin-top:12px" class="blog-extension-box">d iv>