10

My application multiplies vectors after a (costly) conversion using an FFT. As a result, when I write

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

I only want to compute the FFT of c once, rather than for every element of xs. There really isn't any need to store the FFT of c for the entire program, just in the local scope.

I attempted to define my Num instance like:

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)

Then, in an application, I call a function similar to f (which works on arbitrary Nums) where c=Vec False v, and I expected that this would be just as fast as if I hack f to:

g :: Foo -> [Foo] -> [Foo]
g c xs = let c' = fft c
         in map (c'*) xs

The function g makes the memoization of fft c occur, and is much faster than calling f (no matter how I define (*)). I don't understand what is going wrong with f. Is it my definition of (*) in the Num instance? Does it have something to do with f working over all Nums, and GHC therefore being unable to figure out how to partially compute (*)?

Note: I checked the core output for my Num instance, and (*) is indeed represented as nested lambdas with the FFT conversion in the top level lambda. So it looks like this is at least capable of being memoized. I have also tried both judicious and reckless use of bang patterns to attempt to force evaluation to no effect.

As a side note, even if I can figure out how to make (*) memoize its first argument, there is still another problem with how it is defined: A programmer wanting to use the Foo data type has to know about this memoization capability. If she wrote

map (*c) xs

no memoization would occur. (It must be written as (map (c*) xs)) Now that I think about it, I'm not entirely sure how GHC would rewrite the (*c) version since I have curried (*). But I did a quick test to verify that both (*c) and (c*) work as expected: (c*) makes c the first arg to *, while (*c) makes c the second arg to *. So the problem is that it is not obvious how one should write the multiplication to ensure memoization. Is this just an inherent downside to the infix notation (and the implicit assumption that the arguments to * are symmetric)?

The second, less pressing issue is that the case where we map (v*) onto a list of scalars. In this case, (hopefully) the fft of v would be computed and stored, even though it is unnecessary since the other multiplicand is a scalar. Is there any way around this?

Thanks

crockeea
  • 21,651
  • 10
  • 48
  • 101
  • compiling with `-O2` and benchmarking with compiled code? – jberryman Feb 26 '13 at 03:53
  • Yes, but I did check the core, and it doesn't seem to be compiling anything away. – crockeea Feb 26 '13 at 03:54
  • BTW, `let c' = fft c in map (c*) xs` does *not* compute an `fft`, because Haskell is lazy. `c'` is never used, so it will never be computed. – luqui Feb 26 '13 at 04:02
  • Very true, a typo on my part. Fixed now. – crockeea Feb 26 '13 at 04:03
  • For the record: the full compile line for GHC 7.4.2 is: ghc -fno-liberate-case -optc03 -O2 -O -fllvm -funbox-strict-fields -funfolding-use-threshold1000 -funfolding-keeness-factor1000. Perhaps one of these other flags is interfering? – crockeea Feb 26 '13 at 05:02

2 Answers2

2

I believe stable-memo package could solve your problem. It memoizes values not using equality but by reference identity:

Whereas most memo combinators memoize based on equality, stable-memo does it based on whether the exact same argument has been passed to the function before (that is, is the same argument in memory).

And it automatically drops memoized values when their keys are garbage collected:

stable-memo doesn't retain the keys it has seen so far, which allows them to be garbage collected if they will no longer be used. Finalizers are put in place to remove the corresponding entries from the memo table if this happens.

So if you define something like

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

you'll get pretty much what you need: Calling map (c *) xs will memoize the computation of fft inside the first call to (*) and it gets reused on subsequent calls to (c *). And if c is garbage collected, so is fft' c.

See also this answer to How to add fields that only cache something to ADT?

Community
  • 1
  • 1
Petr
  • 62,528
  • 13
  • 153
  • 317
  • I will definitely give it a shot, but do you have any idea why "equality" isn't working for memoization? – crockeea Feb 26 '13 at 14:56
  • @Eric There is nothing wrong with equality. It's just that this library took a different path and works on a low level,by comparing references. While this probably requires relying on GHC's low-level API and reduces the package's portability, it has some advantages. In particular, its tight integration with garbage collection (which is important for your task). And you don't have any constraints on memoizable types - you can memoize types for which you don't have an instance of `Eq` or another similar type class. – Petr Feb 26 '13 at 15:32
  • I agree, all of these things are very nice. My question was: why should I believe this will work, when my "equality" memoization attempt failed? Or are you suggesting that it in fact did *not* fail, but that it was just taking linear time to use? I would believe my poor performance results if this was the case. – crockeea Feb 26 '13 at 15:36
  • I believe this idea is exactly the solution I'm looking for, however practical performance is lacking. Using stable-memo is much slower than either approach in the original question. Ideas? – crockeea Feb 26 '13 at 18:12
  • @Eric Have you tried profiling it? Perhaps, do you have some piece of runnable code to play with? – Petr Feb 26 '13 at 20:57
  • I'll try profiling when I get some more time, it will probably end up as a different question if I can't figure it out. Thanks for your help! – crockeea Feb 26 '13 at 23:14
1

I can see two problems that might prevent memoization:

First, f has an overloaded type and works for all Num instances. So f cannot use memoization unless it is either specialized (which usually requires a SPECIALIZE pragma) or inlined (which may happen automatically, but is more reliable with an INLINE pragma).

Second, the definition of (*) for Foo performs pattern matching on the first argument, but f multiplies with an unknown c. So within f, even if specialized, no memoization can occur. Once again, it very much depends on f being inlined, and a concrete argument for c to be supplied, so that inlining can actually appear.

So I think it'd help to see how exactly you're calling f. Note that if f is defined using two arguments, it has to be given two arguments, otherwise it cannot be inlined. It would furthermore help to see the actual definition of Foo, as the one you are giving mentions c and v which aren't in scope.

kosmikus
  • 19,549
  • 3
  • 51
  • 66
  • I can't see how anything could depend on f being inlined or not. I'm not trying to memoize f itself, just the function I'm using to map over the list. The actual def is: f :: (Num a) => [a] -> [[a]] -> [a] f xs = foldl1' (zipWith (+)) . zipWith (\a b -> map (a*) b) xs I see that I missed the out-of-scope parameters on Foo, but the real data has 5 parameters and as many constructors. I think Foo conveys the relevant information. To be more correct about it, however, you can imagine I have – crockeea Feb 26 '13 at 15:01
  • `Foo = Vec Bool [Int] | Scalar Int`. – crockeea Feb 26 '13 at 15:07
  • So what about your `f` do you expect to be memoized? The `(a*)`? But as I said, it cannot possibly reduce this, as (1) the type is too general, and (2) we do not know anything about `a`, whereas the definition of `(*)` first performs a pattern match. So it all depends on `f` being specialized or inlined. – kosmikus Feb 26 '13 at 15:19
  • I don't want to memoize `(c*)` *over* calls to `f`, I just don't want to recompute `fft c` every time I apply it to an item in the list *for a single call of f*. If I happened to call `f` with the same `c`, I am willing to pay the conversion cost twice, just not `2*|xs|` times. – crockeea Feb 26 '13 at 15:25