在Haskell中,哪一个XOR被认为更好

时间:2013-07-08 19:34:04

标签: haskell optimization

我想知道Haskell中最标准的方法是什么。

第一个明确指出我们需要两个参数(大多数时候)。

第二个涉及第二个子句中的函数调用(id),因此效率较低,因为在第一个实现中我们可以简单地返回第二个参数。

所以我倾向于认为第一个更好,应该是一个选择:更容易阅读和弄清楚它做什么[1],以及函数调用保存。

但我是Haskell的新手,也许编译器会优化这个额外的调用。

xor :: Bool -> Bool -> Bool

xor True x = not x
xor False x = x

xor True = not
xor False = id

此外,我想知道我是否可以用通配符替换False

那么,Haskell的好习惯是什么?也许是另一种实施方式?

[1]我们在那里省略它是一个众所周知的功能,让我们想象它是一个非平凡的功能。

由于

3 个答案:

答案 0 :(得分:14)

为了便于阅读,我会尝试避免模式匹配,并使用单个方程定义函数,该方程表达要定义的函数的有趣内容。这并不总是可行,但对于这个例子,有很多选择:

  • xor = (/=)
  • xor a b = a /= b
  • xor a b = not (a == b)
  • xor a b = (a && not b) || (not a && b)
  • xor a b = (a || b) && not (a && b)
  • xor a b = odd (fromEnum a + fromEnum b)

答案 1 :(得分:12)

当然这取决于编译器和传递给编译器的选项。

对于这个特定的例子,如果你编译没有优化,GHC会在你编写代码时生成代码,所以第二个版本包含对id resp的调用。到not。这比第一个版本效率稍低,第一个版本只包含对not的调用:

Xors.xor1 :: GHC.Types.Bool -> GHC.Types.Bool -> GHC.Types.Bool
[GblId, Arity=2]
Xors.xor1 =
  \ (ds_dkm :: GHC.Types.Bool) (x_aeI :: GHC.Types.Bool) ->
    case ds_dkm of _ {
      GHC.Types.False -> x_aeI;
      GHC.Types.True -> GHC.Classes.not x_aeI
    }

Xors.xor2 :: GHC.Types.Bool -> GHC.Types.Bool -> GHC.Types.Bool
[GblId, Arity=1]
Xors.xor2 =
  \ (ds_dki :: GHC.Types.Bool) ->
    case ds_dki of _ {
      GHC.Types.False -> GHC.Base.id @ GHC.Types.Bool;
      GHC.Types.True -> GHC.Classes.not
    }

(调用仍然在生成的程序集中,但核心更具可读性,所以我只发布它。)

但是通过优化,两个函数都编译到同一个核心(从而编译到同一个机器代码),

Xors.xor2 =
  \ (ds_dkf :: GHC.Types.Bool) (eta_B1 :: GHC.Types.Bool) ->
    case ds_dkf of _ {
      GHC.Types.False -> eta_B1;
      GHC.Types.True ->
        case eta_B1 of _ {
          GHC.Types.False -> GHC.Types.True;
          GHC.Types.True -> GHC.Types.False
        }
    }

GHC eta-扩展了第二个版本并内联了对idnot的调用,您获得了纯粹的模式匹配。

第二个等式是否使用False或通配符在任一版本中都没有区别,无论是否有优化。

  

也许编译器会优化这个额外的调用。

如果你要求它进行优化,在这种简单的情况下,GHC将消除额外的呼叫。

  

让我们想象它是一个非平凡的功能。

这是一个可能的问题。如果代码非常简单,编译器可能无法消除通过定义函数而引入的所有调用,而不是提供所有参数。 GHC相当擅长这样做和内联调用,所以你需要相当多的非平凡性来使GHC无法在编译代码时消除对它所知道的简单函数的调用(它当然不会内联对函数的内联调用)不知道编译相关模块时的实现。)

如果是关键代码,请始终检查编译器生成的代码,对于GHC,相关标志为-ddump-simpl以获得优化后生成的核心,-ddump-asm获取生成的程序集。

答案 2 :(得分:4)

  

所以我倾向于认为第一个更好,应该是选择的人:更容易阅读并弄清楚它的作用

我同意可读性。然而,第二个是非常惯用的Haskell,对于有经验的程序员来说更容易阅读:不执行那些微不足道的eta减少是非常可疑的,并且实际上可能会分散注意力。因此,对于优化版本,我宁愿以明确的形式完整地写出来:

True  `xor` False = True
False `xor` True  = True
_     `xor` _     = False

但是,如果这种替代方案的可读性远远低于最惯用的方法,那么您应该考虑不替换,但添加提示以便编译器仍然可以将其优化为理想版本。正如Daniel Fischer所证明的那样,GHC本身非常聪明,并且经常在没有帮助的情况下正确行事;如果没有,可能有助于添加一些INLINE和/或RULES pragma。要弄清楚如何才能获得最佳性能并不容易,但编写快速Haskell98代码也是如此。