# PyCall minimal overhead

**URL:** <https://discourse.julialang.org/t/pycall-minimal-overhead/41127>\
**Category:** Performance\
**Created:** [June 10, 2020, 2:03pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127 "2020-06-10T14:03:38Z")\
**Posts on this page:** 16\
**Page:** 1

<div class="post-metadata">

**Author:** ![LaurentPlagne](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/laurentplagne/32/10103_2.png) [@LaurentPlagne](https://discourse.julialang.org/u/LaurentPlagne)\
**Post date:** [June 10, 2020, 2:03pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/1 "2020-06-10T14:03:38Z")

</div>

Hi,  
I work on a Julia benchmark Data Base project aiming to compare different implementations of basic computational kernels implemented in various languages.

I wonder about the most efficient way to call a Python snippet from Julia. The following MWE

```julia
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
"""
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)
end

foreach(measure,(10^i for i in (1:4)))

```

returns the following results on my machine:

```julia
n = 10
  14.097 μs (23 allocations: 1.05 KiB)
  12.548 μs (11 allocations: 400 bytes)
n = 100
  14.210 μs (23 allocations: 1.05 KiB)
  12.696 μs (11 allocations: 400 bytes)
n = 1000
  15.854 μs (23 allocations: 1.05 KiB)
  14.438 μs (11 allocations: 400 bytes)
n = 10000
  23.135 μs (23 allocations: 1.05 KiB)
  22.240 μs (11 allocations: 400 bytes)

```

- It appears that there is a minimal overhead of about **12 microseconds** on a Python call.
- It also appear that I can save **1 microsecond** by _pre-PyObjecting_ function arguments.
- When I use Python `perfplot`, it seems that the minimal timings are around **1 microsecond** (and not 10).

My questions are:

1. Is there a way to reduce this overhead from Julia?
2. Could I store my Python snippet in a separated file ?

Thank you for your help 😉

---

<div class="post-metadata">

**Author:** ![lungben](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lungben/32/12314_2.png) [@lungben](https://discourse.julialang.org/u/lungben)\
**Post date:** [June 10, 2020, 3:50pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/2 "2020-06-10T15:50:55Z")

</div>

I saved much time by assigning the Python function to a Julia name.

```julia
@btime py"pyaxpy"($y,$x,$a) # n=10

```

11.699 μs (23 allocations: 1.05 KiB)

```julia
pyaxpy = py"pyaxpy"
@btime pyaxpy($y,$x,$a)

```

4.614 μs (18 allocations: 896 bytes)

Combining with converting the inputs beforehand to PyObjects:

```julia
px,py,pa=map(PyObject,(x,y,a))
@btime pyaxpy($py,$px,$pa)

```

3.563 μs (6 allocations: 224 bytes)

in Python:

```julia
%timeit pyaxpy(a,b,c)

```

1.5 µs ± 9.79 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

Thus, the PyCall function call overhead is approximately 2 μs.

---

<div class="post-metadata">

**Author:** ![stevengj](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stevengj/32/71_2.png) [@stevengj](https://discourse.julialang.org/u/stevengj)\
**Post date:** [June 10, 2020, 4:00pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/3 "2020-06-10T16:00:12Z")

</div>

You can also save time by suppressing the output conversion: `pycall(myfunc, PyObject, args...)`.

---

<div class="post-metadata">

**Author:** ![lungben](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lungben/32/12314_2.png) [@lungben](https://discourse.julialang.org/u/lungben)\
**Post date:** [June 10, 2020, 4:03pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/4 "2020-06-10T16:03:34Z")

</div>

Including @stevengj 's suggestion:

```julia
@btime pycall(pyaxpy, PyObject, $py,$px,$pa)

```

2.244 μs (4 allocations: 112 bytes)  
Only \<1 µs overhead remains.

---

<div class="post-metadata">

**Author:** ![LaurentPlagne](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/laurentplagne/32/10103_2.png) [@LaurentPlagne](https://discourse.julialang.org/u/LaurentPlagne)\
**Post date:** [June 10, 2020, 4:06pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/5 "2020-06-10T16:06:15Z")

</div>

Thanx a lot,  
I modified the MWE accordingly (with _pre-PyObjecting_)

```julia
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)
end

foreach(measure,(10^i for i in (1:4)))

```

And now I get:

```julia
n = 10
  14.030 μs (23 allocations: 1.05 KiB)
  12.486 μs (11 allocations: 400 bytes)
  3.150 μs (4 allocations: 176 bytes)
n = 100
  13.967 μs (23 allocations: 1.05 KiB)
  12.635 μs (11 allocations: 400 bytes)
  3.298 μs (4 allocations: 176 bytes)
n = 1000
  15.655 μs (23 allocations: 1.05 KiB)
  14.202 μs (11 allocations: 400 bytes)
  4.419 μs (4 allocations: 176 bytes)
n = 10000
  25.840 μs (23 allocations: 1.05 KiB)
  22.458 μs (11 allocations: 400 bytes)
  12.384 μs (4 allocations: 176 bytes)

```

---

<div class="post-metadata">

**Author:** ![LaurentPlagne](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/laurentplagne/32/10103_2.png) [@LaurentPlagne](https://discourse.julialang.org/u/LaurentPlagne)\
**Post date:** [June 10, 2020, 4:23pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/6 "2020-06-10T16:23:01Z")

</div>

Wow, thank you very much @stevengj and @lungben !!

I must confess that I was not able to use the @stevengj suggestion (`pycall(myfunc, PyObject, args...)`) without an example 😌

---

<div class="post-metadata">

**Author:** ![lungben](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lungben/32/12314_2.png) [@lungben](https://discourse.julialang.org/u/lungben)\
**Post date:** [June 10, 2020, 4:24pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/7 "2020-06-10T16:24:03Z")

</div>

A minimal working example is in my 2nd post above.  
Hope this helps!

```julia
@btime pycall(pyaxpy, PyObject, $py,$px,$pa)

```

---

<div class="post-metadata">

**Author:** ![LaurentPlagne](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/laurentplagne/32/10103_2.png) [@LaurentPlagne](https://discourse.julialang.org/u/LaurentPlagne)\
**Post date:** [June 10, 2020, 4:29pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/8 "2020-06-10T16:29:05Z")

</div>

Yes, your example was necessary (for me). The final MWE

```julia
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)
    @btime pycall($pyaxpy, PyObject, $py,$px,$pa)
end

foreach(measure,(10^i for i in (1:4)))

```

and results:

```julia
n = 10
  14.007 μs (23 allocations: 1.05 KiB)
  12.474 μs (11 allocations: 400 bytes)
  3.172 μs (4 allocations: 176 bytes)
  1.526 μs (2 allocations: 48 bytes)

```

Thank you very much.  
Do you know how to include the Python snippet from a file ?

---

<div class="post-metadata">

**Author:** ![lungben](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lungben/32/12314_2.png) [@lungben](https://discourse.julialang.org/u/lungben)\
**Post date:** [June 10, 2020, 4:53pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/9 "2020-06-10T16:53:45Z")

</div>

Assume that your Python code is in `my_module.py` in the current directory, the following should work:

```julia
ENV["PYTHONPATH"] = "." # to enforce Python looking in current directory for imports
my_module = pyimport("my_module")
my_module.pyaxpy(y,x,a)

```

Edit: it is better to replace the `ENV` command by the following line to preserve potentially existing entries in `PYTHONPATH`:

```julia
pushfirst!(PyVector(pyimport("sys")."path"), "")

```

Source: [GitHub - JuliaPy/PyCall.jl: Package to call Python functions from the Julia language](https://github.com/JuliaPy/PyCall.jl#troubleshooting)

---

<div class="post-metadata">

**Author:** ![LaurentPlagne](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/laurentplagne/32/10103_2.png) [@LaurentPlagne](https://discourse.julialang.org/u/LaurentPlagne)\
**Post date:** [June 10, 2020, 5:00pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/10 "2020-06-10T17:00:13Z")

</div>

Thank you again !

---

<div class="post-metadata">

**Author:** ![Palli](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/palli/32/3380_2.png) [@Palli](https://discourse.julialang.org/u/Palli)\
**Post date:** [June 10, 2020, 5:56pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/11 "2020-06-10T17:56:39Z")

</div>

> [@LaurentPlagne](#):
>
> Do you know how to include the Python snippet from a file ?

You would think this would be easy, even in Python (but it differed by version, here for Python 3).

It was surprisingly obscure, and at work, I ended up doing this way (may not be best way, and as I’m rewriting in Julia anyway, so will not investigate, but please tell me if you find a better way):

```julia
using PyCall

py"""
filename="app.py"
with open(filename, "rb") as source_file:
    code = compile(source_file.read(), filename, "exec")
exec(code)

app.run_server(host='localhost', port=8050)
"""

```

[I was calling a Python program using Dash, i.e. JavaScript in a web browser, but will be using [GitHub - plotly/Dash.jl: Dash for Julia - A Julia interface to the Dash ecosystem for creating analytic web applications in Julia. No JavaScript required.](https://github.com/plotly/Dash.jl)]

---

<div class="post-metadata">

**Author:** ![ffevotte](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/ffevotte/32/6587_2.png) [@ffevotte](https://discourse.julialang.org/u/ffevotte)\
**Post date:** [June 11, 2020, 7:25am UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/12 "2020-06-11T07:25:37Z")

</div>

> [@LaurentPlagne](#):
>
> I would like to be able to read the python snippet from a file like  
> `py_snippet=read("axpy.py")`

For this, how about using the `$$` interpolation mechanism built in `@py_str`?

```julia
help?> @py_str
  py".....python code....."

  Evaluate the given Python code string in the main Python module.

  If the string is a single line (no newlines), then the Python
  expression is evaluated and the result is returned. If the string
  is multiple lines (contains a newline), then the Python code is
  compiled and evaluated in the __main__ Python module and nothing
  is returned.

  If the o option is appended to the string, as in py"..."o, then
  the return value is an unconverted PyObject; otherwise, it is
  automatically converted to a native Julia type if possible.

  Any $var or $(expr) expressions that appear in the Python code
  (except in comments or string literals) are evaluated in Julia
  and passed to Python via auto-generated global variables.
  This allows you to "interpolate" Julia values into Python code.

  Similarly, ny $$var or $$(expr) expressions in the Python code
  are evaluated in Julia, converted to strings via string, and are
  pasted into the Python code. This allows you to evaluate code
  where the code itself is generated by a Julia expression.

```

For example:

```julia
julia> using PyCall

       # could as well be:
       # pycode = read("myfile.py", String)
julia> pycode = """
       def hello():
           print("Hello from python!")
       """
"def hello():\n print(\"Hello from python!\")\n"

julia> py"""
       $$pycode
       """

julia> py"hello()"
Hello from python!
```

---

<div class="post-metadata">

**Author:** ![LaurentPlagne](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/laurentplagne/32/10103_2.png) [@LaurentPlagne](https://discourse.julialang.org/u/LaurentPlagne)\
**Post date:** [June 11, 2020, 6:50pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/13 "2020-06-11T18:50:07Z")

</div>

Thank you both !

---

<div class="post-metadata">

**Author:** ![filchristou](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/filchristou/32/26760_2.png) [@filchristou](https://discourse.julialang.org/u/filchristou)\
**Post date:** [October 21, 2021, 6:16pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/14 "2021-10-21T18:16:10Z")

</div>

Would it be possible to directly call `timeit` from inside julia ? this way the evaluation will be done completely in python without considering the object pass overhead.  
something like:

```julia
timeit = pyimport("timeit")
timeit.timeit("benchmark_function()", globals=locals())

```

At the moment, it’s not running because locals() doesn’t exist in julia the way I experience it.

Is there a work around ?

edit:

Okey so I tested it without the namespace being relevant:  
(appending 1000 elements in a list)

```julia
timeit.timeit("""
times=1000
a = []
for i in range(1,times+1):
    a.append(i)
""", number=10000)/10000*1000000000 #nanoseconds

```

but running it directly in python is faster by 10 us.  
Don’t know why though, since I thought `timeit` should start measuring by the moment the transition to the python environment is complete. 😕

---

<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:02pm UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/15 "2023-06-08T19:02:34Z")

</div>

> [@filchristou](#):
>
> Would it be possible to directly call `timeit` from inside julia ?

Sorry for unearthing this thread but I have a related question for PythonCall

@filchristou do you have the answer to your question by now?

> [@Minimizing PythonCall overhead with BenchmarkTools](https://discourse.julialang.org/t/minimizing-pythoncall-overhead-with-benchmarktools/100067):
>
> 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? using BenchmarkTools, PythonCall # define obj and x @btime $(obj).meth($x) @btime $(obj.meth)($x) @btime pycall($(obj).meth, $x) @btime pycall($(ob…

---

<div class="post-metadata">

**Author:** ![filchristou](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/filchristou/32/26760_2.png) [@filchristou](https://discourse.julialang.org/u/filchristou)\
**Post date:** [June 12, 2023, 8:01am UTC](https://discourse.julialang.org/t/pycall-minimal-overhead/41127/16 "2023-06-12T08:01:00Z")

</div>

In the end I ended up creating a temp python module with all the benchmarks as functions. I also had a python wrapper function that would use `timeit` to benchmark the functions of interest.  
Later i was calling the python wrapper function from a Pluto notebook after importing my python local module using PyCall.jl
