Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve evaluation of nested ComposedFunctions #45925

Merged
merged 4 commits into from
Jul 13, 2022

Conversation

martinholters
Copy link
Member

@martinholters martinholters commented Jul 4, 2022

  • Add (the equivalent of) @assume_effects :terminates_globally to unwrap_composed. Although it could be inferred as Const, without the annotation, it was not elided for too complex inputs, resulting in unnecessary runtime overhead.
    EDIT: now effects: relax recursion detection for effects analysis #45993 is merged and this part isn't necessary.
  • Reverse recursion order in call_composed. This prevents potentially changing argument types being piped through the recursion, making inference bail out. With the reversed order, only the tuple of remaining functions is changing during recursion and is becoming strictly simpler, letting inference succeed.
julia> f = reduce(, fill(sin,500));

julia> const g = reduce(, fill(sin,500));

julia> @time f(1.0)
  1.474751 seconds (1.83 M allocations: 120.273 MiB, 3.22% gc time, 99.00% compilation time) # master
  6.011575 seconds (4.02 M allocations: 278.036 MiB, 3.33% gc time, 100.00% compilation time) # PR
0.07698641344541407

julia> @btime f(1.0)
  14.102 ms (1000 allocations: 15.62 KiB) # master
  5.337 μs (1 allocation: 16 bytes) # PR
0.07698641344541407

julia> @btime g(1.0)
  14.091 ms (998 allocations: 15.59 KiB) # master
  1.878 ns (0 allocations: 0 bytes) # PR
0.07698641344541407

So increase in compilation time is noticeable, but execution is also much faster. (Yes, we can const-prop and completely elide g here). Also note that compilation time is still much better than with 1.8-rc1 (or 1.7), where it is several minutes.

That said, the fact that we have to replace the concise (c::ComposedFunction)(x...; kw...) = c.outer(c.inner(x...; kw...)) with the convoluted construction still makes me sad.

Fixes #45877.

base/operators.jl Outdated Show resolved Hide resolved
base/operators.jl Outdated Show resolved Hide resolved
@aviatesk
Copy link
Member

aviatesk commented Jul 5, 2022

You can test this improvement as like:

test/operators.jl

include("compiler/irutils.jl")
const issue_45877 = reduce(, fill(sin,500))
@test fully_eliminated() do
    issue_45877(1.0)
end

@martinholters martinholters force-pushed the mh/improve-composed-eval branch 2 times, most recently from 6582429 to 07ffbaf Compare July 6, 2022 07:32
@martinholters
Copy link
Member Author

@aviatesk can you explain why the @assume_effects is necessary? On master I find

julia> const g = reduce(, fill(sin,500));

julia> only(code_typed() do
           Base.unwrap_composed(g)
       end)[2]
NTuple{500, typeof(sin)}

julia> Base.infer_effects() do
           Base.unwrap_composed(g)
       end
(+c,+e,+n,!t,+s)

So inference succeeds which implies that it follows the recursion to the end, therefore obviously has figured out that there is an end. But the effects don't seem to catch that. Is there some way we could make the effects here more precise? If we could avoid the @assume_effects, there would also no longer be a need for the code movement.

@aviatesk
Copy link
Member

aviatesk commented Jul 7, 2022

While debugging #45781, I found that the root problem there was that :terminates effect seems to be tainted too aggressively by recursion and type information and effect information are not synced in terms of when they are cached or refined.

I think we are tainting the effect too aggressively here:

elseif edgecycle
# Some sort of recursion was detected. Even if we did not limit types,
# we cannot guarantee that the call will terminate
effects = Effects(effects; terminates=ALWAYS_FALSE)

since edgecycle basically means there are calls of same Method, but still their MethodInstances can be different.
We can sync them more if we taint the terminates effect by detecting recursion per MethodInstance basis. So basically we need to implement such analysis (maybe reusing a part of the similar type-level analysis for interprocedural recursion detection).

Copy link
Member

@aviatesk aviatesk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can try to improve the recursion detection for effect analysis instead, but I'm also fine with merging this for now and tackle the improvement later.

@martinholters
Copy link
Member Author

I don't mind either way. I won't have the bandwidth to look at improving effect analysis anytime soon, don't know whether you can tackle it in the foreseeable future. If no, we probably should merge as is. But I have no personal interest in deeply nested composed functions, so no hurry from my side.

@aviatesk aviatesk self-assigned this Jul 8, 2022
@ianatol
Copy link
Member

ianatol commented Jul 8, 2022

LGTM.

I think the @assume_effects is fine for the time being, while Shuhei looks into improving the recursion detection for effects.

@aviatesk aviatesk force-pushed the mh/improve-composed-eval branch from 07ffbaf to 7006603 Compare July 12, 2022 04:22
@aviatesk aviatesk changed the base branch from master to avi/recursion-effects July 12, 2022 04:22
Base automatically changed from avi/recursion-effects to master July 12, 2022 09:09
martinholters and others added 4 commits July 12, 2022 18:10
* Add (the equivalent of) `@assume_effects :terminates_globally` to
  `unwrap_composed`. Although it could be inferred as `Const`, without
  the annotation, it was not elided for too complex inputs, resulting in
  unnecessary runtime overhead.
* Reverse recursion order in `call_composed`. This prevents potentially
  changing argument types being piped through the recursion, making
  inference bail out. With the reversed order, only the tuple of
  remaining functions is changing during recursion and is becoming
  strictly simpler, letting inference succeed.
Motivation is to be able to use `@assume_effects` by including that file
at an appropriate position during bootstrap.
This reverts commit bb5ea07319a47d325b57b7b62c561bd6fa325838.
@aviatesk aviatesk force-pushed the mh/improve-composed-eval branch from 7006603 to 5b7ec82 Compare July 12, 2022 09:10
@aviatesk aviatesk added the merge me PR is reviewed. Merge when all tests are passing label Jul 12, 2022
@aviatesk aviatesk merged commit 531b1b9 into master Jul 13, 2022
@aviatesk aviatesk deleted the mh/improve-composed-eval branch July 13, 2022 01:32
@DilumAluthge DilumAluthge removed the merge me PR is reviewed. Merge when all tests are passing label Jul 17, 2022
ffucci pushed a commit to ffucci/julia that referenced this pull request Aug 11, 2022
* ~~Add (the equivalent of) `@assume_effects :terminates_globally` to
  `unwrap_composed`. Although it could be inferred as `Const`, without
  the annotation, it was not elided for too complex inputs, resulting in
  unnecessary runtime overhead.~~
  EDIT: now JuliaLang#45993 is merged and this part isn't included.
* Reverse recursion order in `call_composed`. This prevents potentially
  changing argument types being piped through the recursion, making
  inference bail out. With the reversed order, only the tuple of
  remaining functions is changing during recursion and is becoming
  strictly simpler, letting inference succeed.

Co-authored-by: Shuhei Kadowaki <aviatesk@gmail.com>
pcjentsch pushed a commit to pcjentsch/julia that referenced this pull request Aug 18, 2022
* ~~Add (the equivalent of) `@assume_effects :terminates_globally` to
  `unwrap_composed`. Although it could be inferred as `Const`, without
  the annotation, it was not elided for too complex inputs, resulting in
  unnecessary runtime overhead.~~
  EDIT: now JuliaLang#45993 is merged and this part isn't included.
* Reverse recursion order in `call_composed`. This prevents potentially
  changing argument types being piped through the recursion, making
  inference bail out. With the reversed order, only the tuple of
  remaining functions is changing during recursion and is becoming
  strictly simpler, letting inference succeed.

Co-authored-by: Shuhei Kadowaki <aviatesk@gmail.com>
vtjnash added a commit that referenced this pull request Oct 25, 2022
This was written to be a very difficult compiler stress test, and be
nearly close to failing at runtime too. While we like tests to be
comprehensive, we do not like them to be a stress test on unrelated
parts of the compiler simultaneously.

From: #45925
Fix: #47179
aviatesk pushed a commit that referenced this pull request Oct 25, 2022
This was written to be a very difficult compiler stress test, and be
nearly close to failing at runtime too. While we like tests to be
comprehensive, we do not like them to be a stress test on unrelated
parts of the compiler simultaneously.

From: #45925
Fix: #47179
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Deeply composed functions much slower than equivalent static functions
5 participants