专业化是GHC用来消除ad-hoc多态性的性能开销,并支持其他强大的优化。然而,专业化并不是免费的,因为它需要更多的工作由GHC在编译期间执行,并导致更大的可执行文件。事实上,过度专业化会导致编译成本和可执行文件大小,运行时性能优势最小。因此,GHC悲观地避免了默认情况下的过度专业化,可能会离开在这样做的过程中,没有发现相对低成本的性能改进。
乐观的Haskell程序员希望利用这些错过的机会因此,机会面临着发现和制定的艰巨任务在平衡任何通过增加编译成本和可执行文件提高性能尺寸。到目前为止,这是一种笨重的舞蹈,需要拼命涉水通过GHC核心转储,只会产生不稳定、效率低下、,似乎可以提高性能的一组无动机的杂注和/或GHC标志。
在这个由两部分组成的系列文章中,我描述了我们最近所做的工作改善这种情况,使Haskell程序更加专业化这是一门科学,而不是一门黑暗的艺术。在第一篇帖子中,我将
- 全面介绍GHC的专业化优化,
- 探索GHC为观察和控制它提供的各种设施,以及
- 提出一个简单的框架来思考专业化。
在本系列的下一篇文章中,我将
- 介绍我们开发用于诊断的新工具和技术由自组织多态性引起的性能问题,
- 演示如何系统地使用这些新工具确定有用的专业,以及
- 从以下方面理解他们的影响本文中描述的框架。
本文的目标读者包括中级Haskell开发人员,他们想更多了解GHC中的专门化和特殊多态性,以及对系统方法感兴趣的高级Haskell开发人员以最小化编译成本和可执行文件大小,同时最大限度地提高性能。
这项工作之所以能够进行,是因为哈苏拉,谁有支持Well-Typed的许多成功举措到改进商业Haskell用户的工具。
我在Haskell Unfolder上总结了这篇文章的内容:
重载函数在Haskell中很常见,但也有代价。值得庆幸的是,GHC的专家非常善于消除这一成本。因此,我们可以编写高级的多态程序,并相信GHC会将它们编译成非常高效的单态化代码。在本集中,我们将揭开GHC为实现这一目标所做的看似神奇的事情的神秘面纱。
特殊多态性
在哈斯克尔特殊多态性或过载函数是其类型包含类约束。例如,这个(f)
是重载函数:
传真::(订单a、,号码a)=>一->一->一
f x y轴=
如果x个<年然后
x个+年
其他的
x个-年
对于某些类型一
这样的话命令a
和数字a
提供了实例,(f)
采用两个类型的值一
并对另一个进行评估一
。
重要的是,与类型参数不同,这些类约束不会在运行时!事实上,他们会被传给(f)
就像其他任何值一样参数,含义(f)
在运行时更像是:
传真:: 订单一-> 号码一->一->一->一
f orda数字a x y= ...
如何定义(f)
改变来代表这个?这些是什么订单a
和数字(&a)
值看起来像什么?其工作原理如下:
- 实例被编译为记录,通常称为字典,其字段是实例中提供的定义。
- 类函数(例如
<
在的身体里(f)
)成为记录选择器应用于字典以查找适当的定义。
因此,(f)
在运行时更像是:
传真:: 订单一-> 号码一->一->一->一
f orda数字a x y=
如果(<)订单a x y然后
(+)数字a x y
其他的
(-)数字a x y
以前的infix类运算符现在应用于前缀位置从字典中选择适当的定义,然后应用于参数。
我们可以通过编译(f)
在模块中F.hs公司
并发出中间表示(以GHC的核心语言):
温室气体F.hs公司-O(运行) -dno可键入索引 -dsuppress-all所有 -数据抑制-唯一 -ddump数据库
这个-O(运行)
标志启用优化,并且-ddump-ds(数据转储-ds)
flag告诉GHC转储程序在去糖后,优化前。其他标志使输出更具可读性。
全面介绍GHC核心和GHC接受的标志查看,查看哈斯克尔揭秘第九集:GHC核心。
上述命令将为输出以下Core(f)
以下为:
(f)=\@一$命令$dNum x y(数字x y)->
案例 < $dOrd x y(顺序x y)属于{
False(错误) -> - $dNum x y(数字x y);
真的 -> + $dNum x y(数字x y)
}
这个如果
已转换为案例
(核心没有如果
构造)。这个$订单
和$dNum(数字)
参数是命令a
和数字a
实例字典,分别是。这个<
运算符应用于前缀位置操作员(核心)$订单
字典以获取适当的实施<
,进一步应用于x个
和年
. The-
和+
分支机构中的操作员案例
都是类似的。
传递这些隐式字典参数和对它们应用选择器确实会产生可测量的开销,尽管是这样对于大多数意图和目的来说无关紧要。正如我们将看到的ad-hoc多态性来自于它所阻止的优化,而不是它引入的开销。
专业化
在这种情况下,专业化指移除特殊多态性。当我们专门化重载表达式时e::C a=>S a
,我们创建一个新绑定eT::S T
,其中T型
是某种混凝土类型对于其中一个C T公司
实例存在。在这里电子技师
是专业化 e(电子)
在(或到)类型T型
。
例如,我们可以手动创建(f)
在类型国际
. The源定义保持不变,只有类型更改:
fInt:: 国际 -> 国际 -> 国际
fInt x y(积分x年)=
如果x个<年然后
x个+年
其他的
x个-年
在核心级别,作为值参数传递给(f)
现在直接用于fInt公司
。如果我们添加fInt公司
对我们的示例模块进行编译,我们得到以下输出:
(f)=\@一$命令$dNum x y(数字x y)->
案例 < $dOrd x y(顺序x y)属于{
False(错误) -> - $dNum x y(数字x y);
真的 -> + $dNum x y(数字x y)
}
fInt公司
=\x年->
案例 < $fOrdInt x y(顺序整数x y)属于{
False(错误) -> - $fNumInt x y;
真的 -> + $fNumInt x年
}
飞行情报
不再接受字典参数,而是引用全局订单Int
和数字Int
直接使用字典。事实上,这个定义fInt公司
是确切地如果GHC专家决定创造什么专门从事(f)
到国际
。我们可以通过手动指示自行查看GHC使用专门从事
杂注。我们的整个模块现在是:
模块 F类 哪里
{-#指定f::Int->Int->Int#-}
传真::(订单a、,号码a)=>一->一->一
f x y轴=
如果x个<年然后
x个+年
其他的
x个-年
传真:: 国际 -> 国际 -> 国际
fInt x y(积分x年)=
如果x个<年然后
x个+年
其他的
x个-年
以及-ddump-ds(数据转储-ds)
核心输出变为:
fInt公司
=\x年->
案例 < $fOrdInt x y(顺序整数x y)属于{
False(错误) -> - $fNumInt x y;
真的 -> + $fNumInt x y(数字积分x y)
}
$平方英尺
=\x年->
案例 < $fOrdInt x y(顺序整数x y)属于{
False(错误) -> - $fNumInt x y;
真的 -> + $fNumInt x y(数字积分x y)
}
(f)=\@一$命令$dNum x y(数字x y)->
案例 < $dOrd x y(顺序x y)属于{
False(错误) -> - $dNum x y(数字x y);
真的 -> + $dNum x y(数字x y)
}
GHC产生的专业化被命名为平方英尺美元
(GHC的所有专业生成的前缀为%s美元
). 请注意,我们的专业化(fInt公司
)以及GHC产生的专业化(平方英尺美元
)完全等价!
为什么这是一个优化?
上述转变实际上是GHC专家对我们的程序。可能还不清楚为什么这种优化是有意义的完全优化。这是因为专业化是一种有可能优化:真正的好处来自它所支持的优化稍后在管道中,例如内联。
内联将定义的(顶级或let-bound)变量替换为他们的定义。尽管(f)
及其专业化平方英尺美元
看起来很相似关键区别在于(f)
包括对作为部分传递的“未知”函数的调用字典参数的,而平方英尺美元
包括对“已知”函数的调用包含在$fOrdInt(美元)
和$fNumInt(美元)
字典。由于GHC可以访问这些字典和包含的函数的定义可以是内联,提供更多优化机会。
通过比较我们的示例模块。为此,使用与上面的命令相同,但添加-ddump-simpl(ddump-impl)
标志,它告诉GHC在Core优化管道的末端转储Core(同时添加-强制重新补偿
强制重新编译,因为自上次编译以来,我们没有更改代码):
温室气体F.hs公司-强制重新补偿 -O(运行) -dno可键入索引 -dsuppress-all所有 -数据抑制-唯一 -ddump-ds(数据转储-ds) -ddump-simpl(ddump-impl)
转储输出为:
==================== 脱糖剂(优化后)====================
结果大小属于 脱糖剂(优化后)
={条款以下为: 57,类型以下为: 37,胁迫以下为: 0,个连接以下为: 0/0}
fInt公司
=\x年->
案例 < $fOrdInt x y(顺序整数x y)属于{
False(错误) -> - $fNumInt x y;
真的 -> + $fNumInt x y(数字积分x y)
}
$平方英尺
=\x年->
案例 < $fOrdInt x y(顺序整数x y)属于{
False(错误) -> - $fNumInt x y;
真的 -> + $fNumInt x y(数字积分x y)
}
(f)=\@一$命令$dNum x y(数字x y)->
案例 < $dOrd x y(顺序x y)属于{
False(错误) -> - $dNum x y(数字x y);
真的 -> + $dNum x y(数字x y)
}
==================== 整洁 核心 ====================
结果大小属于 整洁 核心
={术语以下为: 44,类型以下为: 29,胁迫以下为: 0,联接以下为: 0/0}
fInt公司
=\x年->
案例x个属于{我#x1个->
案例年属于{我#y1个->
案例 <#x1 y1个属于{
__默认-> 我#(-#x1 y1);
1# -> 我#(+#x1 y1)
}
}
}
(f)=\@一$命令$dNum x y(数字x y)->
案例 < $dOrd x y(顺序x y)属于{
False(错误) -> - $dNum x y(数字x y);
真的 -> + $dNum x y(数字x y)
}
------导入的ID的本地规则--------
“USPEC f@Int” 对于所有人 $数字$命令。(f)$命令$数字=fInt公司
脱糖过程的输出在“脱糖(优化后)”中部分,而完全优化的输出位于“Tidy Core”部分。这个名称“去糖(优化后)”仅表示它是去糖核心输出在GHC的简单优化器运行之后。简单的优化器只做非常对Core程序的轻量级、纯转换。我们仍将提及这一阶段的核心产出为“未优化”。
在整个优化过程中,GHC确定了fInt公司
和平方英尺美元
并决定删除平方英尺美元
。的完全优化绑定飞行情报
正在取消装箱国际
s(模式匹配我#
建造商)和使用有效的基本操作(<#
,-#
,+#
),而完全的优化绑定(f)
与未优化的绑定相同。优化器在这种情况下,那些晦涩难懂的字典简直什么也做不了!
输出的底部是重写规则那个专门从事
创建了杂注,这将导致(f)
已知处于类型国际
作为应用程序重写fInt公司
。这是是什么让程序的其余部分从专业化中受益。规则只需放弃字典参数$dNum::整数
和$dOrd::订单Int
,这是安全的,因为全球类型类一致性:任何字典显式传递的必须最初来自相同的全局实例。
总之,通过将不透明字典参数替换为(f)
带有参考混凝土订单Int
和数字Int
词典fInt公司
,GHC能够稍后在管道中进行更多优化。
自动专业化
在我们的示例模块中,我们手动指示GHC生成专门化属于(f)
在国际
使用专门化
实用主义。实际上,我们通常依赖GHC来确定需要哪些专业化并生成它们自动为我们服务。但GHC需要小心,因为专业化需要创建和优化更多绑定,这会增加编译成本和可执行文件大小
GHC使用了几种启发式方法,通过违约。启发式非常悲观,这意味着GHC很容易漏掉程序员可能希望手动实现的宝贵专业化机会地址。这正是我们最近工作的目的所以在我们继续之前,我们必须准确地了解GHC决定专业化应该(或不应该)发生的时间和原因。
自动专业化何时发生?
GHC仅可能尝试自动专业化确切地一场景:在具体的、静态已知的类型上遇到重载调用(从现在起,我们将此类调用称为“可专门化”调用)。这意味着自动专门化只会在调用站点触发,而不会定义站点。即使在这种情况下,也需要考虑其他因素下面的示例将对此进行演示。
让我们添加一个绑定foo公司
到我们的示例模块F.hs公司
从上面:
foo::(整数,整数)-> 整数
foo(x,y)=f x y轴
foo公司
对进行专门化调用(f)
混凝土类型整数
,所以我们可能期望自动专业化发生。然而,内联程序胜过这里是冲床的特工,这在-ddump-simpl(ddump-impl)
输出:
$世界粮食组织
=\网址:ww1->
案例integerLt网址ww1属于{
False(错误) ->integerSub ww1;
真的 ->integer添加ww1
}
foo公司=\ds公司-> 案例ds公司属于{(ww,ww1)-> $wfoo网址ww1}
GHC没有专门化,而是决定通过内联完全消除调用(f)
从而暴露出其他优化机会(例如工人/包装工)GHC利用了这一点。这是计划,因为(f)
很小,GHC知道内联非常便宜,而且可能值得绩效结果。
另一种观察GHC内联决策的方法是通过-ddump插件
标志,导致GHC转储它决定内联的任何绑定的名称。使用编译模块
温室气体F.hs公司-O(运行) -强制重新补偿 -ddump插件
输出结果表明GHC确实决定内联(f)
以下为:
内嵌完成:F.F
内联还是专门化?
如果可能,GHC更喜欢内联而不是专门化,因为内联消除了调用,不需要创建新绑定。然而,过多的内联通常甚至更多危险的而不是过度专业化。因此,即使一个专门化呼叫被认为成本过高而无法内联,GHC也会仍然尝试将其专门化。
我们可以通过调整GHC来人为地在我们的示例中创建这样的场景称之为“展开使用阈值”。粗略地说,“展开”是一个定义GHC决定内联或专门化对该绑定的调用时使用的绑定结合。展开使用阈值控制最大有效值大小GHC将纳入的展项使用手动调整-折叠式美国门槛标志。让我们设置将使用阈值展开为-1,本质上使GHC认为所有内联都非常价格昂贵,请检查-ddump-simpl(ddump-impl)
输出:
ghc F.hs-O-force-recomp-ddump-simpl-funfolding-use-threshold=-1
正如我们所见,GHC确实专门进行了呼叫:
...
(f)_$sf1型
=\x年->
案例整数Lt x y属于{
False(错误) ->integerSub x y;
真的 ->整数相加x y
}
foo公司=\数据集-> 案例ds公司属于{(ww,ww1)->(f)_$sf1网址ww1}
------导入的ID的本地规则--------
“SPEC f@Integer” 对于所有人 $命令$数字。(f)$命令$数字=(f)_$sf1型
...
专业化的名称(f_$sf1
)重写规则表明GHC已成功自动将重载调用专门化为(f)
。
有趣的是foo公司
及其专业化f_$平方英尺
是α-等效物GHC达成的条款内联调用并应用工人/包装工相反,专业化也一样作为工人的角色。
跨模块自动专业化
我们现在讨论了呼叫自动专业化的两个先决条件:
- 调用必须是可专门化的(即它必须是对重载的调用以已知类型绑定)。
- 其他优化,如内联,可以删除调用或以其他方式破坏在专门化可以启动之前,调用的专门化不能启动发生。
实际上,对于在重载绑定(正如我们上一个示例中的情况),这些是唯一的前提条件。当重载绑定从另一个模块导入时(通常情况下),还有一些附加的先决条件,我们将现在讨论。
暴露的展开和INLINABLE公司
杂注
GHC执行单独编译(与整个程序编译相反),一次编译一个Haskell模块。GHC编译模块时,会生成不仅在目标文件中编译代码,而且接口文件(带有后缀你好
) . 接口文件包含有关以下模块的信息:GHC在编译其他模块时可能需要引用,例如名称和模块导出的绑定类型。如果满足某些标准,GHC将在模块的接口文件中包含一个绑定的展开,以便稍后用于跨模块内联或专门化。这样的展开是称为暴露的展开。
现在,您可能会有理由怀疑:如果使用展开来实现这些强大功能优化,为什么GHC只公开符合某些标准的展开?为什么?不暴露所有展开?原因是在编制期间,GHC持有的接口每一个内存中程序中的模块。因此,为了保持GHC自己的默认性能和内存使用合理,模块接口需要如下尽可能小,同时仍能生成优化良好的程序。一种方式GHC通过限制包含在接口文件,以便默认情况下只显示小的展开。
这里还有另一个影响跨模块专业化的问题:即使GHC决定公开重载绑定的展开,以及一个可专门化的对该绑定的调用发生在另一个模块中,GHC仍将从未除非获得显式权限,否则自动专门化该调用创造专业化。这种显式权限只能在一个以下方式之一:
让我们通过继续我们的示例来探讨这个事实。移动foo公司
,这使得可专门调用(f)
,到另一个模块食品.hs
那个有-折叠式美国门槛
设置为-1表示愚弄与之前一样的内嵌:
{-#OPTIONS_GHC-funfolding-use-threshold=-1#-}
模块 富 哪里
进口 F类
foo::(整数,整数)-> 整数
foo(x,y)=f x y轴
同时从中删除所有内容F.hs公司
除了(f)
,为了更好地衡量:
模块 F类 哪里
传真::(订单a中,号码a)=>一->一->一
x年=
如果x个<年然后
x个+年
其他的
x个-年
自(f)
规模如此之小,我们可能期望GHC在F.hi公司
默认情况下为模块接口。如果我们用just编译
我们得到目标文件F.o公司
和接口文件F.hi公司
。我们可以确定GHC是否决定揭露(f)
通过查看的内容使用GHC的接口文件--展示我的脸
选项:
模块中每个绑定的具体信息列在底部输出。任何外露展开的GHC核心将显示在它们各自的绑定。在这种情况下(f)
看起来像这个:
bcb4b04f3cb5e6aa2f776d6226a0930f::(序号a,数字a)=>a->a->a[]
它只包含类型,没有展开!这是因为GHC违约的优化级别-0号机组
,的-界面上的泡沫
和-图形界面-图形
标志是启用此选项可防止展开(以及其他内容)包含在和中从模块接口读取。启用优化后重新编译并检查再次显示模块界面:
温室气体 -O(运行)F.hs公司
温室气体 --显示iface你好-dsuppress-all所有
这一次,GHC确实揭露了事态的发展:
152dd20f273a86bea689edd6a298afe6
传真::(订单a、,号码a)=>一->一->一
[...,
展开以下为: 核心以下为: <香草>
\@一
($命令[“很多]:: 订单a)
($数字[“很多]:: 号码a)
(x)[“很多]::a)
(年)[“很多]::a)->
案例 < @一$dOrd x y(顺序x y)属于野生的{
False(错误) -> - @一$数字x y真的 -> + @一$数字x y}]
记住,我们仍然没有明确授权GHC专门调用(f)
跨模块,因此我们应该期望食品.hs
到仍然包括对的重载调用(f)
。让我们检查一下:
温室气体食品.hs-O(运行) -dno可键入索引 -dsuppress-all所有 -数据抑制-唯一 -ddump-simpl(ddump-impl)
倾倒的堆芯包括:
$世界粮食组织=\网址:ww1->(f)$fOrdInteger(顺序整数)$fNumInteger全球ww1
foo公司=\ds公司-> 案例ds公司属于{(ww,ww1)-> $wfoo网址ww1}
事实上,GHC应用了工人/包装工转型到foo公司
,但无法专门调用(f)
尽管它满足了我们的前面讨论了自动专门化的前提条件。
GHC中有一个警告标志,可以通知我们这种情况:-Wall-missed专业化
.编译食品.hs
同样,包括此标志:
温室气体食品.hs-O(运行) -强制重新补偿 -Wall-missed专业化
这将输出以下警告:
Foo.hs:警告:[-Wall-missed-specialitions]无法专门化导入的函数“f”可能的修复方法:在“f”上添加INLINABLE杂注
如果我们按照警告所说的那样添加可内联的
上的杂注(f)
,并转储食品.hs
,我们会看到那个自动的专业化成功:
$平方英尺
=\x年->
案例整数Lt x y属于{
False(错误) ->integerSub x y;
真的 ->整数相加x y
}
foo公司=\ds公司-> 案例ds公司属于{(ww,ww1)-> $sf网址ww1}
------导入的ID的本地规则--------
“SPEC/Foo f@整数” 对于所有人 $命令$数字。(f)$命令$数字= $平方英尺
卸下可内联的
杂注打开(f)
而是启用-fspecialize-aggressively专攻
有相同的结果。
自动专业化决策图
我们现在已经介绍了自动专业化的所有主要先决条件。收件人总结一下,下面是一个决策图,说明了任意函数调用可以触发自动专门化:
现在,我们完全了解了GHC专业人员是如何、为什么以及何时工作的,我们可以继续讨论其行为导致的实际问题。大多数本系列的下一篇文章将讨论此问题,但之前最后,我想介绍一些我称之为“专业化谱”的东西。
专业化谱
专业化是一种非常有价值的编译器优化,但我已经提到了许多在这篇文章中,过度专业化可能是一件坏事。这就引出了一个问题:我们如何知道自己是否适当地受益来自专业化?此处“适当”的含义取决于特定于应用程序的要求,规定了我们的可执行文件,我们多么关心编译成本,以及我们多么关心关于性能。
例如,如果我们想不惜一切代价实现性能最大化,我们应该确保我们正在生成和使用一组最大化的专门化我们感兴趣的性能指标,忽略了编译成本和可执行文件大小。
基本上,我们的目标是在专业化光谱。
专业化谱
这是我们的搜索空间,在一个轴上有性能,代码大小和另一方面是编译成本。绘制的点表示重要频谱中与应用程序无关的点。这些要点是:
- 基线:性能最低、成本最低。这一点代表GHC的其启发式将导致较小代码大小和编译成本较低,但可能会导致错过专门化在重大表现中获胜。
- 理想:作为应用程序的作者,我们可以选择此根据我们的优先事项确定。通常,我们希望此为“高左”。
- 最大性能:这一点表示最佳的专业化设置,这将比其他任何一组专业化。
- 最大专业化:此点是生成每一个通过启用可能的专业化
-fexpose-所有褶皱
和-fspecialize-aggressively专攻
重要的是,这是不始终等同于最大性能!如果我们产生无用的专门化只会带来很少或根本没有的性能改进增加代码大小,我们最终可能会失败的更多代码带来的性能交换CPU缓存。
虚线表示近似的“最佳路径”,表示当我们按降序生成所有专业化时,我们可能会看到结果性能改进。
这个框架清楚地表明,这实际上只是一个优化问题,传统优化问题的所有正常问题都在发挥作用。不幸的是,由于缺乏探索这种光谱的良好工具特别容易让程序员迷失方向,陷入危险、不理想的境地路径如下:
这样的案例是欺骗性的,让程序员认为他们已经陷入了困境当它们实际上处于表现不佳的局部最优时。幸运的是我们将在本系列的下一篇文章中讨论的工具和技术将非常有用简化专业化谱的优化搜索。
总结
我们对专业化的初步探索到此结束。这就是我们已了解:
- 对重载函数的调用是通过使用每个类型类约束的函数记录。
- 专业化从重载的函数,并将对它们的引用替换为对混凝土的引用而不是字典。
- 专业化的几乎所有好处都来自以下优化它通过将不透明字典参数替换为具体字典来启用其内容可以内联。
- 只有在特定的条件集下,GHC才会自动专门化呼叫持有。请参见专业化的自动决策图表。
- 这个专业化谱方便吗专业化影响概念化框架程序的编译成本和运行时性能。
在本系列的下一篇文章中,我们将应用所学的所有知识介绍一些示例应用程序,并演示我们拥有的新工具发达国家可以帮助我们实现最佳专业化和绩效。
]]>