记忆化乘法

10
我的应用程序在使用FFT进行(昂贵的)转换后对向量进行乘法运算。因此,当我写下

时,
f :: (Num a) => a -> [a] -> [a]
f c xs = map (c*) xs

我只想计算一次 c 的FFT,而不是每个 xs 元素都要计算。 实际上没有必要为整个程序存储 c 的FFT,只需要在本地作用域中存储即可。
我尝试定义我的 Num 实例:
data Foo = Scalar c
         | Vec Bool v -- the bool indicates which domain v is in

instance Num Foo where
    (*) (Scalar c) = \x -> case x of
                         Scalar d -> Scalar (c*d)
                         Vec b v-> Vec b $ map (c*) v
    (*) v1 = let Vec True v = fft v1
             in \x -> case x of
                    Scalar d -> Vec True $ map (c*) v
                    v2 -> Vec True $ zipWith (*) v (fft v2)

然后,在一个应用程序中,我调用类似于f的函数(它适用于任意的Num),其中c=Vec False v,我期望这将与我对f进行修改时一样快:
g :: Foo -> [Foo] -> [Foo]
g c xs = let c' = fft c
         in map (c'*) xs

函数g实现了对fft c的记忆化,比调用f要快得多(无论我如何定义(*))。我不明白f出了什么问题。是我在Num实例中定义(*)的问题吗?它是否与f在所有数字上运行有关,因此GHC无法确定如何部分计算(*)注意:我检查了我的Num实例的核心输出,并且(*)确实表示为带有FFT转换的嵌套lambda在顶层lambda中。因此,看起来至少可以对其进行记忆化。我还尝试过精明和鲁莽地使用爆炸模式来尝试强制评估,但没有效果。
另外,即使我能够找出如何使(*)记忆化其第一个参数,它的定义仍然存在另一个问题:想要使用Foo数据类型的程序员必须知道这种记忆化能力。如果她写了
map (*c) xs

没有记忆化会发生。(必须写成(map (c*) xs))。现在我想起来了,我不确定 GHC 会如何重写 (*c) 版本,因为我已经柯里化了 (*)。但我进行了一个快速测试,验证了 (*c)(c*) 都能按预期工作:(c*)c 作为第一个参数传递给 *,而 (*c)c 作为第二个参数传递给 *。所以问题是,要确保记忆化,应该如何编写乘法并不明显。这只是中缀符号(以及假设 * 的参数是对称的)固有的缺点吗?
第二个问题是,当我们将 (v*) 映射到一组标量时。在这种情况下,(希望)会计算和存储 v 的 fft,即使这是不必要的,因为另一个乘数是标量。有什么解决方法吗?
谢谢

使用 -O2 进行编译并使用已编译的代码进行基准测试? - jberryman
是的,但我确实检查了核心代码,它似乎没有编译任何东西。 - crockeea
顺便提一下,let c' = fft c in map (c*) xs 并不会计算 fft,因为 Haskell 是惰性的。c' 从未被使用,所以它永远不会被计算。 - luqui
非常正确,是我打错了。现在已经修正。 - crockeea
记录一下:GHC 7.4.2 的完整编译命令是: ghc -fno-liberate-case -optc03 -O2 -O -fllvm -funbox-strict-fields -funfolding-use-threshold1000 -funfolding-keeness-factor1000。也许其中某个标志正在干扰? - crockeea
2个回答

2

我认为stable-memo包可以解决你的问题。它通过引用标识而不是相等性来进行值的记忆化:

大多数记忆化组合器都是基于相等性进行记忆化,而stable-memo则是基于是否之前已将完全相同的参数传递给函数(即内存中的同一参数)。

并且当其键被垃圾回收时,它会自动删除已记忆化的值:

stable-memo不会保留迄今为止见过的键,这允许它们在不再使用时被垃圾回收。如果发生这种情况,将设置终结器以从记忆表中删除相应条目。

因此,如果你定义了类似于下面的东西

fft = memo fft'
  where fft' = ... -- your old definition

你将会得到你所需的大部分内容:调用 map (c *) xs 将会在第一次调用 (*) 时缓存 fft 的计算结果,并且在后续调用 (c *) 时重复使用。如果 c 被垃圾回收,那么 fft' c 也会被回收。
另请参见 这个答案,以及 如何向 ADT 添加仅缓存某些内容的字段?

我一定会尝试的,但你有没有想过为什么“相等性”在记忆化中不起作用? - crockeea
@Eric 平等没有错。只是这个库采用了不同的路径,通过比较引用在低级别上工作。虽然这可能需要依赖GHC的低级API并降低包的可移植性,但它具有一些优点。特别是它与垃圾收集的紧密集成(对于您的任务很重要)。而且您没有对可记忆类型施加任何限制 - 您可以为没有“Eq”或其他类似类型类实例的类型进行记忆化。 - Petr
我同意,所有这些东西都非常好。我的问题是:当我的“相等性”记忆化尝试失败时,为什么我应该相信这会起作用呢?或者您是在暗示它实际上并没有失败,而只是需要线性时间来使用吗?如果是这种情况,我会相信我的糟糕表现结果。 - crockeea
我相信这个想法正是我正在寻找的解决方案,但实际性能不足。使用 stable-memo 比原问题中的任何一种方法都要慢得多。有什么好的想法吗? - crockeea
@Eric 你尝试过对其进行性能分析吗?或许,你有一些可运行的代码片段可以尝试一下? - Petr
我会在有更多时间的时候尝试分析,如果我无法弄清楚,它可能会成为另一个问题。感谢您的帮助! - crockeea

1

我看到可能会阻止记忆化的两个问题:

首先,f 的类型被重载并适用于所有 Num 实例。因此,除非它被专门化(通常需要 SPECIALIZE pragma),否则 f 不能使用记忆化或内联(这可能会自动发生,但使用 INLINE pragma 更可靠)。

其次,对于 Foo(*) 定义对第一个参数执行模式匹配,但是 f 乘以一个未知的 c。因此,在 f 中,即使专门化,也无法进行记忆化。再次强调,它非常依赖于 f 被内联,并提供一个具体的 c 参数,以便内联实际上可以出现。

我认为看一下你是如何调用f会有所帮助。请注意,如果f使用两个参数定义,则必须给出两个参数,否则它无法进行内联。此外,看到Foo的实际定义也会有所帮助,因为你提供的那个定义中提到了不在作用域内的cv


我看不出任何东西会依赖于f是否被内联。我不是在尝试记忆f本身,而只是用于映射列表的函数。实际的定义是:f :: (Num a) => [a] -> [[a]] -> [a] f xs = foldl1' (zipWith (+)) . zipWith (\a b -> map (a*) b) xs我看到我错过了Foo的超出范围参数,但实际数据有5个参数和许多构造函数。我认为Foo传达了相关信息。但是,为了更正确,您可以想象我有 - crockeea
Foo = Vec Bool [Int] | Scalar IntFoo = 向量 布尔值 [整数] | 标量 整数 - crockeea
那么你期望被记忆的是 f 中的什么部分呢?(a*)吗?但正如我所说,它不可能减少这个部分,因为(1)类型太一般了,(2)我们不知道 a 的任何信息,而 (*) 的定义首先执行模式匹配。所以这完全取决于 f 是否被专门化或内联。 - kosmikus
我不想在对f的调用中进行(c*)的记忆化,我只是不想每次将其应用于列表中的项目时重新计算fft c仅限于单个f的调用。如果我恰好使用相同的c调用了f,我愿意支付两次转换成本,但不愿支付2*|xs|次。 - crockeea

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接