# Minimizing PythonCall overhead with BenchmarkTools

**URL:** https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067
**Category:** Performance
**Tags:** benchmarktools, python, pythoncall
**Created:** [June 8, 2023, 7:01pm UTC](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067 "2023-06-08T19:01:31Z")
**Posts on this page:** 5
**Page:** 1

<div class="post-metadata">

### Author: ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)
#### Post date: [June 8, 2023, 7:01pm UTC](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067/1 "2023-06-08T19:01:31Z")

</div>

Hi there!  
I’m generating benchmarks with BenchmarkTools.jl, where a Python package is called with PythonCall.jl. My question is the following: how can I minimize the overhead of calling a method `meth` from a Python object `obj`?  
Which of these options is the most efficient, or have I perhaps missed a better one? Should I just time from within Python?

```julia
using BenchmarkTools, PythonCall
# define obj and x
@btime $(obj).meth($x)
@btime $(obj.meth)($x)
@btime pycall($(obj).meth, $x)
@btime pycall($(obj.meth), $x)

```

Related topic (does the advice for PyCall.jl also apply here?):

> [@PyCall minimal overhead](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/8):
>
> Yes, your example was necessary (for me). The final MWE using PyCall using BenchmarkTools #I would like to be able to read the python snippet from a file like # py\_snippet=read("axpy.py") #but for now I follow the PyCall example py""" def pyaxpy(y,x,a): y+=a\*x """ pyaxpy = py"pyaxpy" function measure(n) @show n x,y,a=(rand(n),rand(n),1/3) @btime py"pyaxpy"($y,$x,$a) px,py,pa=map(PyObject,(x,y,a)) @btime py"pyaxpy"($py,$px,$pa) @btime $pyaxpy($py,$px,$pa) @bti…

---

<div class="post-metadata">

### Author: ![sylvaticus](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sylvaticus/32/203883_2.png) [@sylvaticus](https://discourse.julialang.org/u/sylvaticus)
#### Post date: [June 8, 2023, 9:46pm UTC](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067/2 "2023-06-08T21:46:27Z")

</div>

You could just try a few cases snd check if one method returns alwsys the lowest timing 🙂

---

<div class="post-metadata">

### Author: ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)
#### Post date: [June 8, 2023, 10:02pm UTC](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067/3 "2023-06-08T22:02:01Z")

</div>

It seems like option 4 is the best one when benchmarking from Julia. Always interpolate _with the object method_

> **Julia benchmark**
>
> ```julia
> julia> using BenchmarkTools, PythonCall
> 
> julia> @pyexec """
> class Fib:
> def __init__ (self, n):
> self.n = n
>        
> def meth(self):
> a, b = 0, 1
> for i in range(self.n):
> a, b = b, a+b
> return a
> """ => Fib
> Python: <class 'Fib'>
> 
> julia> obj_jl = Fib(10)
> Python: <Fib object at 0x7fcb883f1f50>
> 
> julia> @btime $(obj_jl).meth();
> 395.060 ns (4 allocations: 72 bytes)
> 
> julia> @btime $(obj_jl.meth)();
> 279.845 ns (1 allocation: 16 bytes)
> 
> julia> @btime pycall($(obj_jl).meth);
> 391.574 ns (4 allocations: 72 bytes)
> 
> julia> @btime pycall($(obj_jl.meth));
> 278.197 ns (1 allocation: 16 bytes)
> 
> ```

However, benchmarking from Python with `timeit` is faster:

> **Python benchmark**
>
> ```julia
> julia> using PythonCall
> 
> julia> @pyexec """
> import timeit
> import numpy as np
>        
> class Fib:
> def __init__ (self, n):
> self.n = n
>        
> def meth(self):
> a, b = 0, 1
> for i in range(self.n):
> a, b = b, a+b
> return a
>        
> obj_py = Fib(10)
> times_py = np.array(timeit.repeat("obj_py.meth()", globals=locals(), number=100, repeat=1000)) / 100
> """ => times_py;
> 
> julia> minimum(times_py) # result in seconds
> Python: 2.6893000040217883e-07
> 
> ```

---

<div class="post-metadata">

### Author: ![JonasIsensee](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/jonasisensee/32/4704_2.png) [@JonasIsensee](https://discourse.julialang.org/u/JonasIsensee)
#### Post date: [June 9, 2023, 7:55am UTC](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067/4 "2023-06-09T07:55:09Z")

</div>

Option two does not measure the execution time.  
You execute the function once as setup and interpolate the result into the expression.

The fastest of the “actual” benchmarks seems to be no 4.

---

<div class="post-metadata">

### Author: ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)
#### Post date: [June 9, 2023, 7:57am UTC](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067/5 "2023-06-09T07:57:32Z")

</div>

> [@JonasIsensee](#):
>
> Option two does not measure the execution time.

My bad, I misplaced the parentheses. Will edit
