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

Expand documentation on Val{T} #15729

Merged
merged 3 commits into from
Apr 21, 2016
Merged

Expand documentation on Val{T} #15729

merged 3 commits into from
Apr 21, 2016

Conversation

timholy
Copy link
Member

@timholy timholy commented Apr 1, 2016

An alternative to #15725.

@eschnett
Copy link
Contributor

eschnett commented Apr 1, 2016

Thank you.

A = fill(5.0, (3, 3))

This approach works very well: the compiler can figure out that ``A``
is an ``Array{Float64,2}`` because it know the type of the fill value
Copy link
Member

Choose a reason for hiding this comment

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

"knows" I think.

@vtjnash
Copy link
Member

vtjnash commented Apr 1, 2016

I think I would rather use this example as a demonstration of why you shouldn't make a type that depends on a value since it leads to further complication of the API later. But I agree there are a number of optimizations missing for NTuple that need to be addressed.

@timholy
Copy link
Member Author

timholy commented Apr 1, 2016

Meaning, Array{T,N} should become Array{T}? Refs:
JuliaLang/LinearAlgebra.jl#42
#11339

It's a hard road, though, because so much depends on knowing the dimensionality. Almost all of our indexing rules, for example. Which happen to be extraordinarily performance-sensitive.

@@ -1237,24 +1237,15 @@ If you apply :func:`supertype` to other type objects (or non-type objects), a
julia> supertype(Union{Float64,Int64})
ERROR: `supertype` has no method matching supertype(::Type{Union{Float64,Int64}})

.. _man-val-trick:

"Value types"
-------------

As one application of these ideas, Julia includes a parametric type,
Copy link
Contributor

Choose a reason for hiding this comment

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

Never understood what kind of "these ideas" are being referred to. I guess many people who just followed the _man-val-trick link have the same experience.
Also, I find it a little bit confusing that Val{T} is introduced as some sort of Julia type system quirk, whereas in fact it's just an empty immutable, and the same effect could be achieved with any user-defined type. Maybe it's worth clarifying.

@alyst
Copy link
Contributor

alyst commented Apr 1, 2016

Based on my experience of reading this section I would suggest a few things.

  • Maybe the title could be changed from "Val{} trick" into something like "Converting run-time parameters into compile-time ones"
  • The discussion of the example is nice, but since it's a mixture of do-s and don't-s, a summary of recommendations would be highly beneficial:
    • Why one might want to convert runtime parameter into compile-time (type stability, staged functions?)
    • When such conversion should be done (as early as possible in the call tree, absolutely not in the loop?)
  • Is foo(Val{:bar}) really that slow? I thought, once compiled, it should be as fast as bar(). If it's really slow, maybe it's worth explaining why or providing the link to the relevant discussion of the dispatch mechanism.

@vtjnash
Copy link
Member

vtjnash commented Apr 2, 2016

Converting run-time parameters into compile-time ones

that's not really meaningful. there is a ordering of events such that runtime occurs after compile-time

foo(Val{:bar})

it's not particularly slow, but foobar() would be equivalent and more readable. b = :bar; foo(Val{b}) otoh, will be on the order of hundreds to thousands of times slower – so the performance cliff here is very bad and may be fairly unpredictable.

Why one might want to convert runtime parameter into compile-time (type stability, staged functions?)
When such conversion should be done (as early as possible in the call tree, absolutely not in the loop?)

design the api so it isn't necessary

@eschnett
Copy link
Contributor

eschnett commented Apr 2, 2016

On Sat, Apr 2, 2016 at 12:28 PM, Jameson Nash [email protected]
wrote:

Converting run-time parameters into compile-time ones

that's not really meaningful. there is a ordering of events such that
runtime occurs after compile-time

The compiler's optimizer can optimize based on compile-time constants, but
not run-time constants. A function taking a Val argument can be orders of
magnitudes faster than with a run-time parameter.

foo(Val{:bar})

it's not particularly slow, but foobar() would be equivalent and more
readable. b = :bar; foo(Val{b}) otoh, will be on the order of hundreds to
thousands of times slower – so the performance cliff here is very bad and
may be fairly unpredictable.

Why one might want to convert runtime parameter into compile-time (type
stability, staged functions?)
When such conversion should be done (as early as possible in the call
tree, absolutely not in the loop?)

design the api so it isn't necessary

This isn't an API question; functionally, passing Val{x} and x are
equivalent. The point here is performance.

One of the reasons I'm using Julia (and not CUDA, C++, and Python) is that
I can control exactly how the code for my performance-critical loops looks.
What compilers produce by default is usually just not good enough. I'm
currently using Mathematica (another Lispy language) to generate C++ or
CUDA code, then maybe another optimizer for memory access patterns, and
then a regular compiler that is basically reduced to a register allocator
and instruction selector. I wish this all worked automatically with a
clever compiler, but it doesn't. And I understand the reasons why it
doesn't work, and why one can't expect that the next round of compiler
optimizations will cure this.

Originally I came to Julia for the macros, because they are much easier to
use than generating C++ code in Mathematica. Then I found out about
generated functions, which are even more convenient to use. Val is just
one other important ingredient. Tongue in cheek: I don't care if it takes
an hour to compile the function that takes a Val argument, and to create
the Val type object, and do the dynamic dispatch; that's still on par
with the development cycle for Mathematica, C++, and ahead-of-time
compiling.

-erik

Erik Schnetter [email protected]
http://www.perimeterinstitute.ca/personal/eschnetter/

@alyst
Copy link
Contributor

alyst commented Apr 4, 2016

This isn't an API question; functionally, passing Val{x} and x are
equivalent. The point here is performance.

Actually, in many cases, after using Val{T}, I later realized that it would make perfect sense to embed T into an already existing type or convey the same information by subtyping. Using "normal" types somewhat reduces the chances that Julia dispatch would be abused.
So, I would say, if one is about to use "Val{T} trick", s/he has to double check whether a more conventional approach would be possible.

@timholy
Copy link
Member Author

timholy commented Apr 20, 2016

Thanks for the review comments, everyone. Very helpful. I'll merge unless additional concerns are raised.

Also adds numerous tweaks in response to review comments.
@timholy timholy merged commit 72ba87a into master Apr 21, 2016
@timholy timholy deleted the teh/val_manual branch April 21, 2016 17:57
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.

6 participants