World Age Problem Explanation

Hello.

Recently I have faced the “world age problem” several times. That means, after searching for my problems, this website came up with themes like “world age workaround” or “How to Bypass the World Age Problem”.
I am not a computer scientist, so maybe this is a stupid question, but I have not found a explanation of this problem; where can I get information what actually happens there? Or, more clearly, what is the “World Age Problem”?
As you can imagine, if I google this, I get a lot of information about our Earth’s age…

I am not absolutely sure, if this topic fits the “Usage” category. So if I should have decided another part of this board, I apologize for this.

Thank you in advance.
LS64

7 Likes

How to Bypass the World Age Problem does have some information
Seems to be to do with redeclaring functions and/or the order they are defined in, and using eval / @eval

Thank you for this information. I’ve read some pages and maybe, I get an idea.

But I need a starting point to understand this issue completely and want to be sure, that I’ve understood this correctly. Can someone please revise or confirm the following?

Every compilation of a function gets a “time stamp”, here known as world age. If I change dependencies to already compiled functions in a new world and execute my code, I get these errors? (In my case, these functions are parts of packages (e.g. Plots), which heightens the level of complexity to me.)
So, in the end, it is precompilation/recompilation problem, which I can solve by using eval, because @eval forces a new compilation in the latest world (by letting jl_toplevel_eval_flex() decide again, what to do)?

The reason for worlds is because of optimizations. If functions can be defined (and redefined) at arbitrary points (and instantly take effect), then that would limit the possible optimizations that can be made. Therefore, new functions are only made visible to the system (become usable) after hitting toplevel. If you try to call a new function before this, then you get a world error.

In some cases (like if you are writing a REPL), you do actually want to call your newly defined function without hitting toplevel. This can be done by using Base.invokelatest(f, args...) but it will of course prevent certain optimizations in the scope where f is called (it can pretty much not be reasoned about at all, so no inlining or inference).

23 Likes

Check out "running in world age X, while current world is Y" errors for another explanation.

2 Likes

You might also want to share a minimal working example (MWE) of your code. In my experience, world age issues are rather uncommon and perhaps you are using eval in cases where you shouldn’t.

3 Likes

Thank you very much for your answer, kristoffer.carlsson. This helps me a lot.

I will take a closer look at this whole theme on the weekend; especially at your link, mbauman.

If errors related to world age continue to appear, I will let you know and share my code. At the moment, I just want to understand the point in general.

Thank you all!

1 Like

I would add that requiring modifications to dispatch to take effect immediately is one of those classic behaviors in dynamic languages that makes them hard/impossible to compile without heroic efforts (along with reified local scopes, the ability to add state to objects dynamically at any point, etc. – we should make a list some time). In Julia we’re nipping that one in the bud by defining the language to work in a way that can be implemented in the presence of compilation without insane contortions, namely with world ages that only take effect at the top-level, just the way that @kristoffer.carlsson described.

13 Likes

Seel also here for a detailed explanation, and here for an explanation of invokelatest.

It would be nice to have something about this in the manual at some point…

10 Likes

This is a great explanation, thank you! Would you be so kind as to elaborate on “after hitting toplevel” please? What does that mean? Thanks

1 Like

Top level is where the execution hits global scope for example if the code below was a file that we run, I’ve indicated the places where “top level” is reached

<-------------- top level
module M
<-------------- top level
function f(x)
     return x
end
<-------------- top level
struct Bar
    a::Int 
end
<-------------- top level
f(Bar(3))
<-------------- top level
end
<-------------- top level
15 Likes

While the term is familiar to users who used some kind of Lisp or similar, I wonder if the manual should define/explain toplevel, as users coming from Matlab/R/Python may not have seen it in this context.

18 Likes

There is now a very good and readable research paper about Julia’s novel world age mechanism as a way to avoid needing on-stack replacement in the presence of eval:

I think one of the most important observations about this is that in the presence of concurrency in a modern compiled system, the classic notion that eval takes immediate effect is actually not well-defined and therefore quite confusing. Saying instead that changes don’t kick in until the next top-level evaluation simplifies the mental model considerably and is actually well-defined. Here’s a twitter thread about the paper: https://twitter.com/julbinb/status/1317195401846554624.

29 Likes

A minimal example showing this effect:

foo() = "foo"
function makebar()
	sleep(rand())
	res= (foo(), Base.invokelatest(foo))
	@eval foo() = "bar"
	res
end
fetch.(Threads.@spawn makebar() for i in 1:2)

The standard call to foo() always returns “foo” for both threads, Base.invokelatest(foo) returns “foo” for the thread with the shorter sleep time and “bar” for the other one.

6 Likes

If you want to just demonstrate the basic world age behavior, you don’t need threads or anything, you just evaluate a new function definition in the same local scope before calling it:

f() = 1
function g()
    @eval f() = 2
    f()
end

If you call g once it will return 1, if you call it again it will return 2. If you do this instead:

f() = 1
function g()
    @eval f() = 2
    Base.invokelatest(f)
end 

This will return 2 every time you call it.

33 Likes

Does anyone have a recommendation on how to make the following minimal example work?

function g( i )
    f = eval( :( () -> $i ) )
    f()
end

g(1)
g(2)
1 Like

Given the completely unspecified requirements of what you want to do the only suggestion I can give you is:

julia> g(i) = i
g (generic function with 1 method)

julia> g(1)
1

julia> g(2)
2

Ideally don’t use eval at all:

julia> function g(i)
           f = () -> i
           f()
       end
g (generic function with 1 method)

julia> g(1)
1

julia> g(2)
2

If you must use eval (why?) then use invokelatest to call f:

function g(i)
    f = eval(:(() -> $i))
    invokelatest(f)
end
3 Likes

Use RuntimeGeneratedFunctions.jl.

using RuntimeGeneratedFunctions
RuntimeGeneratedFunctions.init(@__MODULE__)

function g( i )
    f = @RuntimeGeneratedFunction( :( (x) -> $i ) )
    f(1)
end

g(1)
g(2)

It doesn’t handle no argument functions (Failure when defining a function with no args · Issue #53 · SciML/RuntimeGeneratedFunctions.jl · GitHub) but that’s easy to fix for anyone who specifically needs that.

3 Likes

That works, but in this example at least, there’s no need for eval at all, you can just use a closure. If there is some need for eval to construct the body of the inner function based on values, then generated functions are indeed the way to go.

1 Like