我有一个在内部循环中经常调用的函数。它看起来像这样:
import qualified Data.Vector.Storable as SV
newtype Timedelta = Timedelta Double
cklsLogDens :: SV.Vector Double -> Timedelta -> Double -> Double -> Double
cklsLogDens p (Timedelta dt) x0 x1 = if si <= 0 then -1e50 else c - 0.5*((x1-mu)/sd)^2
where
al = p `SV.unsafeIndex` 0
be = p `SV.unsafeIndex` 1
si = p `SV.unsafeIndex` 2
xi = p `SV.unsafeIndex` 3
sdt = sqrt dt
mu = x0 + (al + be*x0)*dt
sd = si * (x0 ** xi) * sdt
c = sd `seq` -0.5 * log (2*pi*sd^2)
(使用了Data.Vector.Storable,因为此函数需要稍后处理来自C函数的数据)
GHC对此进行了非常好的优化(据我所知,所有变量和操作都是原语),但是看一下核心,有一个let
仍然在(是)的主体之内功能。我已经读过here(以及其他我不记得的地方)'允许'分配懒惰的thunk,因此可能对紧密循环中的性能不利。我可以摆脱它吗?如果一切皆有可能,我宁愿不将我的功能转换为20个案例陈述,但如果这太过于要求我接受。
以下是核心:
$wloop_s4Li [Occ=LoopBreaker]
:: GHC.Prim.Double#
-> GHC.Prim.Int# -> GHC.Prim.Int# -> GHC.Prim.Double#
[LclId, Arity=3, Str=DmdType LLL]
$wloop_s4Li =
\ (ww_X4OR :: GHC.Prim.Double#)
(ww1_X4OW :: GHC.Prim.Int#)
(ww2_X4P1 :: GHC.Prim.Int#) ->
case GHC.Prim.<# ww1_X4OW ww2_X4P1 of _ {
GHC.Types.False -> ww_X4OR;
GHC.Types.True ->
case GHC.Prim.<=## x_a4tg 0.0 of _ {
GHC.Types.False ->
case GHC.Prim.indexDoubleArray#
rb2_a4rT (GHC.Prim.+# rb_a4rR (GHC.Prim.-# ww1_X4OW 1))
of wild17_X4xM { __DEFAULT ->
let {
---- ^^^^ want to get rid off this!
----
----
ipv1_X2S8 [Dmd=Just L] :: GHC.Prim.Double#
[LclId, Str=DmdType]
ipv1_X2S8 =
GHC.Prim.*##
(GHC.Prim.*## x_a4tg (GHC.Prim.**## wild17_X4xM y_a3BN))
(GHC.Prim.sqrtDouble# tpl1_B3) } in
case GHC.Prim.logDouble#
(GHC.Prim.*##
6.283185307179586 (GHC.Prim.*## ipv1_X2S8 ipv1_X2S8))
of wild18_X3Gn { __DEFAULT ->
case GHC.Prim.indexDoubleArray#
rb2_a4rT (GHC.Prim.+# rb_a4rR ww1_X4OW)
of wild19_X4AY { __DEFAULT ->
case GHC.Prim./##
(GHC.Prim.-##
wild19_X4AY
(GHC.Prim.+##
wild17_X4xM
(GHC.Prim.*##
(GHC.Prim.+##
x1_X3GA (GHC.Prim.*## x2_X3cb wild17_X4xM))
tpl1_B3)))
ipv1_X2S8
of wild20_X3x8 { __DEFAULT ->
$wloop_s4Li
(GHC.Prim.+##
ww_X4OR
(GHC.Prim.-##
(GHC.Prim.negateDouble# (GHC.Prim.*## 0.5 wild18_X3Gn))
(GHC.Prim.*##
0.5 (GHC.Prim.*## wild20_X3x8 wild20_X3x8))))
(GHC.Prim.+# ww1_X4OW 1)
ww2_X4P1
}
}
}
};
GHC.Types.True ->
$wloop_s4Li
(GHC.Prim.+## ww_X4OR -1.0e50)
(GHC.Prim.+# ww1_X4OW 1)
ww2_X4P1
}
}; }
(是的,当然,既然你必须要问,我花了太多时间来过早优化......)
以下是NOINLINE的当前版本
import qualified Data.Vector.Storable as SV
newtype Timedelta = Timedelta Double
cklsLogDens :: SV.Vector Double -> Timedelta -> Double -> Double -> Double
{-# NOINLINE cklsLogDens #-}
cklsLogDens p (Timedelta dt) x0 x1 = si `seq` (if si <= 0 then -1e50 else (sd `seq` (c - 0.5*((x1-mu)/sd)^2)))
where
al = p `SV.unsafeIndex` 0
be = p `SV.unsafeIndex` 1
si = p `SV.unsafeIndex` 2
xi = p `SV.unsafeIndex` 3
sdt = sqrt dt
mu = x0 + (al + be*x0)*dt
sd = si * (x0 ** xi) * sdt
c = sd `seq` (-0.5 * log (2*pi*sd^2))
main = putStrLn . show $ cklsLogDens SV.empty (Timedelta 0.1) 0.1 0.15
对应的核心代码段:
Main.cklsLogDens [InlPrag=NOINLINE]
:: Data.Vector.Storable.Vector GHC.Types.Double
-> Main.Timedelta
-> GHC.Types.Double
-> GHC.Types.Double
-> GHC.Types.Double
[GblId, Arity=4, Caf=NoCafRefs, Str=DmdType U(ALL)LLL]
Main.cklsLogDens =
\ (p_atw :: Data.Vector.Storable.Vector GHC.Types.Double)
(ds_dVa :: Main.Timedelta)
(x0_aty :: GHC.Types.Double)
(x1_atz :: GHC.Types.Double) ->
case p_atw
of _ { Data.Vector.Storable.Vector rb_a2ml rb1_a2mm rb2_a2mn ->
case GHC.Prim.readDoubleOffAddr#
@ GHC.Prim.RealWorld rb1_a2mm 2 GHC.Prim.realWorld#
of _ { (# s2_a2nH, x_a2nI #) ->
case GHC.Prim.touch#
@ GHC.ForeignPtr.ForeignPtrContents rb2_a2mn s2_a2nH
of _ { __DEFAULT ->
case GHC.Prim.<=## x_a2nI 0.0 of _ {
GHC.Types.False ->
case x0_aty of _ { GHC.Types.D# x2_a13d ->
case GHC.Prim.readDoubleOffAddr#
@ GHC.Prim.RealWorld rb1_a2mm 3 GHC.Prim.realWorld#
of _ { (# s1_X2oB, x3_X2oD #) ->
case GHC.Prim.touch#
@ GHC.ForeignPtr.ForeignPtrContents rb2_a2mn s1_X2oB
of _ { __DEFAULT ->
case ds_dVa
`cast` (Main.NTCo:Timedelta :: Main.Timedelta ~# GHC.Types.Double)
of _ { GHC.Types.D# x4_a13m ->
let {
--- ^^^^ want to get rid of this!
---
ipv_sYP [Dmd=Just L] :: GHC.Prim.Double#
[LclId, Str=DmdType]
ipv_sYP =
GHC.Prim.*##
(GHC.Prim.*## x_a2nI (GHC.Prim.**## x2_a13d x3_X2oD))
(GHC.Prim.sqrtDouble# x4_a13m) } in
case x1_atz of _ { GHC.Types.D# x5_X14E ->
case GHC.Prim.readDoubleOffAddr#
@ GHC.Prim.RealWorld rb1_a2mm 0 GHC.Prim.realWorld#
of _ { (# s3_X2p2, x6_X2p4 #) ->
case GHC.Prim.touch#
@ GHC.ForeignPtr.ForeignPtrContents rb2_a2mn s3_X2p2
of _ { __DEFAULT ->
case GHC.Prim.readDoubleOffAddr#
@ GHC.Prim.RealWorld rb1_a2mm 1 GHC.Prim.realWorld#
of _ { (# s4_X2pi, x7_X2pk #) ->
case GHC.Prim.touch#
@ GHC.ForeignPtr.ForeignPtrContents rb2_a2mn s4_X2pi
of _ { __DEFAULT ->
case GHC.Prim.logDouble#
(GHC.Prim.*## 6.283185307179586 (GHC.Prim.*## ipv_sYP ipv_sYP))
of wild9_a13D { __DEFAULT ->
case GHC.Prim./##
(GHC.Prim.-##
x5_X14E
(GHC.Prim.+##
x2_a13d
(GHC.Prim.*##
(GHC.Prim.+## x6_X2p4 (GHC.Prim.*## x7_X2pk x2_a13d)) x4_a13m)))
ipv_sYP
of wild10_a13O { __DEFAULT ->
GHC.Types.D#
(GHC.Prim.-##
(GHC.Prim.negateDouble# (GHC.Prim.*## 0.5 wild9_a13D))
(GHC.Prim.*## 0.5 (GHC.Prim.*## wild10_a13O wild10_a13O)))
}
}
}
}
}
}
}
}
}
}
};
GHC.Types.True -> lvl_r2v7
}
}
}
}
答案 0 :(得分:10)
丹尼尔是对的 - 事实上,let
问题没有分配一个thunk。这实际上是不可能的,因为诸如Double#
之类的原始类型没有堆表示。实际上,在所谓的核心准备阶段,这些let
转换为case
表达式,然后转换为STG(这是“let
=分配”规则实际成立的地方)。请参阅CorePrep.lhs中有关此主题的评论。
以下是准备前的核心(-ddump-simpl
):
let {
ipv_sPL [Dmd=Just L] :: GHC.Prim.Double#
ipv_sPL =
GHC.Prim.*##
(GHC.Prim.*## x_a160 (GHC.Prim.**## x1_a11G x2_X17h))
(GHC.Prim.sqrtDouble# x3_a11P) } in [...]
此后是(-ddump-prep
):
case GHC.Prim.sqrtDouble# x3_s1aU of sat_s1cB { __DEFAULT ->
case GHC.Prim.**## x1_s1aQ x2_s1aR of sat_s1cC { __DEFAULT ->
case GHC.Prim.*## x_s1aC sat_s1cC of sat_s1cD { __DEFAULT ->
case GHC.Prim.*## sat_s1cD sat_s1cB of ipv_s1aW [Dmd=Just L] { __DEFAULT ->
所以实际上没有任何堆分配。
另一方面,请注意,核心准备工作还会将每个应用程序显式包装到let
或case
语句中,从而产生非常详细的代码。这就是为什么-ddump-simpl
可能被视为查看Core的默认值,即使它的性能模型实际上更令人惊讶。
答案 1 :(得分:5)
使用ghc-7.6.1,我在-O
和-O2
之间没有区别,任何seq
或爆炸模式都没有区别。{{1}仍然是核心。
但我怀疑let
是否真的有害,它会绑定一个原始值,而不是一个盒装值,然后在三个地方使用该值。此外,在制作的集会中,我找不到任何懒惰的东西(但由于我对集会的了解相当有限,所以不要把它作为福音)。
我可以通过引入案例分支
来摆脱let
let
仅在核心中生成cklsLogDens p (Timedelta dt) x0 x1
= case p `SV.unsafeIndex` 2 of
si | si <= 0 -> -1e50
| otherwise ->
let al = p `SV.unsafeIndex` 0
be = p `SV.unsafeIndex` 1
xi = p `SV.unsafeIndex` 3
sdt = sqrt dt
mu = x0 + (al + be*x0)*dt
in case si*(x0**xi)*sdt of
0 -> 0
sd -> -0.5*log (2*pi*sd^2) - 0.5*((x1-mu)/sd)^2
个。由于case
永远不应该是0,所以在循环中,即使是平庸的分支预测器也应该使该分支基本上自由。
但是,我怀疑这是否会真正提高性能。与0的比较使寄存器成本,原始组件产生的组件需要较少的间接寻址,并且在需要时可以在寄存器中保留更多值。