FunctionWranglers.jl

Fast, inlined execution of arrays of functions
Author tisztamo
Popularity
8 Stars
Updated Last
3 Years Ago
Started In
September 2020

FunctionWranglers.jl

Lifecycle Build Status codecov.io

This package allows fast, inlined execution of functions provided in an array. It can be used to speed up dispatch-heavy calculations directly, or as a building block for developing high-performance programming primitives.

The following operations are supported currently:

  • smap! maps a single set of arguments using all the functions into a preallocated array.
  • sfindfirst looks for the first function which returns true for the given arguments, and returns its index.
  • sindex selects and calls a function from the wrangler. This is not constant time, but for small indexes faster than indexing to the original array directly.
  • sreduce transforms a single value with the composite of the functions, and also allows providing extra "context" arguments to the functions

Please note that merging the method bodies at the first call have some compilation overhead, which may be significant if the array contains more than a few dozen functions. Tests run with 200 functions.

Reference

smap!

smap!(outputs, wrangler::FunctionWrangler, args...)

Map a single set of arguments using all the functions into a preallocated array.

  | | |_| | | | (_| |  |  Version 1.5.2 (2020-09-23)
 _/ |\__'_|_|_|\__'_|  |  Official https://julialang.org/ release
|__/ 

julia> using FunctionWranglers

julia> create_adder(value) = (x) -> x + value
create_adder (generic function with 1 method)

julia> adders = Function[create_adder(i) for i = 1:5]
5-element Array{Function,1}:
 #3 (generic function with 1 method)
 #3 (generic function with 1 method)
 #3 (generic function with 1 method)
 #3 (generic function with 1 method)
 #3 (generic function with 1 method)

julia> w = FunctionWrangler(adders)
FunctionWrangler with 5 items: #3, #3, #3, #3, #3, 

julia> result = zeros(Float64, length(adders))
5-element Array{Float64,1}:
 0.0
 0.0
 0.0
 0.0
 0.0

julia> smap!(result, w, 10.0) # If your functions accept more arguments, you can also provide them here

julia> result
5-element Array{Float64,1}:
 11.0
 12.0
 13.0
 14.0
 15.0

julia> @btime smap!($result, $w, d) setup = (d = rand())
  3.934 ns (0 allocations: 0 bytes)

sfindfirst

sfindfirst(wrangler::FunctionWrangler, args...)

Look for the first function which returns true for the given arguments, and return its index. Return nothing if no function resulted in true.


julia> using FunctionWranglers, BenchmarkTools

julia> predicates = Function[(x) -> x < i for i=1:5]
5-element Array{Function,1}:
 #1 (generic function with 1 method)
 #1 (generic function with 1 method)
 #1 (generic function with 1 method)
 #1 (generic function with 1 method)
 #1 (generic function with 1 method)

julia> fw = FunctionWrangler(predicates)
FunctionWrangler with 5 items: #1, #1, #1, #1, #1, 

julia> sfindfirst(fw, 4)
5

julia> sfindfirst(fw, 5)

julia> @btime sfindfirst($fw, 4)
  1.699 ns (0 allocations: 0 bytes)
5

sindex

sindex(wrangler::FunctionWrangler, idx, args...)

Call the idx-th function with args.

julia> adders = Function[create_adder(i) for i = 1:50]
50-element Vector{Function}:
 #1 (generic function with 1 method)
 
 #1 (generic function with 1 method)
 
julia> w = FunctionWrangler(adders)
FunctionWrangler with 50 items: #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, #1, 

julia> sindex(w, 3, 10)
13

julia> sindex(w, 30, 10)
40

Note that this call iterates the wrangler from 1 to idx. Try to put the frequently called functions at the beginning to minimize overhead:

julia> @btime sindex($w, i, i) setup=(i=rand(1:10))
  2.228 ns (0 allocations: 0 bytes)
8

julia> @btime sindex($w, i, i) setup=(i=rand(20:30))
  3.454 ns (0 allocations: 0 bytes)
60

julia> @btime sindex($w, i, i) setup=(i=rand(40:50))
  3.695 ns (0 allocations: 0 bytes)
88

sreduce

sreduce(wrangler::FunctionWrangler, args...; init = nothing)

Transform a single init value with the composite of the functions in wrangler.

The functions will be applied in their order in wrangler. If you provide extra args, they will be passed to the functions after the current value of the computation:

julia> using FunctionWranglers, BenchmarkTools

julia> pluses = Function[(+) for i = 1:5]
5-element Array{Function,1}:
 + (generic function with 184 methods)
 + (generic function with 184 methods)
 + (generic function with 184 methods)
 + (generic function with 184 methods)
 + (generic function with 184 methods)

julia> fw = FunctionWrangler(pluses)
FunctionWrangler with 5 items: +, +, +, +, +, 

julia> sreduce(fw, 1; init = 10)
15

julia> @btime sreduce($fw, 1; init = 10)
  1.099 ns (0 allocations: 0 bytes)
15