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.
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).
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.
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.
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.
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
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.
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.
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.
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
using RuntimeGeneratedFunctions
RuntimeGeneratedFunctions.init(@__MODULE__)
function g( i )
f = @RuntimeGeneratedFunction( :( (x) -> $i ) )
f(1)
end
g(1)
g(2)
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.