Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When to use various language pragmas and optimisations?

I have a fair bit of understanding of haskell but I am always little unsure about what kind of pragmas and optimizations I should use and where. Like

  • Like when to use SPECIALIZE pragma and what performance gains it has.
  • Where to use RULES. I hear people taking about a particular rule not firing? How do we check that?
  • When to make arguments of a function strict and when does that help? I understand that making argument strict will make the arguments to be evaluated to normal form, then why should I not add strictness to all function arguments? How do I decide?
  • How do I see and check I have a space leak in my program? What are the general patterns which constitute to a space leak?
  • How do I see if there is a problem with too much lazyness? I can always check the heap profiling but I want to know what are the general cause, examples and patterns where lazyness hurts?

Is there any source which talks about advanced optimizations (both at higher and very low levels) especially particular to haskell?

like image 449
Satvik Avatar asked Sep 21 '12 16:09

Satvik


1 Answers

Like when to use SPECIALIZE pragma and what performance gains it has.

You let the compiler specialise a function if you have a (type class) polymorphic function, and expect it to be called often at one or a few instances of the class(es).

The specialisation removes the dictionary lookup where it is used, and often enables further optimisation, the class member functions can often be inlined then, and they are subject to strictness analysis, both give potentially huge performance gains. If the only optimisation possible is the elimination of the dicitonary lookup, the gain won't generally be huge.

As of GHC-7, it's probably more useful to give the function an {-# INLINABLE #-} pragma, which makes its (nearly unchanged, some normalising and desugaring is performed) source available in the interface file, so the function can be specialised and possibly even inlined at the call site.

Where to use RULES. I hear people taking about a particular rule not firing? How do we check that?

You can check which rules have fired by using the -ddump-rule-firings command line option. That usually dumps a large number of fired rules, so you have to search a bit for your own rules.

You use rules

  • when you have a more efficient version of a function for special types, e.g.

    {-# RULES
    "realToFrac/Float->Double"  realToFrac   = float2Double
      #-}
    
  • when some functions can be replaced with a more efficient version for special arguments, e.g.

    {-# RULES
    "^2/Int"        forall x. x ^ (2 :: Int) = let u = x in u*u
    "^3/Int"        forall x. x ^ (3 :: Int) = let u = x in u*u*u
    "^4/Int"        forall x. x ^ (4 :: Int) = let u = x in u*u*u*u
    "^5/Int"        forall x. x ^ (5 :: Int) = let u = x in u*u*u*u*u
    "^2/Integer"    forall x. x ^ (2 :: Integer) = let u = x in u*u
    "^3/Integer"    forall x. x ^ (3 :: Integer) = let u = x in u*u*u
    "^4/Integer"    forall x. x ^ (4 :: Integer) = let u = x in u*u*u*u
    "^5/Integer"    forall x. x ^ (5 :: Integer) = let u = x in u*u*u*u*u
      #-}
    
  • when rewriting an expression according to general laws might produce code that's better to optimise, e.g.

    {-# RULES
    "map/map"  forall f g. (map f) . (map g) = map (f . g)
      #-}
    

Extensive use of RULES in the latter style is made in fusion frameworks, for example in the text library, and for the list functions in base, a different kind of fusion (foldr/build fusion) is implemented using rules.

When to make arguments of a function strict and when does that help? I understand that making argument strict will make the arguments to be evaluated to normal form, then why should I not add strictness to all function arguments? How do I decide?

Making an argument strict will ensure that it is evaluated to weak head normal form, not to normal form.

You do not make all arguments strict because some functions must be non-strict in some of their arguments to work at all and some are less efficient if strict in all arguments.

For example partition must be non-strict in its second argument to work at all on infinite lists, more general every function used in foldr must be non-strict in the second argument to work on infinite lists. On finite lists, having the function non-strict in the second argument can make it dramatically more efficient (foldr (&&) True (False:replicate (10^9) True)).

You make an argument strict, if you know that the argument must be evaluated before any worthwhile work can be done anyway. In many cases, the strictness analyser of GHC can do that on its own, but of course not in all.

A very typical case are accumulators in loops or tail recursions, where adding strictness prevents the building of huge thunks on the way.

I know no hard-and-fast rules for where to add strictness, for me it's a matter of experience, after a while you learn in what places adding strictness is likely to help and where to harm.

As a rule of thumb, it makes sense to keep small data (like Int) evaluated, but there are exceptions.

How do I see and check I have a space leak in my program? What are the general patterns which constitute to a space leak?

The first step is to use the +RTS -s option (if the programme was linked with rtsopts enabled). That shows you how much memory was used overall, and you can often judge by that whether you have a leak. A more informative output can be obtained from running the programme with the +RTS -hT option, that produces a heap profile that can help locating the space leak (also, the programme needs to be linked with enabled rtsopts).

If further analysis is required, the programme needs to be compiled with profiling enabled (-rtsops -prof -fprof-auto, in older GHCs, the -fprof-auto option wasn't available, the -prof-auto-all option is the closest correspondence there).

Then you run it with various profiling options and look at the generated heap profiles.

The two most common causes for space leaks are

  • too much laziness
  • too much strictness

the third place is probably taken by unwanted sharing, GHC does little common subexpression elimination, but it occasionally shares long lists even where not wanted.

For finding the cause of a leak, I know again no hard-and-fast rules, and occasionally, a leak can be fixed by adding strictness in one place or by adding laziness in another.

How do I see if there is a problem with too much lazyness? I can always check the heap profiling but I want to know what are the general cause, examples and patterns where lazyness hurts?

Generally, laziness is wanted where results can be built up incrementally, and unwanted where no part of the result can be delivered before processing is complete, like in left folds or generally in tail-recursive functions.

like image 143
Daniel Fischer Avatar answered Oct 30 '22 10:10

Daniel Fischer