<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 |
Recall that functions in Julia are first-class objects, meaning that they can be manipulated as any other variable. [note] Equivalently, it's said that functions are first-class citizens. This enables us to define vectors of functions, create functions that return other functions, and more.
In particular, it enables the definition of high-order functions: functions that take another function as an argument. We've already worked with several examples of them, where anonymous functions were used as an input (e.g., map
and filter
).
Next, we consider the conditions under which high-order functions are type-stable.
For the explanations, let's refer to the high-order function as the parent function, and the child function as the one passed as an argument.
Each function in Julia defines its own unique concrete type. [note] Additionally, each function is a subtype of an abstract type called Function
This feature can become problematic for specialization of high-order functions. Specifically, it may cause a combinatorial explosion of specialized methods, as each unique child function would require separate compilation.
To mitigate this issue, Julia has opted for not always specializing the methods of high-order functions. In some circumstances, this may result in a degraded performance, similar to working in the global scope. Therefore, it's important to pinpoint the scenarios where specialization is inhibited and monitor its consequences. In situations where performance is significantly impaired, it's still possible to explicitly induce specialization. Next, we explore the methods for achieving this.
To illustrate the scenario, suppose we want to transform a vector x
using map
, and then sum its transformed elements. Additionally, assume we want to keep the transforming function generic, for which the transforming function is defined as an argument f
. The implementation of this task is shown below, where we illustrate its usage through the function abs
as the transformed function f
.
x = rand(100)
function foo(f, x)
y = map(f, x)
sum(y)
end
@code_warntype foo(abs,x)
The situation is especially tricky because @code_warntype
fails to detect any type-stability issues. The reason is that @code_warntype
evaluates type stability under the assumption of specialization, which doesn't hold in our example: Julia bypasses specialization in the particular case where f
is not explicitly called. Specifically, f
in our example only enters foo
as an argument of map
.
To corroborate the absence of specialization, let's compare the original function with a modified version that explicitly calls f
.
x = rand(100)
function foo(f, x)
y = map(f, x)
sum(y)
end
foo(abs, x)
48.447
@btime foo(abs, $x)
x = rand(100)
function foo(f, x)
y = map(f, x)
f(1) # irrelevant computation to force specialization
sum(y)
end
foo(abs, x)
48.447
@btime foo(abs, $x)
We can observe a significant reduction in time by adding f(1)
, along with a decrease in memory allocations. Excessive allocations are a common symptom of type instability, as we'll demonstrate when exploring the subject.
Nonetheless, explicitly calling the function to circumvent the no-specialization issue isn't optimal, since it introduces an unnecessary computation. Fortunately, alternative solutions exist to address the problem. One approach is to type-annotate f
, which provides Julia with a hint to specialize. Another solution is to wrap the function in a tuple and then call it. This ensures the identification of the function's type, as the information is inherent to the tuple's type.
For the sake of completeness, we next outline all the approaches, including the previous method with an explicit function call.
x = rand(100)
function foo(f, x)
y = map(f, x)
f(1) # irrelevant computation to force specialization
sum(y)
end
foo(abs, x)
48.447
@btime foo(abs, $x)
x = rand(100)
function foo(f::F, x) where F
y = map(f, x)
sum(y)
end
foo(abs, x)
48.447
@btime foo(abs, $x)
x = rand(100)
f_tup = (abs,)
function foo(f_tup, x)
y = map(f_tup[1], x)
sum(y)
end
foo(f_tup, x)
48.447
@btime foo($f_tup, $x)