JuliaHEP / JuliaHEP-2023

Materials for the JuliaHEP 2023 Workshop
https://juliahep.github.io/JuliaHEP-2023/
Creative Commons Attribution 4.0 International
4 stars 4 forks source link

reduction #15

Closed Moelf closed 10 months ago

Moelf commented 10 months ago

fix #13, #14

github-actions[bot] commented 10 months ago

PR Preview Action v1.4.4 :---: :rocket: Deployed preview to https://JuliaHEP.github.io/JuliaHEP-2023/pr-preview/pr-15/ on branch gh-pages at 2023-10-24 08:47 UTC

aoanla commented 10 months ago

Some thoughts:

I think the contracted tables of operations are good - although I see you decided to keep "backwards division" in, despite it being a little subtle compared to the others. (I wouldn't object to removing it, thus resolving #13 )

I'm in two minds about removing the "typeclass" variable (restricted to ::Number) - I see your point, but it might be nice to have some example there that shows use of abstract types (maybe just an example of the supertype() or <: operators on some typed values?).

I'm okay with the change to broadcasting to use iseven(), since as you say it lets us demonstrate ? on a Julia library function to show how good it is, even if I miss the "even"/"odd" strings!

I think if we also want to resolve #12 we could easily remove the Compact Functions bit from the functions and control flow notebook, unless @graeme-a-stewart has a position on this?

Moelf commented 10 months ago

I'm in two minds about removing the "typeclass" variable (restricted to ::Number) - I see your point, but it might be nice to have some example there that shows use of abstract types (maybe just an example of the supertype() or <: operators on some typed values?).

I think for practical audience you just need:

Moelf commented 10 months ago

the whole \circ thing is cute but not so useful IMO, can we put the anonymous function into the main notebook and delete the rest?

aoanla commented 10 months ago

the whole \circ thing is cute but not so useful IMO, can we put the anonymous function into the main notebook and delete the rest?

By "the whole \circ thing", do you mean the entire discussion of function composition in Julia? If so, I'm not sure why you don't think it's useful - it's pretty significant that Julia will optimise composed functions as "entire units", especially since it means that it's much more efficient to broadcast (or map or whatever) over a composition. It especially motivates a Julia pattern of not avoiding "decomposing" complex functions into smaller reusable subfunctions, since composition will always allow the result to be optimised without additional function call overhead.

@graeme-a-stewart @tamasgal opinions in either direction?

Moelf commented 10 months ago

function composition just means you can pass a function as an argument to another function. It doesn't surprise Python users. \circ is cute but can be confusing (another distraction), it has use cases but it's not a main thing they need to know about Julia, and doesn't come with any advantage in terms of performance or composability.

Likewise, I wouldn't mention >(3) is functionally same as x -> x > 3, even though I would use it day to day, but it's not important to know if you're just getting started


Honestly I'd much rather cover do() syntax, like

map(ary) do x
   ...
end

# reminds Python users of their with
open("file.txt", "w") do f
   println(f, "hello world")
end

cd(temp_dir) do
   read(`ls`)
end

cuz it's used much more often than \circ, and has implications in terms of capture or whatever.

aoanla commented 10 months ago

So, \circ is different to just calling a function in a function (because "composed functions" are JITed as an entity - so once you do f \circ g once, the entity "f \circ g" is optimised and cached by the JIT, meaning that it's efficient to reuse it - compared to f(g(.)), which is not, and will be re-JITed each time). Indeed, that's the point of the @code_llvm example at the bottom of the function composition example - twice \circ thrice is demonstrably different in terms of the resulting llvm IR to twice(thrice()) - and yes, the native code produced looks different too. This probably will be surprising to python users.

(I do agree about do notation - I was considering making the "function composition" notebook a "cool function things" notebook with composition + do notation in it, but this was the minimum viable product)

Moelf commented 10 months ago

So, \circ is different to just calling a function in a function (because "composed functions" are JITed as an entity - so once you do f \circ g once, the entity "f \circ g" is optimised and cached by the JIT, meaning that it's efficient to reuse it - compared to f(g(.)), which is not, and will be re-JITed each time).

I guess I don't know what do you mean here -- Julia doesn't "re-jet", in fact, when you call f() inside g(), f() is often compiled before g() even starts:

julia> @generated function g(x)
           println("this is compile time for g($x)")
           return zero(x)
       end
g (generic function with 1 method)

julia> @generated function f(x)
           println("this is compile time for f($x)")
           return one(x)
       end
f (generic function with 1 method)

julia> function main(x)
           @info 1
           z = f(x)
           @info 2
           g(z)
       end
main (generic function with 1 method)

julia> main(3)
this is compile time for f(Int64)
this is compile time for g(Int64)
[ Info: 1
[ Info: 2
0

you might be saying something technically true, but I don't think saying one JIT more the other JIT less is accurate.


The LLVM/Native code difference doesn't mean \circ is better, in fact, it's probably doing extra stuff to pull functions out of the ComposedFunction{...} struct, which definitely doesn't make things faster.

Moelf commented 10 months ago

If you look at the ComposedFunction call implementation: https://github.com/JuliaLang/julia/blob/0be0b389e300c05abde796f68abbdd95b938f7ad/base/operators.jl#L1041-L1045

it just recursively call each element of the Tuple{...} functions, and you can easily have type inference failure, which has been spotted in the past:

aoanla commented 10 months ago

Hm, it seems you're right - despite the fact that @code_llvm and @code_native insist that the generated code is different (and, notably, elides the internal function call in the \circ composed version, producing more optimised code...), the performance characteristics seem to be basically identical for a number of test cases I just played with

[Which might mean that @code_native and @code_llvm are not quite telling the truth? I'd expect a function call to be noticeable!]

That said, it's still faster than generating a new anonymous function for use in a map or something (that is: map( f \circ g, array) is faster than map( x-> f(g(x), array), especially after the first time, because Julia does compile a new anonymous function each time here, but just keeps around the composed function object once you use it once). Not sure if that's worth the notebook though?

Moelf commented 10 months ago

yeah I would maybe call those "performance gotcha", not super important and likely to be case-by-case I think?

aoanla commented 10 months ago

Aha, I understand what's happening with @code_foo - it's the curse of the JIT liking to optimise things inside function bodies. (That is: h(x) = f(g(x) and h(x) = (f \circ g)(x) produce identical @code_llvm output, so the problem is that the introspection is "lying" about function composition that's "bare" because it doesn't reflect what actually happens on invocation.)

aoanla commented 10 months ago

Anyway, my suggestion is that we either:

completely remove extra "function composition" notebook (and rewrite the mandelbrot set broadcasting example to use a chain of broadcast function calls - which also leads into the "broadcasting is efficient" discussion after it)

or

replace it with a notebook discussing do() notation (and also probably rewrite the mandelbrot set example as above)

Moelf commented 10 months ago

ready for review @aoanla

I replaced the notebook content (and name in _toc) with anonymous function and do-block, also fixed the Mandelbrot to use broadcast and took the chance to tell audience about broadcast fusion.

graeme-a-stewart commented 10 months ago

Hi - thanks for all of these improvements! Covering do-block is a great thing as it's very Julian - it confused me the first time I saw it until I found out what was going on.