pic
Personal
Website

8e. Barrier Functions

PhD in Economics

Introduction

This section explores a technique for mitigating type instability that involves the so-called barrier functions. These are type-stable functions embedded within a type-unstable function. Their key to solving type instability lies the type inference of functions, which identifies any variable's type when passed as a function argument.

A salient aspect of barrier functions is that they can effectively address type instability, regardless of its underlying cause.

Warning! - Barrier Functions Should be a Second Option
The use of barrier functions should be reserved for situations where type instability is either difficult to fix or inherent to the operations performed. The recommendation arises because the original function would still be type unstable. Considering this, strive for type-stable code from the outset whenever possible.

Applying Barrier Functions

To better illustrate the technique, let's revisit a type-unstable function from a previous section. This defines a variable y based on x, and subsequently computes an operation involving y.

Type Instability
function foo(x)
    y = (x < 0) ?  0  :  x
    
    [y * i for i in 1:100]
end

@code_warntype foo(1)       # type stable
@code_warntype foo(1.)      # type UNSTABLE

The type instability in this example arises because 0 is Int64, whereas x could be either Int64 or a Float64. In case x is Int64, y will also be Int64 and so foo(1) type stable. However, if x is Float64, the compiler is unable to determine whether y will be Int64 or a Float64. This turns foo(1.) type unstable.

Addressing the type instability through a barrier functions requires embedding a type-stable function into foo, with y passed as one of its arguments. By doing so, the function will deduce y's type, allowing the compiler to use this information for subsequent operations. The example below implements operation as a barrier function. [note] Notice that there's an easier solution for this example, where 0 with zero(x). The function zero(x) returns the null element for the type of x.

Barrier Function
operation(y) = [y * i for i in 1:100]

function foo(x)
    y = (x < 0) ?  0  :  x
    
    operation(y)
end

@code_warntype operation(1)    # barrier function is type stable
@code_warntype operation(1.)   # barrier function is type stable

@code_warntype foo(1)          # type stable
@code_warntype foo(1.)         # barrier-function solution

In this version, the variable y in foo(1.) could still be an Int64 or Float64. However, this becomes irrelevant, as operation(y) will identify the type of y before entering the array comprehension. Thus, [y * i for i in 1:100] will be computed using a method specialized for the type of y.

Warning!
Barrier Functions should solve the type instability before the type unstable operation is executed. Otherwise, we're back to the original issue, where the compiler has to check y's type at each iteration and select a method accordingly.

For example, foo(1.) in the code below is not applying the barrier-function technique properly: y could be Float64 or Int64, and operation(y,i) only identifies the type inside the for-loop. This feature implies that the compiler must check y's at each iteration of the for-loop.

Wrong Use
operation(y,i) = y * i 

function foo(x)
    y = (x < 0) ?  0  :  x
    
    [operation(y,i) for i in 1:100]
end

@code_warntype foo(1)          # type stable
@code_warntype foo(1.)         # type UNSTABLE

Remarks on @code_warntype

The introduction of barrier functions can hinder the interpretation of @code_warntype. This is because barrier functions typically mitigate the type instability, rather than completely eliminating it. Consequently, we may still receive a red warning.

To illustrate this, let's start presenting a scenario where the barrier function successfully handles the type instability.

x = ["a", 1]                     # variable with type 'Any'



function foo(x)
    y = x[2]
    
    [y * i for i in 1:100]
end
Output in REPL
julia>
@code_warntype foo(x)

x = ["a", 1]                     # variable with type 'Any'

operation(y) = [y * i for i in 1:100]

function foo(x)
    y = x[2]
    
    operation(y)
end
Output in REPL
julia>
@code_warntype foo(x)

The following example demonstrates that the barrier function can alleviate the impact of type instability, even without eliminating it. In the scenario considered, two type-unstable operations are present, and the barrier function targets the one with a more significant impact, which is given by the for-loop operation.

x = ["a", 1]                     # variable with type 'Any'



function foo(x)
    y = 2 * x[2]
    
    [y * i for i in 1:100]
end
Output in REPL
julia>
@code_warntype foo(x)

x = ["a", 1]                     # variable with type 'Any'

operation(y) = [y * i for i in 1:100]

function foo(x)
    y = 2 * x[2]
    
    operation(y)
end
Output in REPL
julia>
@code_warntype foo(x)

x = ["a", 1]                     # variable with type 'Any'

operation(y) = [y * i for i in 1:100]

function foo(z)
    y = 2 * z
    
    operation(y)
end
Output in REPL
julia>
@code_warntype foo(x)