简体   繁体   中英

Tracing/Benchmarking Haskell Functions

What are the common ways of tracing Haskell function calls for debugging (or benchmarking) purposes? I am currently using the Debug.Trace module but I'm looking for something more deterministic. It preferably shouldn't account for the optimiser since I want to test the raw algorithmic performance.

I'd also love to learn about some of the more "formal" methods such as tracking the calls with a writer monad (but I'd prefer not having to resort to formal proofs since they're a little bit unreliable when it comes to predicting behaviours in GHC). Thanks!

Here's an example of how I'm currently testing my implementations:

import Debug.Trace ( trace )

heavyComputation :: Int -> Int
heavyComputation x = trace "Called" (x+1)

impl1 :: [Int]
impl1 = trace "Impl1" [heavyComputation i | i <- [0..5]]

impl2 :: [Int]
impl2 = trace "Impl2" (let n = heavyComputation 0
                       in [i+n | i <- [0..5]])

It'd be nice to have something like this:

INPUT: impl2
OUTPUT: [heavyComputation 0, addition 0 1,addition 1 1,addition 2 1,addition 3 1,addition 4 1,addition 5 1]

As I noted in a comment, trace does what you need. The result is affected by laziness, but so is everything in Haskell, since that is how evaluation works. You can get the output you asked for, for example like this:

import Debug.Trace ( trace )

heavyComputation :: Int -> Int
heavyComputation x = trace ("heavyComputation " ++ show x) (x+1)

plus :: Int -> Int -> Int
plus x y = trace ("addition " ++ show x ++ " " ++ show y) (x + y)

impl1 :: [Int]
impl1 = trace "Impl1" [heavyComputation i | i <- [0..5]]

impl2 :: [Int]
impl2 = trace "Impl2" (let n = heavyComputation 0
                       in [plus i n | i <- [0..5]])

tryBoth :: IO ()
tryBoth = do
    print $ sum impl1
    print $ sum impl2

Which gives the expected output

Main> tryBoth
Impl1
heavyComputation 0
heavyComputation 1
heavyComputation 2
heavyComputation 3
heavyComputation 4
heavyComputation 5
21
Impl2
heavyComputation 0
addition 0 1
addition 1 1
addition 2 1
addition 3 1
addition 4 1
addition 5 1
21

If you try it again, you just get the answers, since the values of impl1 and impl2 are memoized and won't be recomputed.

*Main> tryBoth
21
21

The print $ sum is there to force the result of the computation, but there are other ways to do that as well.

If we just force the spine of the list with print $ length impl2 , it will enter the function, but won't compute any actual sums, nor the heavyComputation

*Main> tryBoth
Impl1
6
Impl2
6

If we just look at the last element of the list with print $ last impl2 , it will perform a single addition and perform the heavyComputation once.

*Main> tryBoth
Impl1
heavyComputation 5
6
Impl2
heavyComputation 0
addition 5 1
6

As you can see, the behaviour is completely deterministic and predictable, but it does indeed take a while to get used to reasoning about laziness.

Maybe this post can help as well? https://www.well-typed.com/blog/2017/09/visualize-cbn/ It does cover another way to trace the execution of a program, by gradually transforming the source code of the program in a manner that is more or less equivalent to how the actual program would execute.


As a final note, there is also an evaluation trace tool in ghci , which can be activated by eg :step impl2 , but it is not as helpful as you might expect.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM