Optimization Haskell中的有限差分,或如何禁用潜在优化

Optimization Haskell中的有限差分,或如何禁用潜在优化,optimization,haskell,numerical-methods,rounding-error,Optimization,Haskell,Numerical Methods,Rounding Error,我想实现以下简单(一阶)有限差分函数: finite_difference :: Fractional a => a -> (a -> a) -> a -> a finite_difference h f x = ((f $ x + h) - (f x)) / h 您可能知道,有一个微妙的问题:必须确保(x+h)和x之间有一个可精确表示的数字。否则,结果会产生巨大的错误,因为(f$x+h)-(f x)涉及灾难性的取消(人们必须仔细选择h,但这不是我的问题) 在C或

我想实现以下简单(一阶)有限差分函数:

finite_difference :: Fractional a => a -> (a -> a) -> a -> a
finite_difference h f x = ((f $ x + h) - (f x)) / h
您可能知道,有一个微妙的问题:必须确保
(x+h)
x
之间有一个可精确表示的数字。否则,结果会产生巨大的错误,因为
(f$x+h)-(f x)
涉及灾难性的取消(人们必须仔细选择
h
,但这不是我的问题)

在C或C++中,问题可以这样解决:

volatile double temp = x + h;
h = temp - x;
而且
volatile
修饰符禁用了与变量
temp
相关的任何优化,因此我们确信“聪明”的编译器不会优化掉这两行

我对哈斯凯尔的了解还不够,还不知道如何解决这个问题。恐怕

let temp = x + h
    hh = temp - x 
in ((f $ x + hh) - (f x)) / h
将通过Haskell(或其使用的后端)进行优化。如何在这里获得与
volatile
等效的值(如果可能的话,不牺牲惰性)?我不介意GHC的具体答案。

我不这么认为

temp = unsafePerformIO $ return $ x + h

将得到优化。只是一个猜测。

一种方法是看核心

专门化为双倍的
(这种情况最有可能触发一些优化):

汇编为:

A.$wfinite_difference h f x =
    case f (case x of
                  D# x' -> D# (+## x' (-## (+## x' h) x'))
           ) of 
        D# x'' -> case f x of D# y -> /## (-## x'' y) h
同样地,对于多态版本(重写更少)

因此,虽然变量是内联的,但数学并没有优化。
除了查看核心之外,我想不出一个方法来保证您想要的财产。

我有两个解决方案和一个建议:

第一个解决方案:您可以保证不会使用两个帮助器函数和NOINLINE pragma优化此功能:

norm1 x h = x+h
{-# NOINLINE norm1 #-}

norm2 x tmp = tmp-x
{-# NOINLINE norm2 #-}

normh x h = norm2 x (norm1 x h)
这是可行的,但成本很低

第二种解决方案:使用volatile在C中编写规范化函数,并通过FFI调用它。性能惩罚将是最小的


现在我们来看一个建议:目前数学还没有优化,所以它目前可以正常工作。你担心它会在未来的编译器中崩溃。我认为这不太可能,但也不太可能,我不想对此加以防范。因此,请编写一些单元测试,涵盖有问题的案例。那么,如果它将来真的会破裂(无论出于什么原因),你就会知道确切的原因。

为什么牺牲懒惰是一个问题?@Don:事实上,可能不是。我计划在
h
来自惰性无限列表的上下文中,以相当人为的方式使用它(例如,使用理查森外推例程)。但就这个特殊的函数而言,你是对的,我并不这么认为(我正在努力熟悉这门语言)。是的,但这有一个缺点,即在未来的编译器版本中可能会引入更积极的优化。所以,用“黑盒”的方法来防止优化是很好的。唯一的黑盒方法是添加{-#GHC#U选项-Onot}谢谢,但我使用的是GHC 7.02,我打赌LLVM后端会对此进行优化(可能有一个“坚持IEEE数学”选项,但我不想在整个库中激活它)@don:-Onot非常有攻击性。我只想禁用两个特定行的优化。编译器执行的任何类型的转换都不尊重浮点数的语义,这是一个bug。你不必做任何事情来获得正确的代码。我很担心LLVM后端会优化数学(当然,如果给我一些“快速数学”选项,我想启用,并仅为这两行禁用)。C中的易失性变量可以完成这项工作,因为可以保证每次需要时都会从内存中读取该变量。但我不想编写C函数,因为我需要
分数a
(而不仅仅是
双精度
)类型。因此,编写一个
NOINLINE
函数(只有一个函数应该足够)确实解决了我的问题。GHC的编译策略是这样的,LLVM或GCC可能不会通过绑定以任何不安全的方式优化数学。如果NOINLINE的函数是多态的且没有导出,我相信LLVM或GCC不会看穿它。不过,我对单元测试的建议仍然有效。
norm1 x h = x+h
{-# NOINLINE norm1 #-}

norm2 x tmp = tmp-x
{-# NOINLINE norm2 #-}

normh x h = norm2 x (norm1 x h)