为啥乘法只在一侧短路
Posted
技术标签:
【中文标题】为啥乘法只在一侧短路【英文标题】:Why does multiplication only short circuit on one side为什么乘法只在一侧短路 【发布时间】:2016-07-03 03:50:22 【问题描述】:我在搞乱fix
,搞砸之后我遇到了一些奇怪的行为,即0 * undefined
是*** Exception: Prelude.undefined
和undefined * 0
是0
。这也意味着fix (0 *)
是*** Exception: <<loop>>
而fix (* 0)
是0
。
在玩弄它之后,似乎原因是因为让它在两个方向上都短路并不是一件容易的事,因为这并没有多大意义,没有某种奇怪的并行计算并从第一个非底部返回。
这种事情是否在其他地方看到过(反身函数对底值不反身),我可以放心地依赖它吗?还有一种实用的方法可以使 (0 *)
和 (* 0)
无论传入的值如何都计算为零。
【问题讨论】:
什么?太棒了! 这正是使得denotational semantics 不完全等同于operational semantics,即不完全抽象。前者可以用f undefined 0 = 0
和f 0 undefined = 0
表示函数f
,而后者不能。语言实现遵循操作语义,因此无法在没有一些技巧的情况下定义这样的f
。
【参考方案1】:
你的推理是正确的。有一个 unamb
包为您所指的那种并行计算提供工具。事实上,它提供了Data.Unamb.pmult
,它并行地尝试检查每个操作数是 1 还是 0,如果是,则立即产生结果。对于简单的算术,这种并行方法在大多数情况下可能要慢得多!
(*)
的短路仅在 GHC 版本 7.10 中发生。这是由于该 GHC 版本中 Integer
类型的实现发生了变化。这种额外的惰性通常被视为一个性能错误(因为它会干扰严格性分析,甚至在理论上可能导致空间泄漏),因此它将在 GHC 8.0 中删除。
【讨论】:
好像是 7.10 had several surprising "features"。 :// 你能解释一下性能错误部分吗?当然,如果您知道其中一个值为 0,那么从性能的角度来看,对其进行评估也是没有意义的。到时候就不能把它扔进深渊吗? @semicolon,GHC 使用严格性分析(特别是它称为需求分析的版本)来确定一个值何时会总是或从不评估。了解其中任何一个对于优化都非常有用。有时(例如,使用&&
)用户期望并依赖短路行为。在许多其他情况下,特殊情况下的节省并不能开始弥补其他情况下的损失。
@dfeuer 啊,我明白了。所以如果总是需要它,它可以在 GHC 想执行它的时候执行,并且不会有任何不必要的 thunk 积累?
@semicolon,类似的,是的。我不知道太多细节,但是 GHC 可以使用它,例如,在许多情况下(有效地)用foldl'
替换foldl
,因为如果它知道它必须执行一个操作,它还不如只执行一个操作最终。如果它发现某些东西从不被使用,它也可以避免为它分配空间。【参考方案2】:
其实fix (* 0) == 0
似乎只对Integer
有效,如果你运行fix (* 0) :: Double
或fix (* 0) :: Int
,你仍然会得到***Exception <<loop>>
这是因为在instance Num Integer
中,(*)
被定义为(*) = timesInteger
timesInteger
在Data.Integer
中定义
-- | Multiply two 'Integer's
timesInteger :: Integer -> Integer -> Integer
timesInteger _ (S# 0#) = S# 0#
timesInteger (S# 0#) _ = S# 0#
timesInteger x (S# 1#) = x
timesInteger (S# 1#) y = y
timesInteger x (S# -1#) = negateInteger x
timesInteger (S# -1#) y = negateInteger y
timesInteger (S# x#) (S# y#)
= case mulIntMayOflo# x# y# of
0# -> S# (x# *# y#)
_ -> timesInt2Integer x# y#
timesInteger x@(S# _) y = timesInteger y x
-- no S# as first arg from here on
timesInteger (Jp# x) (Jp# y) = Jp# (timesBigNat x y)
timesInteger (Jp# x) (Jn# y) = Jn# (timesBigNat x y)
timesInteger (Jp# x) (S# y#)
| isTrue# (y# >=# 0#) = Jp# (timesBigNatWord x (int2Word# y#))
| True = Jn# (timesBigNatWord x (int2Word# (negateInt# y#)))
timesInteger (Jn# x) (Jn# y) = Jp# (timesBigNat x y)
timesInteger (Jn# x) (Jp# y) = Jn# (timesBigNat x y)
timesInteger (Jn# x) (S# y#)
| isTrue# (y# >=# 0#) = Jn# (timesBigNatWord x (int2Word# y#))
| True = Jp# (timesBigNatWord x (int2Word# (negateInt# y#)))
看上面的代码,如果你运行(* 0) x
,那么timesInteger _ (S# 0#)
会匹配,所以x
不会被计算,而如果你运行(0 *) x
,那么在检查timesInteger _ (S# 0#)
是否匹配时,x将被评估并导致无限循环
我们可以使用下面的代码来测试它:
module Test where
import Data.Function(fix)
-- fix (0 ~*) == 0
-- fix (~* 0) == ***Exception<<loop>>
(~*) :: (Num a, Eq a) => a -> a -> a
0 ~* _ = 0
_ ~* 0 = 0
x ~* y = x ~* y
-- fix (0 *~) == ***Exception<<loop>>
-- fix (*~ 0) == 0
(*~) :: (Num a, Eq a) => a -> a -> a
_ *~ 0 = 0
0 *~ _ = 0
x *~ y = x *~ y
在 GHCI 中还有一些更有趣的东西:
*Test> let x = fix (* 0)
*Test> x
0
*Test> x :: Double
*** Exception: <<loop>>
*Test>
【讨论】:
嗯,最后一个只是NoMonomorphismRestriction
的组合,默认。你剩下的答案是伟大的侦探工作。
@dfeuer 虽然你说得对,但我仍然认为它非常有趣。如果你是一个非常坏的人,你可以返回任意的Num a => a
,但是当用作Integer
以外的任何东西时,它会失败。只需执行fix (* 0) + retVal
。【参考方案3】:
以下面为例
(if expensiveTest1 then 0 else 2) * (if expensiveTest2 then 0 else 2)
你必须选择一方来评估。如果expensiveTest2
是一个无限循环,你永远无法判断右边是不是0
,所以你无法判断是否短路右边,所以你永远看不到左侧。您无法同时检查双方是否为0
。
至于你是否可以依靠短路来以某种方式行动,只要记住undefined
和error
就像一个无限循环一样完全只要你不这样做' t 使用 IO。因此,您可以使用undefined
和error
测试短路和惰性。通常,短路行为因功能而异。 (也有不同程度的懒惰。undefined
和Just undefined
可能会给出不同的结果。)
更多详情请见this。
【讨论】:
以上是关于为啥乘法只在一侧短路的主要内容,如果未能解决你的问题,请参考以下文章