To help me better understand Julia’s performance behavior, I am trying to understand the large difference between two strategies for checking for undefined methods: hasmethod
vs try-catch
.
I’ve read through Are exceptions in Julia "Zero Cost" - #8 by StefanKarpinski but that thread seemed to demonstrate negligible differences, whereas this example is quite significant.
# Manual check
function f(x)
hasmethod(cos, typeof((x,))) && return cos(x)
return one(typeof(x))
end
# Try-catch
function g(x)
try
return cos(x)
catch
end
return one(typeof(x))
end
Both of these functions do the same thing, returning cos(x)
where possible, otherwise just one
of the input type.
However, these have a massive performance difference:
julia> @btime f(x) setup=(x="Test");
1.188 μs (4 allocations: 176 bytes)
julia> @btime g(x) setup=(x="Test");
211.333 μs (6 allocations: 224 bytes)
Is this behavior expected? Maybe different types of errors can be more expensive to catch? Particularly the MethodError
due to the type inference required, but I would have expected hasmethod
to do essentially the same thing.
Additional info:
julia> versioninfo()
Julia Version 1.9.0
Commit 8e630552924 (2023-05-07 11:25 UTC)
Platform Info:
OS: macOS (arm64-apple-darwin22.4.0)
CPU: 8 × Apple M1 Pro
WORD_SIZE: 64
LIBM: libopenlibm
LLVM: libLLVM-14.0.6 (ORCJIT, apple-m1)
Threads: 6 on 6 virtual cores
Environment:
JULIA_NUM_THREADS = auto
JULIA_FORMATTER_SO = /Users/mcranmer/julia_formatter.so
JULIA_EDITOR = code
I ran with -O3
.
When I run for valid types, the try-catch actually wins:
julia> @btime f(x) setup=(x=1.0);
163.068 ns (2 allocations: 112 bytes)
julia> @btime g(x) setup=(x=1.0);
1.208 ns (0 allocations: 0 bytes)
If I rethrow non-MethodErrors to make these completely equivalent, I don’t see much of a change:
function g(x)
try
return cos(x)
catch e
isa(e, MethodError) || rethrow(e)
end
return one(typeof(x))
end
@btime g(x) setup=(x="Test");
# 214.042 μs (6 allocations: 224 bytes)