Why there are different execution times for same function calls

Below is my Code:

function bisection(custom_func,starting::T,ending::K) where {T<:Real,K<:Real}
    if custom_func(starting)==0
        return starting
    elseif custom_func(ending)==0
        return ending
    elseif  !_root_finder(starting,ending)
        print("Cannot find roots between a and b")
        return Nothing
    else
        mid_point = _get_mid_point(starting,ending)
        while !_root_evaluate(mid_point)
            if _root_finder(starting,mid_point)
                ending = mid_point
            elseif _root_finder(mid_point,ending)
                starting = mid_point
            end
            mid_point = _get_mid_point(starting,ending)
        end
        return mid_point
    end
end

function _get_mid_point(x::T, y::K) where {T<:Real,K<:Real}
    return x+(y-x)/2
end

function _root_evaluate(x::T) where {T<:Real}
    value = custom_func(x)
    abs_value = abs(value)
    return abs_value<10^-7
end

function _root_finder(x::T, y::K) where {T<:Real,K<:Real}
    x,y = custom_func(x), custom_func(y)
    return x * y < 0
end

function custom_func(x::T) where {T<:Real}
    return x^4-4x^3+x+4
end

Below are the execution Times.

All the above cells are ran sequentially.

@time bisection(custom_func,0,3)

For the first call it takes 31 ms. (Relatively Higher, Because it will be compiled and execute)

@time bisection(custom_func,0,2.4)

For the second call it takes 28 us. (Lesser, May be because it is already compiled)

@time bisection(custom_func,0,2)

For the third call it takes 10ms. (Higher than the second call, could be long computation as there are changes in arguments)

@time bisection(custom_func,0,2)

Surprisingly now for the fourth call it only takes 26us for the same set of arguments as previously run.

Can the community please help me in understanding why there is inconsistency in execution times. Also please let me know if my understanding is right on the first call remarks (i.e, execution time is higher because it should be compiled)

Thanks,
Akhil

First you will notice that the number/amount of allocations changes from run to run, which is odd. I don’t see anything randomness in your code, so calling it with the same parameters should produce the same memory allocation counts. I suspect this allocation is what is causing your fluctuating timings. Memory allocation is not really deterministic, so the amount of code executing can very wildly.

One issue with your bisection function is that you are returning Nothing I believe this is an error you probably want to return nothing. Nothing is a type, nothing is the value. Also are you sure you want starting and ending to be different types? It might be better to do:

function bisection(custom_func,starting::T,ending::T) where {T<:Real}

Next _root_evaluate and _root_finder calls custom_func which is NOT the function passed in, so if you create a custom_func2 and pass that in, you will probably get unexpected results.

Okay now for the big timing changes. Run 1 and 2 are BOTH causing a compilation. Since the first time you are passing in (Int64, Int64) and the second run you are passing in (Int64, Float64). I would recommend using @btime from the BechmarkTools package, that should give you more consistent results.

julia> @btime bisection(custom_func, 0, 3)
  5.290 μs (143 allocations: 2.23 KiB)
1.2377290427684784

julia> @btime bisection(custom_func, 0, 2.4)
  4.499 μs (129 allocations: 2.02 KiB)
1.2377290427684782

julia> @btime bisection(custom_func, 0, 2)
  5.737 μs (156 allocations: 2.44 KiB)
1.2377290427684784

julia> @btime bisection(custom_func, 0, 2)
  5.768 μs (156 allocations: 2.44 KiB)
1.2377290427684784

On types, I’d recommend either canonicalizing inputs:

julia> function cbisection(f::F, l, u) where {F}
          T = typeof(_get_mid_point(l, u))
          bisection(f, convert(T, l), convert(T, u))
       end
cbisection (generic function with 1 method)

julia> @btime cbisection(custom_func, 0, 2)
  1.470 μs (0 allocations: 0 bytes)
1.2377290427684784

julia> @btime bisection(custom_func, 0, 2)
  3.559 μs (156 allocations: 2.44 KiB)
1.2377290427684784

or ensuring that the internal code is type stable regardless of inputs.

3 Likes

This is my first time experience with Julia.

function bisection(custom_func,starting::T,ending::T) where {T<:Real}

This is popping error as arg types are different, i.e, Int64, Float64.

If I didn’t used “where” keyword in defining my params types and instead define my types straight in the arguments I’m getting reasonable perfomance without any huge differences. Ex:

function _get_mid_point(x::Real, y::Real)
    return x+(y-x)/2
end

function _root_evaluate(x::Real)
    value = custom_func(x)
    abs_value = abs(value)
    return abs_value<10^-7
end

function _root_finder(x::Real, y::Real)
    x,y = custom_func(x), custom_func(y)
    return x * y < 0
end


function custom_func(x::Real)
    return x^4-4x^3+x+4
end

Welcome!

If I didn’t used “where” keyword in defining my params types and instead define my types straight in the arguments I’m getting reasonable perfomance without any huge differences.

That’s totally fine. There’s no performance difference between f(x::Real) and f(x::T) where {T <: Real}. The reason you might use the where syntax is when you want a convenient way to refer to the type T, either in the body of the function or in the types of other inputs.

For example, you could do:

function foo(x::Real, y::Real)
  x + y
end

But what if you want x and y to be of the same type of Real?

In that case, you can use where:

function foo(x::T, y::T) where {T <: Real}
  x + y
end

Or if you want an scalar and a matrix with the same element type, where that type is some kind of Real:

function foo(x::T, y::AbstractMatrix{T}) where {T <: Real}
  x * y
end

The where clause is a way of talking about constraints on types. If you don’t need those constraints, then you don’t need to use it. In the functions you’ve shown here, doing x::Real, y::Real is totally fine.

2 Likes