forked from JuliaPy/PyCall.jl
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Faster pycall. Adds pycall! (JuliaPy#492)
* Add PyFuncWrap * Add pydecref to old ret value, and fix bug in benchmark * pycall! * Faster generic pycall * PyFuncWrap -> pywrapfn * rename to pycalls.jl * More tests, better docstrings * Fix some tests and debugging stuff... * no longer needed tuplen * Revert pywrapfn back to a struct worked out why it was benchmarking slightly slower than the closure version. I think the struct version is clearer. * rename pycalls.jl to pyfncall.jl * Ensure only single ref to pyargsptr tuple * Fix bug in PyWrapFn and change to vararg * Fix pyargsptr check for empty tuple * Remove PyWrapFn * Add `pycall!` tests. Remove pywrapfn tests * Remove kwargs type in _pycall! Attempt to fix 0.7 issue * Stop using NumPy in tests * 0.7 Deprecations * Add tests for kwargs * Ptr{Nothing} to Ptr{Cvoid} * Move pycall_legacy and pywrapfn to benchmarks
- Loading branch information
Showing
7 changed files
with
409 additions
and
66 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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...)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.