Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions benchmarks/callperf.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using PyCall, BenchmarkTools, DataStructures
using PyCall: _pycall!

include("pywrapfn.jl")
include("pycall_legacy.jl")

results = OrderedDict{String,Any}()

let
np = pyimport("numpy")
nprand = np["random"]["rand"]
ret = PyNULL()
args_lens = (0,1,2,3,7,12,17)
# args_lens = (7,3,1)
# args_lens = (3,)
arr_sizes = (ntuple(i->1, len) for len in args_lens)

for (i, arr_size) in enumerate(arr_sizes)
nprand_pywrapfn = pywrapfn(nprand, length(arr_size))

pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), length(arr_size))
arr_size_str = args_lens[i] < 5 ? "$arr_size" : "$(args_lens[i])*(1,1,...)"

results["pycall_legacy $arr_size_str"] = @benchmark pycall_legacy($nprand, PyObject, $arr_size...)
println("pycall_legacy $arr_size_str:\n"); display(results["pycall_legacy $arr_size_str"])
println("--------------------------------------------------")

results["pycall $arr_size_str"] = @benchmark pycall($nprand, PyObject, $arr_size...)
println("pycall $arr_size_str:\n"); display(results["pycall $arr_size_str"])
println("--------------------------------------------------")

results["pycall! $arr_size_str"] = @benchmark pycall!($ret, $nprand, PyObject, $arr_size...)
println("pycall! $arr_size_str:\n"); display(results["pycall! $arr_size_str"])
println("--------------------------------------------------")

results["_pycall! $arr_size_str"] = @benchmark $_pycall!($ret, $pyargsptr, $nprand, $arr_size)
println("_pycall! $arr_size_str:\n"); display(results["_pycall! $arr_size_str"])
println("--------------------------------------------------")

results["nprand_pywrapfn $arr_size_str"] = @benchmark $nprand_pywrapfn($arr_size...)
println("nprand_pywrapfn $arr_size_str:\n"); display(results["nprand_pywrapfn $arr_size_str"])
println("--------------------------------------------------")

# args already set by nprand_pywrapfn calls above
results["nprand_pywrapfn_noargs $arr_size_str"] = @benchmark $nprand_pywrapfn()
println("nprand_pywrapfn_noargs $arr_size_str:\n"); display(results["nprand_pywrapfn_noargs $arr_size_str"])
println("--------------------------------------------------")
end
end
#
println("")
println("Mean times")
println("----------")
foreach((r)->println(rpad(r[1],33), "\t", mean(r[2])), results)
println("")
println("Median times")
println("----------")
foreach((r)->println(rpad(r[1],33), "\t", median(r[2])), results)
43 changes: 43 additions & 0 deletions benchmarks/pycall_legacy.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Base: sigatomic_begin, sigatomic_end
using PyCall: @pycheckz, TypeTuple

"""
Low-level version of `pycall(o, ...)` that always returns `PyObject`.
"""
function _pycall_legacy(o::Union{PyObject,PyPtr}, args...; kwargs...)
oargs = map(PyObject, args)
nargs = length(args)
sigatomic_begin()
try
arg = PyObject(@pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,),
nargs))
for i = 1:nargs
@pycheckz ccall((@pysym :PyTuple_SetItem), Cint,
(PyPtr,Int,PyPtr), arg, i-1, oargs[i])
pyincref(oargs[i]) # PyTuple_SetItem steals the reference
end
if isempty(kwargs)
ret = PyObject(@pycheckn ccall((@pysym :PyObject_Call), PyPtr,
(PyPtr,PyPtr,PyPtr), o, arg, C_NULL))
else
#kw = PyObject((AbstractString=>Any)[string(k) => v for (k, v) in kwargs])
kw = PyObject(Dict{AbstractString, Any}([Pair(string(k), v) for (k, v) in kwargs]))
ret = PyObject(@pycheckn ccall((@pysym :PyObject_Call), PyPtr,
(PyPtr,PyPtr,PyPtr), o, arg, kw))
end
return ret::PyObject
finally
sigatomic_end()
end
end

"""
pycall(o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...)

Call the given Python function (typically looked up from a module) with the given args... (of standard Julia types which are converted automatically to the corresponding Python types if possible), converting the return value to returntype (use a returntype of PyObject to return the unconverted Python object reference, or of PyAny to request an automated conversion)
"""
pycall_legacy(o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) =
return convert(returntype, _pycall_legacy(o, args...; kwargs...)) #::returntype

pycall_legacy(o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) =
return convert(PyAny, _pycall_legacy(o, args...; kwargs...))
91 changes: 91 additions & 0 deletions benchmarks/pywrapfn.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using PyCall: @pycheckn, pyincref_, __pycall!

#########################################################################
struct PyWrapFn{N, RT}
o::PyPtr
pyargsptr::PyPtr
ret::PyObject
end

function PyWrapFn(o::Union{PyObject, PyPtr}, nargs::Int, returntype::Type=PyObject)
pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs)
ret = PyNULL()
optr = o isa PyPtr ? o : o.o
pyincref_(optr)
return PyWrapFn{nargs, returntype}(optr, pyargsptr, ret)
end

(pf::PyWrapFn{N, RT})(args...) where {N, RT} =
convert(RT, _pycall!(pf.ret, pf.pyargsptr, pf.o, args, N, C_NULL))

(pf::PyWrapFn{N, RT})() where {N, RT} =
convert(RT, __pycall!(pf.ret, pf.pyargsptr, pf.o, C_NULL))

"""
```
pywrapfn(o::PyObject, nargs::Int, returntype::Type{T}=PyObject) where T
```
Wrap a callable PyObject/PyPtr possibly making calling it more performant. The
wrapped version (of type `PyWrapFn`) reduces the number of allocations made for
passing its arguments, and re-uses the same PyObject as its return value each
time it is called.

Mainly useful for functions called in a tight loop. After wrapping, arguments
should be passed in a tuple, rather than directly, e.g. `wrappedfn((a,b))` rather
than `wrappedfn(a,b)`.
Example
```
@pyimport numpy as np

# wrap a 2-arg version of np.random.rand for creating random matrices
randmatfn = pywrapfn(np.random["rand"], 2, PyArray)

# n.b. rand would normally take multiple arguments, like so:
a_random_matrix = np.random["rand"](7, 7)

# but we call the wrapped version with a tuple instead, i.e.
# rand22fn((7, 7)) not
# rand22fn(7, 7)
for i in 1:10^9
arr = rand22fn((7,7))
...
end
```
"""
pywrapfn(o::PyObject, nargs::Int, returntype::Type=PyObject) =
PyWrapFn(o, nargs, returntype)

"""
```
pysetargs!(w::PyWrapFn{N, RT}, args)
```
Set the arguments with which to call a Python function wrapped using
`w = pywrapfn(pyfun, ...)`
"""
function pysetargs!(pf::PyWrapFn{N, RT}, args) where {N, RT}
check_pyargsptr(pf)
pysetargs!(pf.pyargsptr, args, N)
end

"""
```
pysetarg!(w::PyWrapFn{N, RT}, arg, i::Integer=1)
```
Set the `i`th argument to be passed to a Python function previously
wrapped with a call to `w = pywrapfn(pyfun, ...)`
"""
function pysetarg!(pf::PyWrapFn{N, RT}, arg, i::Integer=1) where {N, RT}
check_pyargsptr(pf)
pysetarg!(pf.pyargsptr, arg, i)
end

"""
See check_pyargsptr(nargs::Int) above
"""
function check_pyargsptr(pf::PyWrapFn{N, RT}) where {N, RT}
if unsafe_load(pf.pyargsptr).ob_refcnt > 1
pydecref_(pf.pyargsptr)
pf.pyargsptr =
@pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs)
end
end
76 changes: 10 additions & 66 deletions src/PyCall.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ module PyCall

using Compat, VersionParsing

export pycall, pyimport, pyimport_e, pybuiltin, PyObject, PyReverseDims,
export pycall, pycall!, pyimport, pyimport_e, pybuiltin, PyObject, PyReverseDims,
PyPtr, pyincref, pydecref, pyversion, PyArray, PyArray_Info,
pyerr_check, pyerr_clear, pytype_query, PyAny, @pyimport, PyDict,
pyisinstance, pywrap, pytypeof, pyeval, PyVector, pystring, pystr, pyrepr,
pyraise, pytype_mapping, pygui, pygui_start, pygui_stop,
pygui_stop_all, @pylab, set!, PyTextIO, @pysym, PyNULL, ispynull, @pydef,
pyimport_conda, @py_str, @pywith, @pycall, pybytes, pyfunction, pyfunctionret
pyimport_conda, @py_str, @pywith, @pycall, pybytes, pyfunction, pyfunctionret,
pywrapfn, pysetarg!, pysetargs!

import Base: size, ndims, similar, copy, getindex, setindex!, stride,
convert, pointer, summary, convert, show, haskey, keys, values,
Expand Down Expand Up @@ -97,8 +98,13 @@ it is equivalent to a `PyNULL()` object.
"""
ispynull(o::PyObject) = o.o == PyPtr_NULL

function pydecref_(o::PyPtr)
ccall(@pysym(:Py_DecRef), Cvoid, (PyPtr,), o)
return o
end

function pydecref(o::PyObject)
ccall(@pysym(:Py_DecRef), Cvoid, (PyPtr,), o.o)
pydecref_(o.o)
o.o = PyPtr_NULL
o
end
Expand Down Expand Up @@ -689,69 +695,7 @@ function pybuiltin(name)
end

#########################################################################

"""
Low-level version of `pycall(o, ...)` that always returns `PyObject`.
"""
function _pycall(o::Union{PyObject,PyPtr}, args...; kwargs...)
oargs = map(PyObject, args)
nargs = length(args)
sigatomic_begin()
try
arg = PyObject(@pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,),
nargs))
for i = 1:nargs
@pycheckz ccall((@pysym :PyTuple_SetItem), Cint,
(PyPtr,Int,PyPtr), arg, i-1, oargs[i])
pyincref(oargs[i]) # PyTuple_SetItem steals the reference
end
if isempty(kwargs)
ret = PyObject(@pycheckn ccall((@pysym :PyObject_Call), PyPtr,
(PyPtr,PyPtr,PyPtr), o, arg, C_NULL))
else
#kw = PyObject((AbstractString=>Any)[string(k) => v for (k, v) in kwargs])
kw = PyObject(Dict{AbstractString, Any}([Pair(string(k), v) for (k, v) in kwargs]))
ret = PyObject(@pycheckn ccall((@pysym :PyObject_Call), PyPtr,
(PyPtr,PyPtr,PyPtr), o, arg, kw))
end
return ret::PyObject
finally
sigatomic_end()
end
end

"""
pycall(o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...)

Call the given Python function (typically looked up from a module) with the given args... (of standard Julia types which are converted automatically to the corresponding Python types if possible), converting the return value to returntype (use a returntype of PyObject to return the unconverted Python object reference, or of PyAny to request an automated conversion)
"""
pycall(o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) =
return convert(returntype, _pycall(o, args...; kwargs...))::returntype

pycall(o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) =
return convert(PyAny, _pycall(o, args...; kwargs...))

(o::PyObject)(args...; kws...) = pycall(o, PyAny, args...; kws...)
PyAny(o::PyObject) = convert(PyAny, o)


"""
@pycall func(args...)::T

Convenience macro which turns `func(args...)::T` into pycall(func, T, args...)
"""
macro pycall(ex)
if !(isexpr(ex,:(::)) && isexpr(ex.args[1],:call))
throw(ArgumentError("Usage: @pycall func(args...)::T"))
end
func = ex.args[1].args[1]
args, kwargs = ex.args[1].args[2:end], []
if isexpr(args[1],:parameters)
kwargs, args = args[1], args[2:end]
end
T = ex.args[2]
:(pycall($(map(esc,[kwargs; func; T; args])...)))
end
include("pyfncall.jl")

#########################################################################
# Once Julia lets us overload ".", we will use [] to access items, but
Expand Down
Loading