[GHC] #14001: Inlining does not work between modules

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Keywords: | Operating System: Unknown/Multiple Architecture: | Type of failure: None/Unknown Unknown/Multiple | Test Case: | Blocked By: Blocking: | Related Tickets: Differential Rev(s): | Wiki Page: -------------------------------------+------------------------------------- Hi! I discovered today something that looks like a terrifying bug. I was discussing it on IRC and so far there is no explanation why it happens. Let's grab a test code from here: https://github.com/luna/dependent- state/tree/irc-testing (branch irc-testing). If you download it and execute `stack --stack-yaml stack-develop.yaml bench dependent-state:layered-state-benchmark` benchmarks from `test/bench/Main.hs` will be executed. Everything is compiled with `-O2` etc (see stack-develop.yaml for details). And now (everything regarding file `Control/Monad/State/Layered.hs`): 1. If you change lines `94-111` to use pointfree instead of normal arg, you get 40x slowdown 2. If you then uncomment lines `80-81`, everything is fast again. The problem is that lines `80-81` are: {{{ (.) :: (b -> c) -> (a -> b) -> a -> c (.) f g = \x -> f (g x) ; {-# INLINE (.) #-} }}} It seems that this function is inlined ONLY if is defined in this module. If we use the one from base OR if we move this function to separate module, we can observe the slowdown. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by j.waldmann): This might also depend on the (perceived) arity of the function, cf. comp1/comp2 in https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts... #inline-pragma -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:1 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by danilo2): @j.waldman I dont think so. It does not explain why `(.)` function in this module is faster than EXACTLY the same implementation in other module. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:2 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by simonpj): Is it possible to make a smaller test case? Just a couple of modules, showing where `(.)` is not inlined? (No need to make it a runnable benchmark.) Thanks -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:3 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by danilo2): Simon, this use case has 2 files - one with definition of StateT wrapper and the second with benchmark. There are 2 other files but completely commented out, sorry for not removing them. I was cleaning in hurry to discuss it on IRC. Anyway there are two source files: 1. Control/Monad/State/Layered.hs - StateT newtype wrapper 2. test/bench/Main.hs - couple lines of benchmarks showing the slowdown Is it good enough ? -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:4 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by mpickering): I managed to reproduce this. Working to produce a smaller example now but the core is certainly different in both cases. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:5 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by mpickering): Here is a smaller reproduction. I don't think a lot of the type family stuff is necessary for the reproduction. https://github.com/mpickering/musical-spork You can build it with `cabal new-build`. I noticed that the `Strict` language extension was enabled and so the definitions were not in fact equivalent. This makes the performance difference less surprising. If you modify the definition of `(.)` in `A.hs` to remove the bang then the program gets a lot slower. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:6 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by simonpj): I'm confused. In Matthew's repro case, * Function `(.)` is defined in module `A` and not exported. * So far as I can see, it is inlined at all its call sites in `A`. * The comment says that adding a bang to one of `(.)`'s arguments makes a big perf difference. That does not sound like a bug to me. Can someone explain what the problem is? -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:7 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by danilo2): @simonpj, there are few things to be explained here: 1. I did not notice that pasting the `(.)` to this file makes it strict (I forgot about the -`Strict` pragma in this file's header). 2. There are some strange things happening with performance when playing with this code, but we've described them separately, here: https://ghc.haskell.org/trac/ghc/ticket/14013 . We can in fact close this topic, because it's main description relates to the fact that the `(.)` operator was pasted into `-XStrict` code and any further discussion here would lead readers to confusion. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:8 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: closed Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: invalid | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Changes (by bgamari): * status: new => closed * resolution: => invalid -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:9 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: closed Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: invalid | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by danilo2): Ok, I've just re-visited this issue and I've got one additional question. GHC.Base has definition of `(.)` like the following : {{{ (.) :: (b -> c) -> (a -> b) -> a -> c (.) f g = \x -> f (g x) ; {-# INLINE (.) #-} }}} @simonpj You've told that making own definition of (.) with one of its arguments strict, obviously affects the performance (makes the custom (.) implementation 40x faster in some use cases). I still consider it to be GHC bug even if we can justify it by some internal compiler design. If GHC is told to inline the `(.)` definition, why inlining such code into strict code doesn't make GHC automatically discover that every parameter was already strict there? Why inlining such definition makes strict code run slow? I'm looking at it from user perspective. For me telling users to have two definitions of (.) - one strict and one lazy and using strict one in strict code and lazy one in lazy code makes Haskell so hard to understand and so hard to use. Thus I believe we should consider this bug and should expect from GHC to automatically optimize usages of (.) in strict code. If I understand correctly, what happens here is that arguments to (.) are boxed (because (.) was defined as lazy) and are unboxed as soon as passed to `f` and `g`, because they were defined as strict. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:10 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Changes (by danilo2): * status: closed => new * resolution: invalid => -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:11 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#14001: Inlining does not work between modules -------------------------------------+------------------------------------- Reporter: danilo2 | Owner: (none) Type: bug | Status: new Priority: high | Milestone: Component: Compiler | Version: 8.0.2 Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by bgamari): Regarding comment:10: In general GHC can only evaluate things which it can prove will eventually be forced by the program. Consequently if you have, {{{#!hs (.) :: (b -> c) -> (a -> b) -> a -> c (.) f g = \x -> f (g x) h = f . g }}} The performance characteristics of `h` will naturally depend upon the strictness characteristics of `f` and `g`. If both are strict in their first argument then it would be safe to first force `h`'s argument, as you do in your strict composition operator. However, if either are lazy then changing the strictness of `(.)` will change the semantics of the program, potentially resulting in an incorrectly bottom program.
Thus I believe we should consider this bug and should expect from GHC to automatically optimize usages of `(.)` in strict code.
Perhaps, but what do you consider to be "strict code"? -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/14001#comment:12 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler
participants (1)
-
GHC