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

Make ntuple(f, n) and ntuple(f, Val{n}) throw ArgumentError when n < 0 #21697

Merged
merged 2 commits into from
May 13, 2017

Conversation

alyst
Copy link
Contributor

@alyst alyst commented May 4, 2017

ntuple(i -> i, -1) returns an empty Tuple{}, but ntuple(i -> i, Val{-1}) sends Julia (both 0.5 and 0.6) into an endless type inference(?) loop consuming CPU and memory (I didn't dare to wait until the end).

The fix is to return an empty tuple immediately.

Probably should be backported to 0.5 and 0.6.

@kshyatt kshyatt added the compiler:inference Type inference label May 4, 2017
@TotalVerb
Copy link
Contributor

I would personally prefer an error to an empty tuple.

@timholy
Copy link
Member

timholy commented May 4, 2017

This seems like a good idea. It looks like the branch gets elided by the compiler anyway, so there probably isn't any cost to checking this.

@alyst
Copy link
Contributor Author

alyst commented May 4, 2017

This could be an error, but for consistency it should also be an error for ntuple(f, N::Int), which would make it a breaking change w.r.t. 0.5.

@TotalVerb
Copy link
Contributor

We could avoid backporting the error for that method to 0.5 and 0.6, tolerating an inconsistency for those releases.

@tkelman
Copy link
Contributor

tkelman commented May 4, 2017

I had the same thought so tracked the non-Val version to see how long it's been that way. Apparently always 97b2e50.

@alyst
Copy link
Contributor Author

alyst commented May 4, 2017

So the consensus is to make it a DomainError, and add DomainError for ntuple(f, -1) to the master?

@alyst alyst changed the title Fix ntuple(i -> i, Val{-1}) Make ntuple(f, n) and ntuple(f, Val{n}) throw DomainError when n < 0 May 4, 2017
@alyst
Copy link
Contributor Author

alyst commented May 4, 2017

Changed it to throwing DomainError (plus separate commit for ntuple(f, -1)). AppVeyor x86 build timeout looks unrelated.

base/tuple.jl Outdated
@@ -105,7 +105,8 @@ julia> ntuple(i -> 2*i, 4)
```
"""
function ntuple(f::F, n::Integer) where F
t = n <= 0 ? () :
(n >= 0) || throw(DomainError())
Copy link
Member

@fredrikekre fredrikekre May 4, 2017

Choose a reason for hiding this comment

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

Maybe ArgumentError? A quick search through base suggests DomainError is mostly used when the value is out of domain for mathematical functions (e.g. sqrt, log etc)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would change it to ArgumentError if there would be more thumbs up than for DomainError (yours is counted) :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd leave the check over _ntuple so the branch gets delayed after the small cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

DomainError changed into ArgumentError.
I'd leave the n check where it is, because it's much cleaner than burying it inside another routine. Anyway, in performance-sensitive context one should use Val{N} alternative.

Copy link
Member

Choose a reason for hiding this comment

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

Seconding @pabloferz's recommendation. Using the Val{N} methods is not always possible, and ntuple's performance is important. Best!

Copy link
Contributor

Choose a reason for hiding this comment

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

I would be surprised if LLVM did not just hoist the branch to the end, but this change can't hurt.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've benchmarked the early and the late n check variants with

@benchmark ntuple(identity, n) setup=(n = rand(0:10))

The late one (inside _ntuple()) was 1ns (~10%) faster, so I've updated the PR.

@Sacha0
Copy link
Member

Sacha0 commented May 4, 2017

@nanosoldier runbenchmarks(ALL, vs = ":master")

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @jrevels

@alyst alyst changed the title Make ntuple(f, n) and ntuple(f, Val{n}) throw DomainError when n < 0 Make ntuple(f, n) and ntuple(f, Val{n}) throw ArgumentError when n < 0 May 5, 2017
@alyst
Copy link
Contributor Author

alyst commented May 5, 2017

Probably the CI has to be rerun. Travis got stuck (no output for 10 minutes) in one configuration, AppVeyor failed to install the package in one configuration.

@alyst
Copy link
Contributor Author

alyst commented May 7, 2017

Travis failed for xcode8 in 1 test of "spawn", but it doesn't look like PR-related. All other test configurations were ok.

@alyst alyst closed this May 8, 2017
@alyst alyst reopened this May 8, 2017
@alyst
Copy link
Contributor Author

alyst commented May 8, 2017

The CI tests pass from the 3rd attempt. 🎉

@Sacha0
Copy link
Member

Sacha0 commented May 8, 2017

@nanosoldier runbenchmarks(ALL, vs = ":master")

@@ -142,6 +146,7 @@ ntuple(f, ::Type{Val{15}}) = (@_inline_meta; (f(1), f(2), f(3), f(4), f(5), f(6)

function ntuple(f::F, ::Type{Val{N}}) where {F,N}
Core.typeassert(N, Int)
(N >= 0) || throw(ArgumentError(string("tuple length should be ≥0, got ", N)))
Copy link
Member

Choose a reason for hiding this comment

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

I imagine this branch is handled at compile time where N is known at compile time?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the theory. That's also what I see with @code_llvm ntuple(identity, Val{-1})

Copy link
Member

Choose a reason for hiding this comment

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

And also I hope with e.g. @code_llvm ntuple(identity, Val{1})? :)

Copy link
Contributor Author

@alyst alyst May 8, 2017

Choose a reason for hiding this comment

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

Sure. Actually, for each 0<=N<=15 there's explicit ntuple(identity, Val{N}) method. The check for N<0 is in a generic method.
In fact, ntuple(f, Val{N}) is substantially slower for N>=16. On the 11 days old master (b838f2e)

julia> @benchmark ntuple(identity, Val{15})
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     3.365 ns (0.00% GC)
  median time:      3.379 ns (0.00% GC)
  mean time:        3.400 ns (0.00% GC)
  maximum time:     16.001 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     1000

julia> @benchmark ntuple(identity, Val{16})
BenchmarkTools.Trial: 
  memory estimate:  288 bytes
  allocs estimate:  2
  --------------
  minimum time:     418.864 ns (0.00% GC)
  median time:      423.166 ns (0.00% GC)
  mean time:        448.653 ns (2.32% GC)
  maximum time:     6.745 μs (87.57% GC)
  --------------
  samples:          10000
  evals/sample:     199

Few nanoseconds could be explained by Base.@_inline_meta absence, but not hundreds.

Copy link
Member

Choose a reason for hiding this comment

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

Sure. Actually, for each 0<=N<=15 there's explicit ntuple(identity, Val{N}) method. The check for N<0 is in a generic method.

Ah, right! Wonseok's recent change. Thanks for reminding me. For @code_llvm ntuple(identity, Val{16}) then :).

Copy link
Member

Choose a reason for hiding this comment

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

Following up --- the branch gets handled at compile time for e.g. @code_llvm ntuple(identity, Val{16})? Assuming that holds (and perhaps even if it doesn't, if the marginal cost of the branch is low) the present incarnation lgtm! :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, apparently, LLVM understands that the check is a constant expression and eliminates it.
The nanosoldier regressions look like noise to me given their high variation between the runs.

@alyst
Copy link
Contributor Author

alyst commented May 8, 2017

Hmm, I was using the following benchmark to estimate the overhead of checking n in the most common situations (the test should never be triggered and _ntuple_xxx() never visited):

_ntuple_before(f, n) = (Base.@_noinline_meta; ([f(i) for i = 1:n]...))

function ntuple_before(f::F, n::Integer) where F
    (n >= 0) || throw(ArgumentError(string("tuple length should be ≥0, got ", n)))
    t = n == 0  ? () :
        n == 1  ? (f(1),) :
        n == 2  ? (f(1), f(2)) :
        n == 3  ? (f(1), f(2), f(3)) :
        n == 4  ? (f(1), f(2), f(3), f(4)) :
        n == 5  ? (f(1), f(2), f(3), f(4), f(5)) :
        n == 6  ? (f(1), f(2), f(3), f(4), f(5), f(6)) :
        n == 7  ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7)) :
        n == 8  ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8)) :
        n == 9  ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8), f(9)) :
        n == 10 ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8), f(9), f(10)) :
        _ntuple_before(f, n)
    return t
end

function _ntuple_after(f, n)
    Base.@_noinline_meta
    (n >= 0) || throw(ArgumentError(string("tuple length should be ≥0, got ", n)))
    ([f(i) for i = 1:n]...)
end

function ntuple_after(f::F, n::Integer) where F
    t = n == 0  ? () :
        n == 1  ? (f(1),) :
        n == 2  ? (f(1), f(2)) :
        n == 3  ? (f(1), f(2), f(3)) :
        n == 4  ? (f(1), f(2), f(3), f(4)) :
        n == 5  ? (f(1), f(2), f(3), f(4), f(5)) :
        n == 6  ? (f(1), f(2), f(3), f(4), f(5), f(6)) :
        n == 7  ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7)) :
        n == 8  ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8)) :
        n == 9  ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8), f(9)) :
        n == 10 ? (f(1), f(2), f(3), f(4), f(5), f(6), f(7), f(8), f(9), f(10)) :
        _ntuple_after(f, n)
    return t
end

using BenchmarkTools

@benchmark ntuple_before(identity, n) setup=(n = rand(0:10))
@benchmark ntuple_after(identity, n) setup=(n = rand(0:10))

But the results behave reproducibly different in different Julia sessions.
Sometimes the early n>=0 check is faster:

Version 0.6.0-pre.beta.367 (2017-04-27 14:08 UTC)
Commit b838f2eec6 (11 days old master)

julia> @benchmark ntuple_before(identity, n) setup=(n = rand(0:10))
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     7.714 ns (0.00% GC)
  median time:      9.419 ns (0.00% GC)
  mean time:        9.758 ns (0.00% GC)
  maximum time:     63.276 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     999

julia> @benchmark ntuple_after(identity, n) setup=(n = rand(0:10))
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     8.712 ns (0.00% GC)
  median time:      10.426 ns (0.00% GC)
  mean time:        10.613 ns (0.00% GC)
  maximum time:     44.453 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     999

Sometimes the late one:

julia> @benchmark ntuple_before(identity, n) setup=(n = rand(0:10))
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     8.378 ns (0.00% GC)
  median time:      10.099 ns (0.00% GC)
  mean time:        10.364 ns (0.00% GC)
  maximum time:     66.474 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     999

julia> @benchmark ntuple_after(identity, n) setup=(n = rand(0:10))
BenchmarkTools.Trial: 
  memory estimate:  0 bytes
  allocs estimate:  0
  --------------
  minimum time:     8.044 ns (0.00% GC)
  median time:      9.738 ns (0.00% GC)
  mean time:        9.867 ns (0.00% GC)
  maximum time:     63.647 ns (0.00% GC)
  --------------
  samples:          10000
  evals/sample:     999

@Sacha0
Copy link
Member

Sacha0 commented May 8, 2017

Perhaps benchmarking without dependence on random variables would be worthwhile, or at least worth excluding as a potential source of inconsistency?

@Sacha0
Copy link
Member

Sacha0 commented May 8, 2017

Thanks for going the distance by the way! :) Performance tuning of ntuple and friends is super finicky.

@timholy
Copy link
Member

timholy commented May 8, 2017

Few nanoseconds could be explained by Base.@_inline_meta absence, but not hundreds.

You're probably hitting

tupletype_len::Int = 15,
. We may also want to introduce the Any16 fallback like we use for map, just to prevent compile time from exploding. (Try ntuple(identity, Val{1000}) to see what I mean.) That will, of course, slow down the runtime, but it prevents catastrophic behavior.

@alyst
Copy link
Contributor Author

alyst commented May 8, 2017

@timholy Yes, from @code_llvm output for the generic ntuple() it looks like this: f is not inlined anymore and there's jl_f_tuple() call for N>=16.

@nanosoldier
Copy link
Collaborator

Your benchmark job has completed - possible performance regressions were detected. A full report can be found here. cc @jrevels

@JeffBezanson JeffBezanson added breaking This change will break code error handling Handling of exceptions by Julia or the user bugfix This change fixes an existing bug and removed compiler:inference Type inference breaking This change will break code labels May 9, 2017
@alyst
Copy link
Contributor Author

alyst commented May 10, 2017

It could be merged without squashing the commits. c6f458e (ntuple(f, Val{N}) fix) should be safe to backport to 0.5 and 0.6.

Copy link
Member

@Sacha0 Sacha0 left a comment

Choose a reason for hiding this comment

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

The present incarnation lgtm! :)

Perhaps @pabloferz could have another look?

Copy link
Contributor

@pabloferz pabloferz left a comment

Choose a reason for hiding this comment

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

LGTM too

@Sacha0
Copy link
Member

Sacha0 commented May 12, 2017

It could be merged without squashing the commits. c6f458e (ntuple(f, Val{N}) fix) should be safe to backport to 0.5 and 0.6.

@tkelman, squash or no? Best!

@martinholters
Copy link
Member

The individual commits are all ok, so I'd say no squashing is necessary, and without squashing, we at least keep the option of an easy backport.

alyst added 2 commits May 13, 2017 01:33
otherwise Julia would stuck in an endless type inference loop
@alyst
Copy link
Contributor Author

alyst commented May 12, 2017

Rebased to resolve conflicts with the updated NEWS.md (no julia code touched). 2b4bb1a (ntuple(f, Val{N}) fix) could be backported to 0.6.

@alyst
Copy link
Contributor Author

alyst commented May 13, 2017

@Sacha0 should be ready to go :)

@Sacha0
Copy link
Member

Sacha0 commented May 13, 2017

Following martinholters suggestion and merging without squash. Thanks for seeing this through @alyst! :)

@Sacha0 Sacha0 merged commit b51b42e into JuliaLang:master May 13, 2017
tkelman pushed a commit that referenced this pull request May 15, 2017
otherwise Julia would stuck in an endless type inference loop

(cherry picked from commit 2b4bb1a)
ref #21697
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking This change will break code bugfix This change fixes an existing bug error handling Handling of exceptions by Julia or the user
Projects
None yet
Development

Successfully merging this pull request may close these issues.