为啥乘法只在一侧短路

Posted

技术标签:

【中文标题】为啥乘法只在一侧短路【英文标题】:Why does multiplication only short circuit on one side为什么乘法只在一侧短路 【发布时间】:2016-07-03 03:50:22 【问题描述】:

我在搞乱fix,搞砸之后我遇到了一些奇怪的行为,即0 * undefined*** Exception: Prelude.undefinedundefined * 00。这也意味着fix (0 *)*** Exception: <<loop>>fix (* 0)0

在玩弄它之后,似乎原因是因为让它在两个方向上都短路并不是一件容易的事,因为这并没有多大意义,没有某种奇怪的并行计算并从第一个非底部返回。

这种事情是否在其他地方看到过(反身函数对底值不反身),我可以放心地依赖它吗?还有一种实用的方法可以使 (0 *)(* 0) 无论传入的值如何都计算为零。

【问题讨论】:

什么?太棒了! 正是使得denotational semantics 不完全等同于operational semantics,即不完全抽象。前者可以用f undefined 0 = 0f 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) :: Doublefix (* 0) :: Int,你仍然会得到***Exception <<loop>>

这是因为在instance Num Integer 中,(*) 被定义为(*) = timesInteger

timesIntegerData.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 =&gt; a,但是当用作Integer 以外的任何东西时,它会失败。只需执行fix (* 0) + retVal【参考方案3】:

以下面为例

(if expensiveTest1 then 0 else 2) * (if expensiveTest2 then 0 else 2)

你必须选择一方来评估。如果expensiveTest2是一个无限循环,你永远无法判断右边是不是0,所以你无法判断是否短路右边,所以你永远看不到左侧。您无法同时检查双方是否为0

至于你是否可以依靠短路来以某种方式行动,只要记住undefinederror 就像一个无限循环一样完全只要你不这样做' t 使用 IO。因此,您可以使用undefinederror 测试短路和惰性。通常,短路行为因功能而异。 (也有不同程度的懒惰。undefinedJust undefined 可能会给出不同的结果。)

更多详情请见this。

【讨论】:

以上是关于为啥乘法只在一侧短路的主要内容,如果未能解决你的问题,请参考以下文章

POJ3613 Cow Relays 最短路+矩阵乘法

为啥这个 SIMD 乘法不比非 SIMD 乘法快?

verilog 为啥乘法器写的那么复杂? 不是这样写也可以吗 assign c = a * b;

为啥乘法、加法的霓虹内在函数比运算符慢?

Logistic 回归模型的参数估计为啥不能采用最小二乘法

为啥 GPU 做矩阵乘法的速度比 CPU 快?