<function>
or <operator>
).
This is just notation, and the symbols <
and >
should not be misconstrued as Julia's syntax.
Action | Keyboard Shortcut |
---|---|
Previous Section | Ctrl + 🠘 |
Next Section | Ctrl + 🠚 |
List of Sections | Ctrl + z |
List of Subsections | Ctrl + x |
Close Any Popped Up Window (like this one) | Esc |
Open All Codes and Outputs in a Post | Alt + 🠛 |
Close All Codes and Outputs in a Post | Alt + 🠙 |
Unit | Acronym | Measure in Seconds |
---|---|---|
Seconds | s | 1 |
Milliseconds | ms | 10-3 |
Microseconds | μs | 10-6 |
Nanoseconds | ns | 10-9 |
The previous section has defined type stability, along with approaches to checking whether the property holds. In this section, we start the analysis of type stability for specific objects. We cover in particular the case of scalars and vectors, providing practical guidance for ensuring type stability with them.
The definition of type stability indicates that the compiler infers a single concrete type for each expression. The principle applied to scalars is direct, demanding operations be performed on variables with the same concrete type. In the case of vectors, type stability rather requires that the elements have a concrete type, such as Float64
, Int64
, or Bool
.
The following table identifies scalars and vectors satisfying the property.
Objects Whose Elements Have Concrete Types | |
---|---|
Scalars | Vectors |
Int |
Vector{Int} |
Int64 |
Vector{Int64} |
Float64 |
Vector{Float64} |
Bool |
BitVector |
Note: Int
defaults to Int64
or Int32
, depending on your CPU's architecture.
In the following, we analyze each case separately.
To turn the definition of type stability operational for scalars, let's revisit our discussion about types. Recall that only concrete types like Int64
or Float64
can be instantiated, while abstract types like Any
or Number
can't.
Instantiation simply means that all values ultimately adopt a unique concrete type. For instance, a variable x::Number = 2
shouldn't be interpreted as x
having the type Number
. Instead, it means that x
can only be reassigned to values whose concrete type is a subtype of Number
. Ultimately, x
must have a concrete type, which in this case is Int64
.
Despite that functions always identify a unique concrete type for scalars, some operations that mix Int64
and Float64
could result in type instability. Before providing examples of these cases, we show some scenarios where their mix don't pose a problem.
Julia employs various mechanisms to handle cases combining Int64
and Float64
. The first one is type promotion, which converts dissimilar types to a common one, whenever possible. Likewise, Julia also engages in conversions when variables are type-annotated, transforming values to the respective type.
Both mechanisms are illustrated below.
function foo()
x1::Float64 = 1 # `Int64` will be converted to `Float64`
x2::Int64 = 2.0 # `Float64` will be converted to `Int64`
return x1,x2
end
x1,x2 = foo() # type stable
x1
x2
foo(x,y) = x * y # if mixing `Int64` and `Float64`, then `Float64`
x = 2
y = 0.5
z = foo(x,y) # type stable
z
In the first tab, Julia transforms the values of x1
and x2
to make them consistent with the type-annotations. As for the second tab, the output's type will depend on the argument's types, but in all cases they can be predicted. Specifically, the product of Int64
or Float64
will inherit the type, while mixing Int64
and Float64
results in Float64
due to automatic type promotion.
While type promotion and conversion can handle certain scenarios, they don't cover all. One of these cases is when a scalar's value is conditional, with each branch returning a value with a different type. Since the compiler only observes types and not values, it can't decide which branch is relevant for the function call. Consequently, it'll generate code that accommodates both possibilities. This can be observed in the following example.
function foo(x,y)
a = (x > y) ? x : y
[a * i for i in 1:100_000]
end
foo(1, 2) # type stable -> `a * i` is always `Int64`
@btime foo(1,2)
23.800 μs (2 allocations: 781.30 KiB)
function foo(x,y)
a = (x > y) ? x : y
[a * i for i in 1:100_000]
end
foo(1, 2.5) # type UNSTABLE -> `a * i` is either `Int64` or `Float64`
@btime foo(1,2.5)
43.200 μs (2 allocations: 781.30 KiB)
In the example, type instability will inevitably arise when x
and y
have different types. Note that type promotion is of no help here. The reason is that this mechanism only ensures that a * i
will be converted to Float64
if a
is Float64
, given that i
is Int64
. Nonetheless, the compiler also needs to consider that a
could be Int64
, in which case a * i
is Int64
.
Considering this ambiguity, the method instance created must be capable of handling both scenarios. Then, during runtime, Julia will gather more information to disambiguate the situation, thus selecting the pertinent computation implementation.
Vectors in Julia are formally defined as collections of elements with a homogenous type. Since operations based on vectors ultimately work with its individual elements, type stability is contingent on the concrete type of their elements.
In this context, it's important to distinguish between the type of the vector itself and that of its elements. This is because vectors whose elements have a concrete type are themselves concrete, while elements with abstract types can still give rise to vectors with concrete types. This is clearly observed with Vector{Any}
, a concrete type comprising elements with abstract type Any
.
Before beginning with the analysis of specific scenarios, we consider automatic conversion of types. This mechanism is relevant for the definition of vectors that mix types.
By definition, vectors require all their elements to share the same type. This implies that, for instance, if mix elements with disparate types such as String
and Int64
, Julia will identify the vector with type Vector{Any}
. Nonetheless, there are some instances where elements can be converted to a common type, as when we mix Float64
and Int64
.
In particular, this automatic conversion occurs during assignments, where all elements are converted to the most general type that can accommodate them.
x = [1, 2, 2.5] # automatic conversion to `Vector{Float64}`
x
y = [1, 2.0, 3.0] # automatic conversion to `Vector{Float64}`
y
In both examples, the assignments are type-unannotated. When assignments are instead declared with type-annotations, Julia will attempt to perform a conversion whenever possible, ensuring that the assigned value conforms to the declared type.
x1 = [1, 2.0, 3.0] # automatic conversion to `Vector{Float64}`
x2::Vector{Int64} = y1 # conversion to `Vector{Int64}`
z2
y1 = [1, 2, 2.5] # automatic conversion to `Vector{Float64}`
y2::Vector{Number} = y1 # `y2` is still `Vector{Number}`
z2
nr_elements = 3
z = Vector{Any}(undef, nr_elements) # `Vector{Any}` always
z .= 1
v
When evaluating type stability in the context of vectors, there are two forms of operations that need to be considered. The first one involves operations that manipulate individual elements, such as x[i]
. This scenario parallels the case of scalars, and so type stability adheres to the same rules.
The second scenario involves functions operating on the entire vector. In this case, the design of function commonly ensures type stability if vectors have elements with a concrete type. Note that whether this holds ultimately depends on the package developer's implementation.
For instance, let's consider the example of summing all elements of type-annotated vectors. All the following cases operate on vectors having elements with concrete types and are type stable.
x1::Vector{Int} = [1, 2, 3]
sum(x1) # type stable
x2::Vector{Int64} = [1, 2, 3]
sum(x2) # type stable
x3::Vector{Float64} = [1, 2, 3]
sum(x3) # type stable
x4::BitVector = [true, false, true]
sum(x4) # type stable
In contrast, the following vectors have elements with abstract types and result in type instability.
x5::Vector{Number} = [1, 2, 3]
sum(x5) # type UNSTABLE -> `sum` must consider all possible subtypes of `Number`
x6::Vector{Any} = [1, 2, 3]
sum(x6) # type UNSTABLE -> `sum` must consider all possible subtypes of `Any`