I understand that Julia heavily relies on just-in-time static type derivation (essentially all code needs to be thought of as c++ templates). I also get that this means there is no run-time overhead when using a single algorithm on objects of different type as long as those types are known at compile-time.
When it comes to run-time polymorphism I am less clear on how things work. Say we have the following situation:
abstract Shape
type Circle <: Shape
radius::Float64
end
type Square <: Shape
width::Float64
end
dist(x::Circle, y::Circle) = ...
dist(x::Circle, y::Square) = ...
dist(x::Square, y::Circle) = ...
dist(x::Square, y::Square) = ...
s = get_shape()
t = get_shape()
a = dist(s,t)
Here, get_shape
can return either circles or squares, based on e.g. user input. In c++, dispatch would simply take a virtual table lookup. How does this work in Julia? What is the mechanism behind multiple dispatch? Is it significantly more expensive then virtual table lookup? Is there any benefit from deriving both Square
and Circle
from the same abstract type, or is this completely irrelevant in the context of run-time dispatching?
EDT: Running @code_warntype on this example gives:
Variables:
s::Union{Circle,Square}
t::Union{Circle,Square}
Body:
begin # none, line 2:
s = (Main.get_shape)()::Union{Circle,Square} # none, line 3:
t = (Main.get_shape)()::Union{Circle,Square} # none, line 4:
return (Main.dist)(s::Union{Circle,Square},t::Union{Circle,Square})::ASCIIString
end::ASCIIString
So the compiler is not completely clueless about the type of s and t. Is this knowledge used to expedite dispatching when calling dist
?
Runtime polymorphism can be achieved only through a pointer (or reference) of base class type. Also, a base class pointer can point to the objects of base class as well as to the objects of derived class. In above code, base class pointer 'b' contains the address of object 'd' of derived class.
The runtime polymorphism can be achieved by method overriding. Java virtual machine determines the proper method to call at the runtime, not at the compile time. It is also called dynamic or late binding. Method overriding says the child class has the same method as declared in the parent class.
The primary advantage of runtime polymorphism is enabling the class to offer its specification to another inherited method. This implementation transfer from one method to another can be done without altering or changing the codes of the parent class object.
Using all of a function's arguments to choose which method should be invoked, rather than just the first, is known as multiple dispatch.
When you have multiple methods for the same function, julia uses method lookup via type intersection (matching the types of the arguments to the types in the signature) to determine which method to call. If the types can be inferred, then that calculation can be performed when the code is being compiled. By doing the lookup ahead of time, it doesn't have to perform type intersection at run time, and this gives the best performance.
When the types are not predictable, then julia has to figure out which method to dispatch to at runtime. This can sometimes be a bottleneck on the runtime, if the called function is doing a trivial amount of work. (When it's doing a lot of work, the lookup is basically inconsequential for performance).
It's a slightly more complicated problem for julia than OOP languages, because the correct method depends on all the arguments, not just the first.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With