Does Julia recompile if the redefinition of the function is the same

Say I have a function foo(x) that takes a long time to compile.

Suppose I have already defined the function and called it once so that it is compiled. Then say I redefined foo(x) with the exact same definition and then call it again with the exact same value.

Will I incur a compilation penalty on the second call? Or would Julia only recompile it if the definition changed?

foo will be recompiled, but doing so will be pretty cheap since all the things foo calls will already be compiled.

2 Likes

But then everything that calls foo will have to be recompiled too right? Which could be rather expensive?

The behavior you describe is correct for lambdas.

Example
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.8.0 (2022-08-17)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time foo=x->x^2
  0.014332 seconds (300 allocations: 18.181 KiB)
#1 (generic function with 1 method)

julia> @time bar=x->foo(x)+1
  0.000072 seconds (27 allocations: 1.703 KiB)
#3 (generic function with 1 method)

julia> @time bar(2)
  0.016165 seconds (2.67 k allocations: 169.455 KiB, 99.59% compilation time)
5

julia> @time bar(2)
  0.000026 seconds
5

julia> @time foo=x->x^2
  0.000090 seconds (26 allocations: 1.681 KiB)
#5 (generic function with 1 method)

julia> @time bar(2)
  0.006600 seconds (794 allocations: 45.651 KiB, 99.48% compilation time)
5

julia> @time bar(2)
  0.000022 seconds
5

julia> @time foo=x->x^2
  0.000084 seconds (25 allocations: 1.649 KiB)
#7 (generic function with 1 method)

julia> @time bar(2)
  0.007130 seconds (793 allocations: 45.573 KiB, 99.21% compilation time)
5

I don’t know what sort of black magic is going on here, but it seems like named functions don’t work that way.

Example
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.8.0 (2022-08-17)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time foo(x)=x^2
  0.014250 seconds (300 allocations: 18.181 KiB)
foo (generic function with 1 method)

julia> @time bar(x)=foo(x)+1
  0.000057 seconds (27 allocations: 1.703 KiB)
bar (generic function with 1 method)

julia> @time bar(2)
  0.000002 seconds
5

julia> @time foo(x)=x^2
  0.000061 seconds (22 allocations: 1.337 KiB)
foo (generic function with 1 method)

julia> @time bar(2)
  0.000002 seconds
5

julia> @time foo(x)=x^3
  0.000061 seconds (21 allocations: 1.306 KiB)
foo (generic function with 1 method)

julia> @time bar(2)
  0.000002 seconds
9

julia> @time foo(x)=x÷2
  0.000066 seconds (22 allocations: 1.297 KiB)
foo (generic function with 1 method)

julia> @time bar(2)
  0.000002 seconds
2

julia> @time foo(x)=x/2
  0.000046 seconds (22 allocations: 1.297 KiB)
foo (generic function with 1 method)

julia> @time bar(2)
  0.000011 seconds
2.0

Oops! Looks like I needed to use eval to capture the compile time for named functions. I keep forgetting.

julia> @time eval(:( foo(x)=x/2 ))
  0.000696 seconds (144 allocations: 7.516 KiB)
foo (generic function with 1 method)

julia> @time eval(:( bar(2) ))
  0.010132 seconds (8.55 k allocations: 491.102 KiB, 97.21% compilation time: 100% of which was recompilation)
2.0

Wait could you surmise what you are talking about here. I am afraid I don’t follow.

I was mistaken by my poor measurement technique. Yes, everything needs to be recompiled, whether you’re using lambdas or named functions.

Example
               _
   _       _ _(_)_     |  Documentation: https://docs.julialang.org
  (_)     | (_) (_)    |
   _ _   _| |_  __ _   |  Type "?" for help, "]?" for Pkg help.
  | | | | | | |/ _` |  |
  | | |_| | | | (_| |  |  Version 1.8.0 (2022-08-17)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/                   |

julia> @time eval(:( foo(x)=x^2 ));
  0.021142 seconds (1.67 k allocations: 116.417 KiB, 29.08% compilation time)

julia> @time eval(:( bar(x)=foo(x)+2 ));
  0.000943 seconds (157 allocations: 8.344 KiB)

julia> @time eval(:( baz(x)=foo(x)*2 ));
  0.000943 seconds (156 allocations: 8.312 KiB)

julia> @time eval(:( bar(2) )) # compiles on first run
  0.007557 seconds (2.39 k allocations: 265.521 KiB, 96.17% compilation time)
6

julia> @time eval(:( bar(2) ))
  0.000302 seconds (44 allocations: 2.062 KiB)
6

julia> @time eval(:( baz(2) )) # compiles on first run
  0.006778 seconds (918 allocations: 53.476 KiB, 95.69% compilation time)
8

julia> @time eval(:( baz(2) ))
  0.000442 seconds (44 allocations: 2.062 KiB)
8

julia> @time eval(:( foo(x)=x^2 )); # redefine foo
  0.000947 seconds (149 allocations: 7.837 KiB)

julia> @time eval(:( bar(2) )) # compiles again
  0.007283 seconds (1.70 k allocations: 97.704 KiB, 96.38% compilation time: 100% of which was recompilation)
6

julia> @time eval(:( bar(2) ))
  0.000269 seconds (44 allocations: 2.062 KiB)
6

julia> @time eval(:( baz(2) )) # compiles again
  0.007036 seconds (909 allocations: 52.616 KiB, 96.33% compilation time: 100% of which was recompilation)
8

julia> @time eval(:( baz(2) ))
  0.000327 seconds (44 allocations: 2.062 KiB)
8

The black magic:

julia> @macroexpand @time 1+1
quote
    #= timing.jl:252 =#
    begin
        #= timing.jl:257 =#
        $(Expr(:meta, :force_compile))

So it forces compilation before timing.
Which is why you need to use the evals, to prevent it from being able to compile ahead of time.

@eval expr is often easier than eval(:(expr)).

1 Like

Thank you kind sir!

Of course it’s in the help string and I was too foolish to read it.