From dbc8105c422bfe8a7fa3e9f249b0a4acc2223e99 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Thu, 29 Mar 2018 04:38:36 +1100 Subject: [PATCH 01/22] Add PyFuncWrap --- benchmarks/callperf.jl | 45 +++++++++++++++++++++ src/PyCall.jl | 1 + src/conversions.jl | 5 +++ src/pyfuncwrap.jl | 90 +++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 2 + test/test_pyfuncwrap.jl | 29 +++++++++++++ 6 files changed, 172 insertions(+) create mode 100644 benchmarks/callperf.jl create mode 100644 src/pyfuncwrap.jl create mode 100644 test/test_pyfuncwrap.jl diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl new file mode 100644 index 00000000..4e924438 --- /dev/null +++ b/benchmarks/callperf.jl @@ -0,0 +1,45 @@ +using PyCall, BenchmarkTools, DataStructures + +results = OrderedDict{String,Any}() + +let + np = pyimport("numpy") + nprand = np["random"]["rand"] + nprand_pyo(sz...) = pycall(nprand, PyObject, sz...) + nprand2d_wrap = PyFuncWrap(nprand, (Int, Int)) + + arr_size = (2,2) + + results["nprand_pyo"] = @benchmark $nprand_pyo($arr_size...) + println("nprand_pyo:\n"); display(results["nprand_pyo"]) + println("--------------------------------------------------") + + results["nprand2d_wrap"] = @benchmark $nprand2d_wrap($arr_size...) + println("nprand2d_wrap:\n"); display(results["nprand2d_wrap"]) + println("--------------------------------------------------") + + # args already set by nprand2d_wrap calls above + results["nprand2d_wrap_noargs"] = @benchmark $nprand2d_wrap() + println("nprand2d_wrap_noargs:\n"); display(results["nprand2d_wrap_noargs"]) + println("--------------------------------------------------") + + arr_size = ntuple(i->2, 15) + + results["nprand_pyo2"] = @benchmark $nprand_pyo($arr_size...) + println("nprand_pyo2:\n"); display(results["nprand_pyo2"]) + println("--------------------------------------------------") + + results["nprand2d_wrap2"] = @benchmark $nprand2d_wrap($arr_size...) + println("nprand2d_wrap2:\n"); display(results["nprand2d_wrap2"]) + println("--------------------------------------------------") + + # args already set by nprand2d_wrap calls above + results["nprand2d_wrap_noargs2"] = @benchmark $nprand2d_wrap() + println("nprand2d_wrap_noargs2:\n"); display(results["nprand2d_wrap_noargs2"]) + println("--------------------------------------------------") +end + +println("") +println("Mean times") +println("----------") +foreach((r)->println(rpad(r[1],23), ": ", mean(r[2])), results) diff --git a/src/PyCall.jl b/src/PyCall.jl index 37cd10c2..14a3bc8b 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -169,6 +169,7 @@ include("pytype.jl") include("pyiterator.jl") include("pyclass.jl") include("callback.jl") +include("pyfuncwrap.jl") include("io.jl") ######################################################################### diff --git a/src/conversions.jl b/src/conversions.jl index 5f4530e2..6e6aad14 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -214,6 +214,11 @@ end # somewhat annoying to get the length and types in a tuple type # ... would be better not to have to use undocumented internals! +function tuplen(T::DataType) + isvatuple(T) && ArgumentError("can't determine length of vararg tuple: $T") + return length(T.parameters) +end +tuplen(T::UnionAll) = tuplen(T.body) istuplen(T,isva,n) = isva ? n ≥ length(T.parameters)-1 : n == length(T.parameters) function tuptype(T::DataType,isva,i) if isva && i ≥ length(T.parameters) diff --git a/src/pyfuncwrap.jl b/src/pyfuncwrap.jl new file mode 100644 index 00000000..7279a269 --- /dev/null +++ b/src/pyfuncwrap.jl @@ -0,0 +1,90 @@ +struct PyFuncWrap{P<:Union{PyObject,PyPtr}, AT<:Tuple, N, RT} + o::P + oargs::Vector{PyObject} + pyargsptr::PyPtr + ret::PyObject +end + +""" +``` +PyFuncWrap(o::P, argtypes::Tuple #= of Types =#, returntype::Type) +``` + +Wrap a callable PyObject/PyPtr to reduce the number of allocations made for +passing its arguments, and its return value, sometimes providing a speedup. +Mainly useful for functions called in a tight loop, particularly if most or +all of the arguments to the function don't change. +``` +@pyimport numpy as np +rand22fn = PyFuncWrap(np.random["rand"], (Int, Int), PyArray) +setargs!(rand22fn, 2, 2) +for i in 1:10^9 + arr = rand22fn() + ... +end +``` +""" +function PyFuncWrap(o::P, argtypes::Tuple{Vararg{<:Union{Tuple, Type}}}, + returntype::Type{RT}=PyObject) where {P<:Union{PyObject,PyPtr}, RT} + AT = typeof(argtypes) + isvatuple(AT) && throw(ArgumentError("Vararg functions not supported, arg signature provided: $AT")) + N = tuplen(AT) + oargs = Array{PyObject}(N) + pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), N) + return PyFuncWrap{P, AT, N, RT}(o, oargs, pyargsptr, PyNULL()) +end + +""" +``` +setargs!(pf::PyFuncWrap, args...) +``` +Set the arguments to a python function wrapped in a PyFuncWrap, and convert them +to `PyObject`s that can be passed directly to python when the function is +called. After the arguments have been set, the function can be efficiently +called with `pf()` +""" +function setargs!(pf::PyFuncWrap{P, AT, N, RT}, args...) where {P, AT, RT, N} + for i = 1:N + setarg!(pf, args[i], i) + end + nothing +end + +""" +``` +setarg!(pf::PyFuncWrap, arg, i::Integer=1) +``` +Set the `i`th argument to a python function wrapped in a PyFuncWrap, and convert +it to a `PyObject` that can be passed directly to python when the function is +called. Useful if a function takes multiple arguments, but only one or two of +them change, when calling the function in a tight loop +""" +function setarg!(pf::PyFuncWrap{P, AT, N, RT}, arg, i::Integer=1) where {P, AT, N, RT} + pf.oargs[i] = PyObject(arg) + @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, + (PyPtr,Int,PyPtr), pf.pyargsptr, i-1, pf.oargs[i]) + pyincref(pf.oargs[i]) # PyTuple_SetItem steals the reference + nothing +end + +function (pf::PyFuncWrap{P, AT, N, RT})(args...) where {P, AT, N, RT} + setargs!(pf, args...) + return pf() +end + +""" +Warning: if pf(args) or setargs(pf, ...) hasn't been called yet, this will likely segfault +""" +function (pf::PyFuncWrap{P, AT, N, RT})() where {P, AT, N, RT} + sigatomic_begin() + try + kw = C_NULL + retptr = ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), pf.o, + pf.pyargsptr, kw) + pyincref_(retptr) + pf.ret.o = retptr + finally + sigatomic_end() + end + convert(RT, pf.ret) +end diff --git a/test/runtests.jl b/test/runtests.jl index 3db84b83..cf35fe60 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -560,3 +560,5 @@ end @test_throws ErrorException @pywith IgnoreError(false) error() @test (@pywith IgnoreError(true) error(); true) end + +include("test_pyfuncwrap.jl") \ No newline at end of file diff --git a/test/test_pyfuncwrap.jl b/test/test_pyfuncwrap.jl new file mode 100644 index 00000000..ed213d54 --- /dev/null +++ b/test/test_pyfuncwrap.jl @@ -0,0 +1,29 @@ +using Compat.Test, PyCall + +@testset "PyFuncWrap" begin + np = pyimport("numpy") + ops = pyimport("operator") + eq = ops["eq"] + npzeros = np["zeros"] + npzeros_pyo(sz, dtype="d", order="F") = pycall(npzeros, PyObject, sz, dtype, order) + npzeros_pyany(sz, dtype="d", order="F") = pycall(npzeros, PyAny, sz, dtype, order) + npzeros_pyarray(sz, dtype="d", order="F") = pycall(npzeros, PyArray, sz, dtype, order) + + # PyObject is default returntype + npzeros2dwrap_pyo = PyFuncWrap(npzeros, ((Int, Int), String, String)) + npzeros2dwrap_pyany = PyFuncWrap(npzeros, ((Int, Int), String, String), PyAny) + npzeros2dwrap_pyarray = PyFuncWrap(npzeros, ((Int, Int), String, String), PyArray) + + arr_size = (2,2) + + # all args + @test np["array_equal"](npzeros2dwrap_pyo(arr_size, "d", "F"), npzeros_pyo(arr_size)) + # args already set + @test np["array_equal"](npzeros2dwrap_pyo(), npzeros_pyo(arr_size)) + + @test all(npzeros2dwrap_pyany(arr_size, "d", "F") .== npzeros_pyany(arr_size)) + @test all(npzeros2dwrap_pyany() .== npzeros_pyany(arr_size)) + + @test all(npzeros2dwrap_pyarray(arr_size, "d", "F") .== npzeros_pyarray(arr_size)) + @test all(npzeros2dwrap_pyarray() .== npzeros_pyarray(arr_size)) +end From fc9fe817b40f79c3766480a97a48891f4e8dd637 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 11 Apr 2018 22:26:05 +1000 Subject: [PATCH 02/22] Add pydecref to old ret value, and fix bug in benchmark --- benchmarks/callperf.jl | 54 +++++++++++++++++------------------------- src/pyfuncwrap.jl | 1 + 2 files changed, 23 insertions(+), 32 deletions(-) diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index 4e924438..1f0f4b00 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -6,40 +6,30 @@ let np = pyimport("numpy") nprand = np["random"]["rand"] nprand_pyo(sz...) = pycall(nprand, PyObject, sz...) - nprand2d_wrap = PyFuncWrap(nprand, (Int, Int)) - - arr_size = (2,2) - - results["nprand_pyo"] = @benchmark $nprand_pyo($arr_size...) - println("nprand_pyo:\n"); display(results["nprand_pyo"]) - println("--------------------------------------------------") - - results["nprand2d_wrap"] = @benchmark $nprand2d_wrap($arr_size...) - println("nprand2d_wrap:\n"); display(results["nprand2d_wrap"]) - println("--------------------------------------------------") - - # args already set by nprand2d_wrap calls above - results["nprand2d_wrap_noargs"] = @benchmark $nprand2d_wrap() - println("nprand2d_wrap_noargs:\n"); display(results["nprand2d_wrap_noargs"]) - println("--------------------------------------------------") - - arr_size = ntuple(i->2, 15) - - results["nprand_pyo2"] = @benchmark $nprand_pyo($arr_size...) - println("nprand_pyo2:\n"); display(results["nprand_pyo2"]) - println("--------------------------------------------------") - - results["nprand2d_wrap2"] = @benchmark $nprand2d_wrap($arr_size...) - println("nprand2d_wrap2:\n"); display(results["nprand2d_wrap2"]) - println("--------------------------------------------------") - - # args already set by nprand2d_wrap calls above - results["nprand2d_wrap_noargs2"] = @benchmark $nprand2d_wrap() - println("nprand2d_wrap_noargs2:\n"); display(results["nprand2d_wrap_noargs2"]) - println("--------------------------------------------------") + ret = PyNULL() + args_lens = (0,3,7,12,17) + arr_sizes = (ntuple(i->1, len) for len in args_lens) + nprand_wraps = [PyFuncWrap(nprand, map(typeof, arr_size)) for arr_size in arr_sizes] + @show typeof(nprand_wraps) + for (i, arr_size) in enumerate(arr_sizes) + nprand_wrap = nprand_wraps[i] + arr_size_str = args_lens[i] < 5 ? "$arr_size" : "$(args_lens[i])*(1,1,...)" + results["nprand_pyo $arr_size_str"] = @benchmark $nprand_pyo($arr_size...) + println("nprand_pyo $arr_size_str:\n"); display(results["nprand_pyo $arr_size_str"]) + println("--------------------------------------------------") + + results["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size...) + println("nprand_wrap $arr_size_str:\n"); display(results["nprand_wrap $arr_size_str"]) + println("--------------------------------------------------") + + # args already set by nprand_wrap calls above + results["nprand_wrap_noargs $arr_size_str"] = @benchmark $nprand_wrap() + println("nprand_wrap_noargs $arr_size_str:\n"); display(results["nprand_wrap_noargs $arr_size_str"]) + println("--------------------------------------------------") + end end println("") println("Mean times") println("----------") -foreach((r)->println(rpad(r[1],23), ": ", mean(r[2])), results) +foreach((r)->println(rpad(r[1],33), ": ", mean(r[2])), results) diff --git a/src/pyfuncwrap.jl b/src/pyfuncwrap.jl index 7279a269..6d118cbc 100644 --- a/src/pyfuncwrap.jl +++ b/src/pyfuncwrap.jl @@ -82,6 +82,7 @@ function (pf::PyFuncWrap{P, AT, N, RT})() where {P, AT, N, RT} retptr = ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), pf.o, pf.pyargsptr, kw) pyincref_(retptr) + pydecref(pf.ret) pf.ret.o = retptr finally sigatomic_end() From 98db8eb92c04a4b6f863563330ef5dac62af0eed Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 11 Apr 2018 22:34:01 +1000 Subject: [PATCH 03/22] pycall! --- benchmarks/callperf.jl | 5 ++++ src/PyCall.jl | 52 +++++++++++++++++++++++++++++++++++++++++- src/pyinit.jl | 2 ++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index 1f0f4b00..85e9e6f2 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -6,6 +6,7 @@ let np = pyimport("numpy") nprand = np["random"]["rand"] nprand_pyo(sz...) = pycall(nprand, PyObject, sz...) + nprand_pyo!(ret::PyObject, sz...) = pycall!(ret, nprand, PyObject, sz...) ret = PyNULL() args_lens = (0,3,7,12,17) arr_sizes = (ntuple(i->1, len) for len in args_lens) @@ -18,6 +19,10 @@ let println("nprand_pyo $arr_size_str:\n"); display(results["nprand_pyo $arr_size_str"]) println("--------------------------------------------------") + results["nprand_pyo! $arr_size_str"] = @benchmark $nprand_pyo!($ret, $arr_size...) + println("nprand_pyo! $arr_size_str:\n"); display(results["nprand_pyo! $arr_size_str"]) + println("--------------------------------------------------") + results["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size...) println("nprand_wrap $arr_size_str:\n"); display(results["nprand_wrap $arr_size_str"]) println("--------------------------------------------------") diff --git a/src/PyCall.jl b/src/PyCall.jl index 14a3bc8b..d80ff87d 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -4,7 +4,7 @@ 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, @@ -690,6 +690,56 @@ function pybuiltin(name) end ######################################################################### +const oargs = Array{PyObject}(32) +const pyargsref = Ref{PyPtr}() +const cur_args_size = Ref{Int}(0) +""" +Low-level version of `pycall!(ret, o, ...)` that always returns `PyObject`. +""" +function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args...; kwargs...) + nargs = length(args) + # oargs = Array{PyObject}(nargs) + sigatomic_begin() + try + if nargs != cur_args_size[] + @pycheckz ccall((@pysym :_PyTuple_Resize), Cint, + (Ptr{PyPtr}, Int,), pyargsref, nargs) + cur_args_size[] = nargs + end + # pyincref_(pyargsref[]) + # pyargs = PyObject(pyargsref[]) # change to pyincref_ (and pydecref later) to avoid the PyObject? + + for i = 1:nargs + oargs[i] = PyObject(args[i]) + @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, + (PyPtr,Int,PyPtr), pyargsref[], i-1, oargs[i]) + pyincref(oargs[i]) # PyTuple_SetItem steals the reference + end + # if isempty(kwargs) + kw = C_NULL + # else + # kw = PyObject(Dict{AbstractString, Any}([Pair(string(k), v) for (k, v) in kwargs])) + # end + retptr = @pycheckn ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), o, + pyargsref[], kw) + pyincref_(retptr) + pydecref(ret) + ret.o = retptr + return ret #::PyObject + finally + sigatomic_end() + end +end + +pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) = + return convert(returntype, _pycall!(pyobj, o, args...; kwargs...)) + +pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, returntype::Type{PyObject}, + args...; kwargs...) = return _pycall!(pyobj, o, args...; kwargs...) + +pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = + return convert(PyAny, _pycall!(pyobj, o, args...; kwargs...)) + """ Low-level version of `pycall(o, ...)` that always returns `PyObject`. diff --git a/src/pyinit.jl b/src/pyinit.jl index 7515b12c..4d655a51 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -143,6 +143,8 @@ function __init__() 1, [""], 0) end + pyargsref[] = ccall((@pysym :PyTuple_New), PyPtr, (Int,), 0) + # Some Python code checks sys.ps1 to see if it is running # interactively, and refuses to be interactive otherwise. # (e.g. Matplotlib: see PyPlot#79) From e0ca826f91f7ce889747119cc25373bfa354d470 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Fri, 13 Apr 2018 16:08:02 +1000 Subject: [PATCH 04/22] Faster generic pycall --- benchmarks/callperf.jl | 70 ++++++++++++++---- src/PyCall.jl | 80 +------------------- src/pyfuncwrap.jl | 162 ++++++++++++++++++++++++++++++++--------- src/pyinit.jl | 2 - 4 files changed, 190 insertions(+), 124 deletions(-) diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index 85e9e6f2..b9ba40a4 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -1,29 +1,44 @@ using PyCall, BenchmarkTools, DataStructures +using PyCall: _pycall! results = OrderedDict{String,Any}() +@inline nprand_pyo(o::PyObject, sz...) = pycall(o, PyObject, sz...) +# nprand_pyo!(ret::PyObject, sz...) = pycall!(ret, nprand, PyObject, sz...) +# _nprand_pyo!(ret::PyObject, sz...) = _pycall!(ret, nprand, sz...) +@inline nprand_pyo!(ret::PyObject, o::PyObject, sz...) = pycall!(ret, o, PyObject, sz...) +@inline _nprand_pyo!(ret::PyObject, o::PyObject, pyargsptr::PyPtr, sz...) = + _pycall!(ret, pyargsptr, o, sz) + + +function fastercall(o::PyObject, nargs::Int) + pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) + ret = PyNULL() + (args) -> _pycall!(ret, pyargsptr, o, args, nargs, C_NULL) +end let np = pyimport("numpy") nprand = np["random"]["rand"] - nprand_pyo(sz...) = pycall(nprand, PyObject, sz...) - nprand_pyo!(ret::PyObject, sz...) = pycall!(ret, nprand, PyObject, sz...) ret = PyNULL() - args_lens = (0,3,7,12,17) + # args_lens = (0,3,7,12,17) + # args_lens = (1,3,7) + nargs = 7 + args_lens = (nargs,) arr_sizes = (ntuple(i->1, len) for len in args_lens) nprand_wraps = [PyFuncWrap(nprand, map(typeof, arr_size)) for arr_size in arr_sizes] - @show typeof(nprand_wraps) + pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) + + # oargs = Array{PyObject}(7) + nprand_pyo!(ret, nprand, first(arr_sizes)...) + _nprand_pyo!(ret, nprand, pyargsptr, first(arr_sizes)...) + # Profile.clear_malloc_data() + # @code_warntype _pycall!(ret, nprand, first(arr_sizes)...) for (i, arr_size) in enumerate(arr_sizes) nprand_wrap = nprand_wraps[i] + fastrand = fastercall(nprand, length(arr_size)) arr_size_str = args_lens[i] < 5 ? "$arr_size" : "$(args_lens[i])*(1,1,...)" - results["nprand_pyo $arr_size_str"] = @benchmark $nprand_pyo($arr_size...) - println("nprand_pyo $arr_size_str:\n"); display(results["nprand_pyo $arr_size_str"]) - println("--------------------------------------------------") - results["nprand_pyo! $arr_size_str"] = @benchmark $nprand_pyo!($ret, $arr_size...) - println("nprand_pyo! $arr_size_str:\n"); display(results["nprand_pyo! $arr_size_str"]) - println("--------------------------------------------------") - - results["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size...) + results["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size) println("nprand_wrap $arr_size_str:\n"); display(results["nprand_wrap $arr_size_str"]) println("--------------------------------------------------") @@ -31,10 +46,39 @@ let results["nprand_wrap_noargs $arr_size_str"] = @benchmark $nprand_wrap() println("nprand_wrap_noargs $arr_size_str:\n"); display(results["nprand_wrap_noargs $arr_size_str"]) println("--------------------------------------------------") + + # results["nprand_pyo $arr_size_str"] = @benchmark $nprand_pyo($arr_size...) + # println("nprand_pyo $arr_size_str:\n"); display(results["nprand_pyo $arr_size_str"]) + # println("--------------------------------------------------") + + results["nprand_pyo! $arr_size_str"] = @benchmark $nprand_pyo!($ret, $nprand, $arr_size...) + println("nprand_pyo! $arr_size_str:\n"); display(results["nprand_pyo! $arr_size_str"]) + println("--------------------------------------------------") + + results["fastrand $arr_size_str"] = @benchmark $fastrand($arr_size) + println("fastrand $arr_size_str:\n"); display(results["fastrand $arr_size_str"]) + println("--------------------------------------------------") + + results["_nprand_pyo! $arr_size_str"] = @benchmark $_nprand_pyo!($ret, $nprand, $pyargsptr, $arr_size...) + println("_nprand_pyo! $arr_size_str:\n"); display(results["_nprand_pyo! $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("--------------------------------------------------") + # _nprand_pyo!(ret, pyargsptr, oargs, arr_size...) end end - +# println("") println("Mean times") println("----------") foreach((r)->println(rpad(r[1],33), ": ", mean(r[2])), results) +println("") +println("Median times") +println("----------") +foreach((r)->println(rpad(r[1],33), ": ", median(r[2])), results) diff --git a/src/PyCall.jl b/src/PyCall.jl index d80ff87d..16621bba 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -169,7 +169,6 @@ include("pytype.jl") include("pyiterator.jl") include("pyclass.jl") include("callback.jl") -include("pyfuncwrap.jl") include("io.jl") ######################################################################### @@ -690,61 +689,12 @@ function pybuiltin(name) end ######################################################################### -const oargs = Array{PyObject}(32) -const pyargsref = Ref{PyPtr}() -const cur_args_size = Ref{Int}(0) -""" -Low-level version of `pycall!(ret, o, ...)` that always returns `PyObject`. -""" -function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args...; kwargs...) - nargs = length(args) - # oargs = Array{PyObject}(nargs) - sigatomic_begin() - try - if nargs != cur_args_size[] - @pycheckz ccall((@pysym :_PyTuple_Resize), Cint, - (Ptr{PyPtr}, Int,), pyargsref, nargs) - cur_args_size[] = nargs - end - # pyincref_(pyargsref[]) - # pyargs = PyObject(pyargsref[]) # change to pyincref_ (and pydecref later) to avoid the PyObject? - - for i = 1:nargs - oargs[i] = PyObject(args[i]) - @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, - (PyPtr,Int,PyPtr), pyargsref[], i-1, oargs[i]) - pyincref(oargs[i]) # PyTuple_SetItem steals the reference - end - # if isempty(kwargs) - kw = C_NULL - # else - # kw = PyObject(Dict{AbstractString, Any}([Pair(string(k), v) for (k, v) in kwargs])) - # end - retptr = @pycheckn ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), o, - pyargsref[], kw) - pyincref_(retptr) - pydecref(ret) - ret.o = retptr - return ret #::PyObject - finally - sigatomic_end() - end -end - -pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) = - return convert(returntype, _pycall!(pyobj, o, args...; kwargs...)) - -pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, returntype::Type{PyObject}, - args...; kwargs...) = return _pycall!(pyobj, o, args...; kwargs...) - -pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = - return convert(PyAny, _pycall!(pyobj, o, args...; kwargs...)) - +include("pyfuncwrap.jl") """ Low-level version of `pycall(o, ...)` that always returns `PyObject`. """ -function _pycall(o::Union{PyObject,PyPtr}, args...; kwargs...) +function _pycall_legacy(o::Union{PyObject,PyPtr}, args...; kwargs...) oargs = map(PyObject, args) nargs = length(args) sigatomic_begin() @@ -776,34 +726,12 @@ end 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...) = +pycall_legacy(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...) = +pycall_legacy(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 - ######################################################################### # Once Julia lets us overload ".", we will use [] to access items, but # for now we can define "get". diff --git a/src/pyfuncwrap.jl b/src/pyfuncwrap.jl index 6d118cbc..99b49be2 100644 --- a/src/pyfuncwrap.jl +++ b/src/pyfuncwrap.jl @@ -1,6 +1,111 @@ -struct PyFuncWrap{P<:Union{PyObject,PyPtr}, AT<:Tuple, N, RT} + +# pyarg_tuples[i] is a pointer to a python tuple of length i-1 +# const pyarg_tuples = Vector{PyPtr}(32) +const pyarg_tuples = PyPtr[] +# const cur_args_size = Ref{Int}(0) +""" +You have the args, but kwargs need to be converted to a Ptr to a Python dict, or +C_NULL if empty +""" +function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, kwargs::Vector{Any}) + if isempty(kwargs) + kw = C_NULL + else + kw = PyObject(Dict{AbstractString, Any}([Pair(string(k), v) for (k, v) in kwargs])) + end + _pycall!(ret, o, args, length(args), kw) +end + +""" +You have the args, and kwargs is in python friendly format, but +you don't yet have the python tuple to hold the arguments. +""" +function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, + nargs::Int=length(args), kw::Union{Ptr{Void}, PyObject}=C_NULL) + # pyarg_tuples[i] is a pointer to a python tuple of length i-1 + for n in length(pyarg_tuples):nargs + push!(pyarg_tuples, @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), n)) + end + pyargsptr = pyarg_tuples[nargs+1] + return _pycall!(ret, pyargsptr, o, args, nargs, kw) #::PyObject +end + +""" +You have the args, and kwargs is in python friendly format, and +you have the python tuple to hold the arguments (pyargsptr), you just haven't +set the tuples values to the python version of your arguments yet +""" +function _pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, + args, nargs::Int=length(args), kw::Union{Ptr{Void}, PyObject}=C_NULL) + setargs!(pyargsptr, args, nargs) + return __pycall!(ret, pyargsptr, o, kw) #::PyObject +end + +""" +You're all frocked up and ready for the dance. `pyargsptr` and `kw` have all +their args set to Python values, so we can call the function. +""" +function __pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, + kw::Union{Ptr{Void}, PyObject}) + sigatomic_begin() + try + retptr = @pycheckn ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), o, + pyargsptr, kw) + pyincref_(retptr) + pydecref(ret) + ret.o = retptr + finally + sigatomic_end() + end + return ret #::PyObject +end + +pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) = + return convert(returntype, _pycall!(pyobj, o, args, kwargs)) + +function pycall!(pyobj::PyObject, o::T, returntype::Type{PyObject}, args...; kwargs...) where + T<:Union{PyObject,PyPtr} + return _pycall!(pyobj, o, args, kwargs) +end + +pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = + return convert(PyAny, _pycall!(pyobj, o, args, kwargs)) + +""" + 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(PyNULL(), o, args, kwargs))::returntype + +pycall(o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = + return convert(PyAny, _pycall(PyNULL(), 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 + +######################################################################### +struct PyFuncWrap{P<:Union{PyObject,PyPtr}, N, RT} o::P - oargs::Vector{PyObject} pyargsptr::PyPtr ret::PyObject end @@ -26,66 +131,57 @@ end """ function PyFuncWrap(o::P, argtypes::Tuple{Vararg{<:Union{Tuple, Type}}}, returntype::Type{RT}=PyObject) where {P<:Union{PyObject,PyPtr}, RT} - AT = typeof(argtypes) - isvatuple(AT) && throw(ArgumentError("Vararg functions not supported, arg signature provided: $AT")) - N = tuplen(AT) - oargs = Array{PyObject}(N) - pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), N) - return PyFuncWrap{P, AT, N, RT}(o, oargs, pyargsptr, PyNULL()) + N = length(argtypes) + pyargsptr = @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), N) + return PyFuncWrap{P, N, RT}(o, pyargsptr, PyNULL()) end """ ``` -setargs!(pf::PyFuncWrap, args...) +setargs!(pyargsptr::PyPtr, args...) ``` Set the arguments to a python function wrapped in a PyFuncWrap, and convert them to `PyObject`s that can be passed directly to python when the function is called. After the arguments have been set, the function can be efficiently called with `pf()` """ -function setargs!(pf::PyFuncWrap{P, AT, N, RT}, args...) where {P, AT, RT, N} +function setargs!(pf::PyFuncWrap{P, N, RT}, args) where {P, N, RT} for i = 1:N - setarg!(pf, args[i], i) + setarg!(pf.pyargsptr, args[i], i) + end + nothing +end + +function setargs!(pyargsptr::PyPtr, args::Tuple, N::Int) + for i = 1:N + setarg!(pyargsptr, args[i], i) end nothing end """ ``` -setarg!(pf::PyFuncWrap, arg, i::Integer=1) +setarg!(pyargsptr::PyPtr, arg, i::Integer=1) ``` Set the `i`th argument to a python function wrapped in a PyFuncWrap, and convert it to a `PyObject` that can be passed directly to python when the function is called. Useful if a function takes multiple arguments, but only one or two of them change, when calling the function in a tight loop """ -function setarg!(pf::PyFuncWrap{P, AT, N, RT}, arg, i::Integer=1) where {P, AT, N, RT} - pf.oargs[i] = PyObject(arg) +function setarg!(pyargsptr::PyPtr, arg, i::Integer=1) + pyarg = PyObject(arg) @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, - (PyPtr,Int,PyPtr), pf.pyargsptr, i-1, pf.oargs[i]) - pyincref(pf.oargs[i]) # PyTuple_SetItem steals the reference - nothing + (PyPtr,Int,PyPtr), pyargsptr, i-1, pyarg) + pyincref(pyarg) # PyTuple_SetItem steals the reference end -function (pf::PyFuncWrap{P, AT, N, RT})(args...) where {P, AT, N, RT} - setargs!(pf, args...) +function (pf::PyFuncWrap{P, N, RT})(args) where {P, N, RT} + setargs!(pf, args) return pf() end """ Warning: if pf(args) or setargs(pf, ...) hasn't been called yet, this will likely segfault """ -function (pf::PyFuncWrap{P, AT, N, RT})() where {P, AT, N, RT} - sigatomic_begin() - try - kw = C_NULL - retptr = ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), pf.o, - pf.pyargsptr, kw) - pyincref_(retptr) - pydecref(pf.ret) - pf.ret.o = retptr - finally - sigatomic_end() - end - convert(RT, pf.ret) -end +(pf::PyFuncWrap{P, N, RT})() where {P, N, RT} = + convert(RT, __pycall!(pf.ret, pf.pyargsptr, pf.o, C_NULL)) # C_NULL is keywords diff --git a/src/pyinit.jl b/src/pyinit.jl index 4d655a51..7515b12c 100644 --- a/src/pyinit.jl +++ b/src/pyinit.jl @@ -143,8 +143,6 @@ function __init__() 1, [""], 0) end - pyargsref[] = ccall((@pysym :PyTuple_New), PyPtr, (Int,), 0) - # Some Python code checks sys.ps1 to see if it is running # interactively, and refuses to be interactive otherwise. # (e.g. Matplotlib: see PyPlot#79) From 573a43e081313a63e81f024250e4d2c968c8f79a Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Fri, 13 Apr 2018 17:03:47 +1000 Subject: [PATCH 05/22] PyFuncWrap -> pywrapfn --- benchmarks/callperf.jl | 65 ++++++++++---------------------------- src/PyCall.jl | 7 +++-- src/pyfuncwrap.jl | 71 +++++++++++++++++------------------------- 3 files changed, 49 insertions(+), 94 deletions(-) diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index b9ba40a4..4cb3be49 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -1,66 +1,27 @@ using PyCall, BenchmarkTools, DataStructures -using PyCall: _pycall! +using PyCall: _pycall!, pycall_legacy results = OrderedDict{String,Any}() -@inline nprand_pyo(o::PyObject, sz...) = pycall(o, PyObject, sz...) -# nprand_pyo!(ret::PyObject, sz...) = pycall!(ret, nprand, PyObject, sz...) -# _nprand_pyo!(ret::PyObject, sz...) = _pycall!(ret, nprand, sz...) -@inline nprand_pyo!(ret::PyObject, o::PyObject, sz...) = pycall!(ret, o, PyObject, sz...) -@inline _nprand_pyo!(ret::PyObject, o::PyObject, pyargsptr::PyPtr, sz...) = - _pycall!(ret, pyargsptr, o, sz) - - -function fastercall(o::PyObject, nargs::Int) - pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) - ret = PyNULL() - (args) -> _pycall!(ret, pyargsptr, o, args, nargs, C_NULL) -end let np = pyimport("numpy") nprand = np["random"]["rand"] ret = PyNULL() - # args_lens = (0,3,7,12,17) + args_lens = (0,1,2,3,7,12,17) # args_lens = (1,3,7) - nargs = 7 - args_lens = (nargs,) arr_sizes = (ntuple(i->1, len) for len in args_lens) - nprand_wraps = [PyFuncWrap(nprand, map(typeof, arr_size)) for arr_size in arr_sizes] - pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) - # oargs = Array{PyObject}(7) - nprand_pyo!(ret, nprand, first(arr_sizes)...) - _nprand_pyo!(ret, nprand, pyargsptr, first(arr_sizes)...) - # Profile.clear_malloc_data() - # @code_warntype _pycall!(ret, nprand, first(arr_sizes)...) for (i, arr_size) in enumerate(arr_sizes) - nprand_wrap = nprand_wraps[i] - fastrand = fastercall(nprand, length(arr_size)) + nprand_wrap = 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["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size) - println("nprand_wrap $arr_size_str:\n"); display(results["nprand_wrap $arr_size_str"]) + 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("--------------------------------------------------") - # args already set by nprand_wrap calls above - results["nprand_wrap_noargs $arr_size_str"] = @benchmark $nprand_wrap() - println("nprand_wrap_noargs $arr_size_str:\n"); display(results["nprand_wrap_noargs $arr_size_str"]) - println("--------------------------------------------------") - - # results["nprand_pyo $arr_size_str"] = @benchmark $nprand_pyo($arr_size...) - # println("nprand_pyo $arr_size_str:\n"); display(results["nprand_pyo $arr_size_str"]) - # println("--------------------------------------------------") - - results["nprand_pyo! $arr_size_str"] = @benchmark $nprand_pyo!($ret, $nprand, $arr_size...) - println("nprand_pyo! $arr_size_str:\n"); display(results["nprand_pyo! $arr_size_str"]) - println("--------------------------------------------------") - - results["fastrand $arr_size_str"] = @benchmark $fastrand($arr_size) - println("fastrand $arr_size_str:\n"); display(results["fastrand $arr_size_str"]) - println("--------------------------------------------------") - - results["_nprand_pyo! $arr_size_str"] = @benchmark $_nprand_pyo!($ret, $nprand, $pyargsptr, $arr_size...) - println("_nprand_pyo! $arr_size_str:\n"); display(results["_nprand_pyo! $arr_size_str"]) + 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...) @@ -70,7 +31,15 @@ let 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("--------------------------------------------------") - # _nprand_pyo!(ret, pyargsptr, oargs, arr_size...) + + results["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size) + println("nprand_wrap $arr_size_str:\n"); display(results["nprand_wrap $arr_size_str"]) + println("--------------------------------------------------") + + # args already set by nprand_wrap calls above + results["nprand_wrap_noargs $arr_size_str"] = @benchmark $nprand_wrap() + println("nprand_wrap_noargs $arr_size_str:\n"); display(results["nprand_wrap_noargs $arr_size_str"]) + println("--------------------------------------------------") end end # diff --git a/src/PyCall.jl b/src/PyCall.jl index 16621bba..990a3184 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -10,7 +10,8 @@ export pycall, pycall!, pyimport, pyimport_e, pybuiltin, PyObject, PyReverseDims 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, setarg!, setargs! import Base: size, ndims, similar, copy, getindex, setindex!, stride, convert, pointer, summary, convert, show, haskey, keys, values, @@ -727,10 +728,10 @@ end 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(o, args...; kwargs...))::returntype + return convert(returntype, _pycall_legacy(o, args...; kwargs...)) #::returntype pycall_legacy(o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = - return convert(PyAny, _pycall(o, args...; kwargs...)) + return convert(PyAny, _pycall_legacy(o, args...; kwargs...)) ######################################################################### # Once Julia lets us overload ".", we will use [] to access items, but diff --git a/src/pyfuncwrap.jl b/src/pyfuncwrap.jl index 99b49be2..5318084f 100644 --- a/src/pyfuncwrap.jl +++ b/src/pyfuncwrap.jl @@ -77,10 +77,10 @@ pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwarg 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(PyNULL(), o, args, kwargs))::returntype + return convert(returntype, _pycall!(PyNULL(), o, args, kwargs))::returntype pycall(o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = - return convert(PyAny, _pycall(PyNULL(), o, args, kwargs)) + return convert(PyAny, _pycall!(PyNULL(), o, args, kwargs)) (o::PyObject)(args...; kws...) = pycall(o, PyAny, args...; kws...) PyAny(o::PyObject) = convert(PyAny, o) @@ -104,54 +104,50 @@ macro pycall(ex) end ######################################################################### -struct PyFuncWrap{P<:Union{PyObject,PyPtr}, N, RT} - o::P - pyargsptr::PyPtr - ret::PyObject -end - """ ``` -PyFuncWrap(o::P, argtypes::Tuple #= of Types =#, returntype::Type) +pywrapfn(o::PyObject, nargs::Int, returntype::Type{T}=PyObject) where T ``` Wrap a callable PyObject/PyPtr to reduce the number of allocations made for passing its arguments, and its return value, sometimes providing a speedup. -Mainly useful for functions called in a tight loop, particularly if most or -all of the arguments to the function don't change. +Mainly useful for functions called in a tight loop. After wrapping, arguments +should be passed in tuple, rather than directly, e.g. `wrappedfn((a,b))` rather +than `wrappedfn(a,b)` ``` @pyimport numpy as np -rand22fn = PyFuncWrap(np.random["rand"], (Int, Int), PyArray) -setargs!(rand22fn, 2, 2) + +# 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((4, 4)) not +# rand22fn(4, 4) for i in 1:10^9 - arr = rand22fn() + arr = rand22fn((4, 4)) ... end ``` """ -function PyFuncWrap(o::P, argtypes::Tuple{Vararg{<:Union{Tuple, Type}}}, - returntype::Type{RT}=PyObject) where {P<:Union{PyObject,PyPtr}, RT} - N = length(argtypes) - pyargsptr = @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), N) - return PyFuncWrap{P, N, RT}(o, pyargsptr, PyNULL()) +function pywrapfn(o::PyObject, nargs::Int, returntype::Type{T}=PyObject) where T + pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) + ret = PyNULL() + wrappedfn(args) = convert(T, _pycall!(ret, pyargsptr, o, args, nargs, C_NULL)) + wrappedfn() = convert(T, __pycall!(ret, pyargsptr, o, C_NULL)) + return wrappedfn end """ ``` setargs!(pyargsptr::PyPtr, args...) ``` -Set the arguments to a python function wrapped in a PyFuncWrap, and convert them +Set the arguments to a python function, and convert them to `PyObject`s that can be passed directly to python when the function is -called. After the arguments have been set, the function can be efficiently -called with `pf()` +called. """ -function setargs!(pf::PyFuncWrap{P, N, RT}, args) where {P, N, RT} - for i = 1:N - setarg!(pf.pyargsptr, args[i], i) - end - nothing -end - function setargs!(pyargsptr::PyPtr, args::Tuple, N::Int) for i = 1:N setarg!(pyargsptr, args[i], i) @@ -163,10 +159,10 @@ end ``` setarg!(pyargsptr::PyPtr, arg, i::Integer=1) ``` -Set the `i`th argument to a python function wrapped in a PyFuncWrap, and convert +Set the `i`th argument to a python function, and convert it to a `PyObject` that can be passed directly to python when the function is -called. Useful if a function takes multiple arguments, but only one or two of -them change, when calling the function in a tight loop +called. Can be used if a function takes multiple arguments, but only one or two +of them change, when the function is called in a tight loop. """ function setarg!(pyargsptr::PyPtr, arg, i::Integer=1) pyarg = PyObject(arg) @@ -174,14 +170,3 @@ function setarg!(pyargsptr::PyPtr, arg, i::Integer=1) (PyPtr,Int,PyPtr), pyargsptr, i-1, pyarg) pyincref(pyarg) # PyTuple_SetItem steals the reference end - -function (pf::PyFuncWrap{P, N, RT})(args) where {P, N, RT} - setargs!(pf, args) - return pf() -end - -""" -Warning: if pf(args) or setargs(pf, ...) hasn't been called yet, this will likely segfault -""" -(pf::PyFuncWrap{P, N, RT})() where {P, N, RT} = - convert(RT, __pycall!(pf.ret, pf.pyargsptr, pf.o, C_NULL)) # C_NULL is keywords From cc3a3af9ab0148e999e114d0b6006f31801f77cc Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Fri, 13 Apr 2018 18:15:20 +1000 Subject: [PATCH 06/22] rename to pycalls.jl --- src/PyCall.jl | 2 +- src/{pyfuncwrap.jl => pycalls.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{pyfuncwrap.jl => pycalls.jl} (100%) diff --git a/src/PyCall.jl b/src/PyCall.jl index 990a3184..ea0d4b47 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -690,7 +690,7 @@ function pybuiltin(name) end ######################################################################### -include("pyfuncwrap.jl") +include("pycalls.jl") """ Low-level version of `pycall(o, ...)` that always returns `PyObject`. diff --git a/src/pyfuncwrap.jl b/src/pycalls.jl similarity index 100% rename from src/pyfuncwrap.jl rename to src/pycalls.jl From f297a61d6c32e0048fead9f25fad9ef74b29b528 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Sat, 14 Apr 2018 01:02:06 +1000 Subject: [PATCH 07/22] More tests, better docstrings --- benchmarks/callperf.jl | 4 +- src/PyCall.jl | 2 +- src/pycalls.jl | 127 +++++++++++++++++++++++++--------------- test/runtests.jl | 2 +- test/test_pycalls.jl | 61 +++++++++++++++++++ test/test_pyfuncwrap.jl | 29 --------- 6 files changed, 145 insertions(+), 80 deletions(-) create mode 100644 test/test_pycalls.jl delete mode 100644 test/test_pyfuncwrap.jl diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index 4cb3be49..ebb32471 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -46,8 +46,8 @@ end println("") println("Mean times") println("----------") -foreach((r)->println(rpad(r[1],33), ": ", mean(r[2])), results) +foreach((r)->println(rpad(r[1],33), "\t", mean(r[2])), results) println("") println("Median times") println("----------") -foreach((r)->println(rpad(r[1],33), ": ", median(r[2])), results) +foreach((r)->println(rpad(r[1],33), "\t", median(r[2])), results) diff --git a/src/PyCall.jl b/src/PyCall.jl index ea0d4b47..7ebcea5d 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -11,7 +11,7 @@ export pycall, pycall!, pyimport, pyimport_e, pybuiltin, PyObject, PyReverseDims 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, - pywrapfn, setarg!, setargs! + pywrapfn, pysetarg!, pysetargs! import Base: size, ndims, similar, copy, getindex, setindex!, stride, convert, pointer, summary, convert, show, haskey, keys, values, diff --git a/src/pycalls.jl b/src/pycalls.jl index 5318084f..2bf56b8a 100644 --- a/src/pycalls.jl +++ b/src/pycalls.jl @@ -2,10 +2,10 @@ # pyarg_tuples[i] is a pointer to a python tuple of length i-1 # const pyarg_tuples = Vector{PyPtr}(32) const pyarg_tuples = PyPtr[] -# const cur_args_size = Ref{Int}(0) + """ -You have the args, but kwargs need to be converted to a Ptr to a Python dict, or -C_NULL if empty +Low-level version of `pycall!(ret, o, ...; kwargs...)` +Sets `ret.o` to the result of the call, and returns `ret::PyObject` """ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, kwargs::Vector{Any}) if isempty(kwargs) @@ -17,8 +17,9 @@ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, kwargs::Vector{ end """ -You have the args, and kwargs is in python friendly format, but -you don't yet have the python tuple to hold the arguments. +Low-level version of `pycall!(ret, o, ...)` for when `kw` is already in python +friendly format but you don't have the python tuple to hold the arguments +(`pyargsptr`). Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, nargs::Int=length(args), kw::Union{Ptr{Void}, PyObject}=C_NULL) @@ -31,19 +32,49 @@ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, end """ -You have the args, and kwargs is in python friendly format, and -you have the python tuple to hold the arguments (pyargsptr), you just haven't -set the tuples values to the python version of your arguments yet +Low-level version of `pycall!(ret, o, ...)` for when `kw` is already in python +friendly format and you have the python tuple to hold the arguments (`pyargsptr`). +Sets the tuple's values to the python version of your arguments, and calls the +function. Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function _pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, args, nargs::Int=length(args), kw::Union{Ptr{Void}, PyObject}=C_NULL) - setargs!(pyargsptr, args, nargs) + pysetargs!(pyargsptr, args, nargs) return __pycall!(ret, pyargsptr, o, kw) #::PyObject end """ -You're all frocked up and ready for the dance. `pyargsptr` and `kw` have all -their args set to Python values, so we can call the function. +``` +pysetargs!(pyargsptr::PyPtr, args...) +``` +Convert `args` to `PyObject`s, and set them as the elements of the Python tuple +pointed to by `pyargsptr` +""" +function pysetargs!(pyargsptr::PyPtr, args, N::Int) + for i = 1:N + pysetarg!(pyargsptr, args[i], i) + end +end + +""" +``` +pysetarg!(pyargsptr::PyPtr, arg, i::Integer=1) +``` +Convert `arg` to a `PyObject`, and set it as the `i-1`th element of the Python +tuple pointed to by `pyargsptr` +""" +function pysetarg!(pyargsptr::PyPtr, arg, i::Integer=1) + pyarg = PyObject(arg) + pyincref(pyarg) # PyTuple_SetItem steals the reference + @show unsafe_load(pyargsptr).ob_refcnt + @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, + (PyPtr,Int,PyPtr), pyargsptr, i-1, pyarg) +end + +""" +Lowest level version of `pycall!(ret, o, ...)`, assumes `pyargsptr` and `kw` +have all their args set to Python values, so we can just call the function `o`. +Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function __pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, kw::Union{Ptr{Void}, PyObject}) @@ -60,21 +91,34 @@ function __pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, return ret #::PyObject end -pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) = - return convert(returntype, _pycall!(pyobj, o, args, kwargs)) +""" +``` +pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, returntype::Type, args...; kwargs...) +``` +Set `ret` to the result of `pycall(o, returntype, args...; kwargs)` and return +`convert(returntype, ret)`. +Avoids allocating an extra PyObject for `ret`. See `pycall` for other details. +""" +pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) = + return convert(returntype, _pycall!(ret, o, args, kwargs)) -function pycall!(pyobj::PyObject, o::T, returntype::Type{PyObject}, args...; kwargs...) where +function pycall!(ret::PyObject, o::T, returntype::Type{PyObject}, args...; kwargs...) where T<:Union{PyObject,PyPtr} - return _pycall!(pyobj, o, args, kwargs) + return _pycall!(ret, o, args, kwargs) end -pycall!(pyobj::PyObject, o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = - return convert(PyAny, _pycall!(pyobj, o, args, kwargs)) +pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = + return convert(PyAny, _pycall!(ret, o, args, kwargs)) """ - 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...) +``` +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!(PyNULL(), o, args, kwargs))::returntype @@ -82,7 +126,9 @@ pycall(o::Union{PyObject,PyPtr}, returntype::TypeTuple, args...; kwargs...) = pycall(o::Union{PyObject,PyPtr}, ::Type{PyAny}, args...; kwargs...) = return convert(PyAny, _pycall!(PyNULL(), o, args, kwargs)) -(o::PyObject)(args...; kws...) = pycall(o, PyAny, args...; kws...) +(o::PyObject)(args...; kwargs...) = + return convert(PyAny, _pycall!(PyNULL(), o, args, kwargs)) + PyAny(o::PyObject) = convert(PyAny, o) """ @@ -112,8 +158,9 @@ pywrapfn(o::PyObject, nargs::Int, returntype::Type{T}=PyObject) where T Wrap a callable PyObject/PyPtr to reduce the number of allocations made for passing its arguments, and its return value, sometimes providing a speedup. Mainly useful for functions called in a tight loop. After wrapping, arguments -should be passed in tuple, rather than directly, e.g. `wrappedfn((a,b))` rather -than `wrappedfn(a,b)` +should be passed in a tuple, rather than directly, e.g. `wrappedfn((a,b))` rather +than `wrappedfn(a,b)`. +Example ``` @pyimport numpy as np @@ -124,10 +171,10 @@ randmatfn = pywrapfn(np.random["rand"], 2, PyArray) a_random_matrix = np.random["rand"](7, 7) # but we call the wrapped version with a tuple instead, i.e. -# rand22fn((4, 4)) not -# rand22fn(4, 4) +# rand22fn((7, 7)) not +# rand22fn(7, 7) for i in 1:10^9 - arr = rand22fn((4, 4)) + arr = rand22fn((7,7)) ... end ``` @@ -137,36 +184,22 @@ function pywrapfn(o::PyObject, nargs::Int, returntype::Type{T}=PyObject) where T ret = PyNULL() wrappedfn(args) = convert(T, _pycall!(ret, pyargsptr, o, args, nargs, C_NULL)) wrappedfn() = convert(T, __pycall!(ret, pyargsptr, o, C_NULL)) + return wrappedfn end """ ``` -setargs!(pyargsptr::PyPtr, args...) +pysetargs!(w, args...) ``` -Set the arguments to a python function, and convert them -to `PyObject`s that can be passed directly to python when the function is -called. +Set the arguments to call a Python function wrapped with `w = pywrapfn(pyfun)` """ -function setargs!(pyargsptr::PyPtr, args::Tuple, N::Int) - for i = 1:N - setarg!(pyargsptr, args[i], i) - end - nothing -end +pysetargs!(w, args, N::Int) = pysetargs!(w.pyargsptr, args, N) """ ``` -setarg!(pyargsptr::PyPtr, arg, i::Integer=1) +pysetarg!(w::typeof{wrappedfn}, arg, i::Integer=1) ``` -Set the `i`th argument to a python function, and convert -it to a `PyObject` that can be passed directly to python when the function is -called. Can be used if a function takes multiple arguments, but only one or two -of them change, when the function is called in a tight loop. +Set argument the `i`th argument to be passed to the Python function wrapped by `w` """ -function setarg!(pyargsptr::PyPtr, arg, i::Integer=1) - pyarg = PyObject(arg) - @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, - (PyPtr,Int,PyPtr), pyargsptr, i-1, pyarg) - pyincref(pyarg) # PyTuple_SetItem steals the reference -end +pysetarg!(w, arg, i::Integer=1) = pysetarg!(w.pyargsptr, arg, i) diff --git a/test/runtests.jl b/test/runtests.jl index cf35fe60..39836ea3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -561,4 +561,4 @@ end @test (@pywith IgnoreError(true) error(); true) end -include("test_pyfuncwrap.jl") \ No newline at end of file +include("test_pyfuncwrap.jl") diff --git a/test/test_pycalls.jl b/test/test_pycalls.jl new file mode 100644 index 00000000..508e358d --- /dev/null +++ b/test/test_pycalls.jl @@ -0,0 +1,61 @@ +using Compat.Test, PyCall + +@testset "pywrapfn" begin + np = pyimport("numpy") + ops = pyimport("operator") + eq = ops["eq"] + npzeros = np["zeros"] + npzeros_pyo(sz, dtype="d", order="F") = pycall(npzeros, PyObject, sz, dtype, order) + npzeros_pyany(sz, dtype="d", order="F") = pycall(npzeros, PyAny, sz, dtype, order) + npzeros_pyarray(sz, dtype="d", order="F") = pycall(npzeros, PyArray, sz, dtype, order) + + npzeros2dwrap_pyo = pywrapfn(npzeros, 3) # PyObject is default returntype + npzeros2dwrap_pyany = pywrapfn(npzeros, 3, PyAny) + npzeros2dwrap_pyarray = pywrapfn(npzeros, 3, PyArray) + + arr_size = (2,2) + + # all args + @test np["array_equal"](npzeros2dwrap_pyo((arr_size, "d", "C")), npzeros_pyo(arr_size)) + # args already set + @test np["array_equal"](npzeros2dwrap_pyo(), npzeros_pyo(arr_size)) + + @test all(npzeros2dwrap_pyany((arr_size, "d", "C")) .== npzeros_pyany(arr_size)) + @test all(npzeros2dwrap_pyany() .== npzeros_pyany(arr_size)) + + @test all(npzeros2dwrap_pyarray((arr_size, "d", "C")) .== npzeros_pyarray(arr_size)) + @test all(npzeros2dwrap_pyarray() .== npzeros_pyarray(arr_size)) + + @testset "setarg(s)!" begin + arr_size = (3,3) + # set arg 1, then call without args, old args should be unchanged + setarg!(npzeros2dwrap_pyo, arr_size, 1) + int32mat_pyo = npzeros2dwrap_pyo() + @test np["array_equal"](int32mat_pyo, npzeros_pyo(arr_size)) + @test int32mat_pyo["dtype"] == np["dtype"]("d") + @test int32mat_pyo["flags"]["c_contiguous"] == true + @test int32mat_pyo["shape"] == arr_size + + # set arg 2 (data type), then call without args + setarg!(npzeros2dwrap_pyo, "i", 2) + int32mat_pyo1 = npzeros2dwrap_pyo() + @test int32mat_pyo1["dtype"] == np["dtype"]("i") + @test int32mat_pyo1["flags"]["c_contiguous"] == true + @test int32mat_pyo1["shape"] == arr_size + + # set arg 3 (order - C or Fortran), then call without args + setarg!(npzeros2dwrap_pyo, "F", 3) + int32mat_pyo2 = npzeros2dwrap_pyo() + @test int32mat_pyo2["flags"]["f_contiguous"] == true + @test int32mat_pyo2["dtype"] == np["dtype"]("i") + @test int32mat_pyo2["shape"] == arr_size + + # set all args then call without args + arr_size = (4,4) + setargs!(npzeros2dwrap_pyo, (arr_size, "c", "F"), 3) + cmplxmat_pyo = npzeros2dwrap_pyo() + @test cmplxmat_pyo["dtype"] == np["dtype"]("c") + @test cmplxmat_pyo["flags"]["f_contiguous"] == true + @test cmplxmat_pyo["shape"] == arr_size + end +end diff --git a/test/test_pyfuncwrap.jl b/test/test_pyfuncwrap.jl deleted file mode 100644 index ed213d54..00000000 --- a/test/test_pyfuncwrap.jl +++ /dev/null @@ -1,29 +0,0 @@ -using Compat.Test, PyCall - -@testset "PyFuncWrap" begin - np = pyimport("numpy") - ops = pyimport("operator") - eq = ops["eq"] - npzeros = np["zeros"] - npzeros_pyo(sz, dtype="d", order="F") = pycall(npzeros, PyObject, sz, dtype, order) - npzeros_pyany(sz, dtype="d", order="F") = pycall(npzeros, PyAny, sz, dtype, order) - npzeros_pyarray(sz, dtype="d", order="F") = pycall(npzeros, PyArray, sz, dtype, order) - - # PyObject is default returntype - npzeros2dwrap_pyo = PyFuncWrap(npzeros, ((Int, Int), String, String)) - npzeros2dwrap_pyany = PyFuncWrap(npzeros, ((Int, Int), String, String), PyAny) - npzeros2dwrap_pyarray = PyFuncWrap(npzeros, ((Int, Int), String, String), PyArray) - - arr_size = (2,2) - - # all args - @test np["array_equal"](npzeros2dwrap_pyo(arr_size, "d", "F"), npzeros_pyo(arr_size)) - # args already set - @test np["array_equal"](npzeros2dwrap_pyo(), npzeros_pyo(arr_size)) - - @test all(npzeros2dwrap_pyany(arr_size, "d", "F") .== npzeros_pyany(arr_size)) - @test all(npzeros2dwrap_pyany() .== npzeros_pyany(arr_size)) - - @test all(npzeros2dwrap_pyarray(arr_size, "d", "F") .== npzeros_pyarray(arr_size)) - @test all(npzeros2dwrap_pyarray() .== npzeros_pyarray(arr_size)) -end From be758c303e53a79770928a9b4f19c97aa42690b0 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Sat, 14 Apr 2018 01:52:20 +1000 Subject: [PATCH 08/22] Fix some tests and debugging stuff... --- src/pycalls.jl | 1 + test/runtests.jl | 2 +- test/test_pycalls.jl | 10 +++++----- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pycalls.jl b/src/pycalls.jl index 2bf56b8a..b463022c 100644 --- a/src/pycalls.jl +++ b/src/pycalls.jl @@ -51,6 +51,7 @@ Convert `args` to `PyObject`s, and set them as the elements of the Python tuple pointed to by `pyargsptr` """ function pysetargs!(pyargsptr::PyPtr, args, N::Int) + println("nargs is $N") for i = 1:N pysetarg!(pyargsptr, args[i], i) end diff --git a/test/runtests.jl b/test/runtests.jl index 39836ea3..990c6994 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -561,4 +561,4 @@ end @test (@pywith IgnoreError(true) error(); true) end -include("test_pyfuncwrap.jl") +include("test_pycalls.jl") diff --git a/test/test_pycalls.jl b/test/test_pycalls.jl index 508e358d..08246339 100644 --- a/test/test_pycalls.jl +++ b/test/test_pycalls.jl @@ -26,10 +26,10 @@ using Compat.Test, PyCall @test all(npzeros2dwrap_pyarray((arr_size, "d", "C")) .== npzeros_pyarray(arr_size)) @test all(npzeros2dwrap_pyarray() .== npzeros_pyarray(arr_size)) - @testset "setarg(s)!" begin + @testset "pysetarg(s)!" begin arr_size = (3,3) # set arg 1, then call without args, old args should be unchanged - setarg!(npzeros2dwrap_pyo, arr_size, 1) + pysetarg!(npzeros2dwrap_pyo, arr_size, 1) int32mat_pyo = npzeros2dwrap_pyo() @test np["array_equal"](int32mat_pyo, npzeros_pyo(arr_size)) @test int32mat_pyo["dtype"] == np["dtype"]("d") @@ -37,14 +37,14 @@ using Compat.Test, PyCall @test int32mat_pyo["shape"] == arr_size # set arg 2 (data type), then call without args - setarg!(npzeros2dwrap_pyo, "i", 2) + pysetarg!(npzeros2dwrap_pyo, "i", 2) int32mat_pyo1 = npzeros2dwrap_pyo() @test int32mat_pyo1["dtype"] == np["dtype"]("i") @test int32mat_pyo1["flags"]["c_contiguous"] == true @test int32mat_pyo1["shape"] == arr_size # set arg 3 (order - C or Fortran), then call without args - setarg!(npzeros2dwrap_pyo, "F", 3) + pysetarg!(npzeros2dwrap_pyo, "F", 3) int32mat_pyo2 = npzeros2dwrap_pyo() @test int32mat_pyo2["flags"]["f_contiguous"] == true @test int32mat_pyo2["dtype"] == np["dtype"]("i") @@ -52,7 +52,7 @@ using Compat.Test, PyCall # set all args then call without args arr_size = (4,4) - setargs!(npzeros2dwrap_pyo, (arr_size, "c", "F"), 3) + pysetargs!(npzeros2dwrap_pyo, (arr_size, "c", "F"), 3) cmplxmat_pyo = npzeros2dwrap_pyo() @test cmplxmat_pyo["dtype"] == np["dtype"]("c") @test cmplxmat_pyo["flags"]["f_contiguous"] == true From 23267f568d66ebccffd4123be14154e4569953be Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Sat, 14 Apr 2018 02:06:44 +1000 Subject: [PATCH 09/22] no longer needed tuplen --- src/conversions.jl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/conversions.jl b/src/conversions.jl index 6e6aad14..5f4530e2 100644 --- a/src/conversions.jl +++ b/src/conversions.jl @@ -214,11 +214,6 @@ end # somewhat annoying to get the length and types in a tuple type # ... would be better not to have to use undocumented internals! -function tuplen(T::DataType) - isvatuple(T) && ArgumentError("can't determine length of vararg tuple: $T") - return length(T.parameters) -end -tuplen(T::UnionAll) = tuplen(T.body) istuplen(T,isva,n) = isva ? n ≥ length(T.parameters)-1 : n == length(T.parameters) function tuptype(T::DataType,isva,i) if isva && i ≥ length(T.parameters) From 3edb8f0506ff1172ce285c872d118cf372a222fc Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Sun, 15 Apr 2018 00:26:36 +1000 Subject: [PATCH 10/22] 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. --- benchmarks/callperf.jl | 14 ++++++----- src/pycalls.jl | 55 ++++++++++++++++++++++++++++-------------- test/test_pycalls.jl | 2 +- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index ebb32471..732b9b7a 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -9,10 +9,12 @@ let ret = PyNULL() args_lens = (0,1,2,3,7,12,17) # args_lens = (1,3,7) + # args_lens = (3,) arr_sizes = (ntuple(i->1, len) for len in args_lens) for (i, arr_size) in enumerate(arr_sizes) - nprand_wrap = pywrapfn(nprand, length(arr_size)) + 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,...)" @@ -32,13 +34,13 @@ let println("_pycall! $arr_size_str:\n"); display(results["_pycall! $arr_size_str"]) println("--------------------------------------------------") - results["nprand_wrap $arr_size_str"] = @benchmark $nprand_wrap($arr_size) - println("nprand_wrap $arr_size_str:\n"); display(results["nprand_wrap $arr_size_str"]) + 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_wrap calls above - results["nprand_wrap_noargs $arr_size_str"] = @benchmark $nprand_wrap() - println("nprand_wrap_noargs $arr_size_str:\n"); display(results["nprand_wrap_noargs $arr_size_str"]) + # 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 diff --git a/src/pycalls.jl b/src/pycalls.jl index b463022c..08a2bb44 100644 --- a/src/pycalls.jl +++ b/src/pycalls.jl @@ -51,7 +51,7 @@ Convert `args` to `PyObject`s, and set them as the elements of the Python tuple pointed to by `pyargsptr` """ function pysetargs!(pyargsptr::PyPtr, args, N::Int) - println("nargs is $N") + # println("nargs is $N") for i = 1:N pysetarg!(pyargsptr, args[i], i) end @@ -67,7 +67,7 @@ tuple pointed to by `pyargsptr` function pysetarg!(pyargsptr::PyPtr, arg, i::Integer=1) pyarg = PyObject(arg) pyincref(pyarg) # PyTuple_SetItem steals the reference - @show unsafe_load(pyargsptr).ob_refcnt + # @show unsafe_load(pyargsptr).ob_refcnt @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, (PyPtr,Int,PyPtr), pyargsptr, i-1, pyarg) end @@ -151,13 +151,34 @@ macro pycall(ex) end ######################################################################### +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 + 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. -Wrap a callable PyObject/PyPtr to reduce the number of allocations made for -passing its arguments, and its return value, sometimes providing a speedup. 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)`. @@ -180,27 +201,25 @@ for i in 1:10^9 end ``` """ -function pywrapfn(o::PyObject, nargs::Int, returntype::Type{T}=PyObject) where T - pyargsptr = ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) - ret = PyNULL() - wrappedfn(args) = convert(T, _pycall!(ret, pyargsptr, o, args, nargs, C_NULL)) - wrappedfn() = convert(T, __pycall!(ret, pyargsptr, o, C_NULL)) - - return wrappedfn -end +pywrapfn(o::PyObject, nargs::Int, returntype::Type=PyObject) = + PyWrapFn(o, nargs, returntype) """ ``` -pysetargs!(w, args...) +pysetargs!(w::PyWrapFn{N, RT}, args) ``` -Set the arguments to call a Python function wrapped with `w = pywrapfn(pyfun)` +Set the arguments with which to call a Python function wrapped using +`w = pywrapfn(pyfun, ...)` """ -pysetargs!(w, args, N::Int) = pysetargs!(w.pyargsptr, args, N) +pysetargs!(pf::PyWrapFn{N, RT}, args) where {N, RT} = + pysetargs!(pf.pyargsptr, args, N) """ ``` -pysetarg!(w::typeof{wrappedfn}, arg, i::Integer=1) +pysetarg!(w::PyWrapFn{N, RT}, arg, i::Integer=1) ``` -Set argument the `i`th argument to be passed to the Python function wrapped by `w` +Set the `i`th argument to be passed to a Python function previously +wrapped with a call to `w = pywrapfn(pyfun, ...)` """ -pysetarg!(w, arg, i::Integer=1) = pysetarg!(w.pyargsptr, arg, i) +pysetarg!(pf::PyWrapFn{N, RT}, arg, i::Integer=1) where {N, RT} = + pysetarg!(pf.pyargsptr, arg, i) diff --git a/test/test_pycalls.jl b/test/test_pycalls.jl index 08246339..a816886f 100644 --- a/test/test_pycalls.jl +++ b/test/test_pycalls.jl @@ -52,7 +52,7 @@ using Compat.Test, PyCall # set all args then call without args arr_size = (4,4) - pysetargs!(npzeros2dwrap_pyo, (arr_size, "c", "F"), 3) + pysetargs!(npzeros2dwrap_pyo, (arr_size, "c", "F")) cmplxmat_pyo = npzeros2dwrap_pyo() @test cmplxmat_pyo["dtype"] == np["dtype"]("c") @test cmplxmat_pyo["flags"]["f_contiguous"] == true From 3fc1093a7876e2aec0c731bb79cc422e0fdc126e Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Sun, 15 Apr 2018 00:30:57 +1000 Subject: [PATCH 11/22] rename pycalls.jl to pyfncall.jl --- src/PyCall.jl | 2 +- src/{pycalls.jl => pyfncall.jl} | 0 test/runtests.jl | 2 +- test/{test_pycalls.jl => test_pyfncall.jl} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename src/{pycalls.jl => pyfncall.jl} (100%) rename test/{test_pycalls.jl => test_pyfncall.jl} (100%) diff --git a/src/PyCall.jl b/src/PyCall.jl index 7ebcea5d..ee02c9f5 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -690,7 +690,7 @@ function pybuiltin(name) end ######################################################################### -include("pycalls.jl") +include("pyfncall.jl") """ Low-level version of `pycall(o, ...)` that always returns `PyObject`. diff --git a/src/pycalls.jl b/src/pyfncall.jl similarity index 100% rename from src/pycalls.jl rename to src/pyfncall.jl diff --git a/test/runtests.jl b/test/runtests.jl index 990c6994..552356c4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -561,4 +561,4 @@ end @test (@pywith IgnoreError(true) error(); true) end -include("test_pycalls.jl") +include("test_pyfncall.jl") diff --git a/test/test_pycalls.jl b/test/test_pyfncall.jl similarity index 100% rename from test/test_pycalls.jl rename to test/test_pyfncall.jl From b1a989963e1b43364af639e1239e4b829a258142 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Sun, 15 Apr 2018 15:34:50 +1000 Subject: [PATCH 12/22] Ensure only single ref to pyargsptr tuple --- src/PyCall.jl | 7 ++++++- src/pyfncall.jl | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/PyCall.jl b/src/PyCall.jl index ee02c9f5..7a7ad4fa 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -98,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 diff --git a/src/pyfncall.jl b/src/pyfncall.jl index 08a2bb44..1a06393a 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -27,10 +27,29 @@ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, for n in length(pyarg_tuples):nargs push!(pyarg_tuples, @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), n)) end + check_pyargsptr(nargs) pyargsptr = pyarg_tuples[nargs+1] return _pycall!(ret, pyargsptr, o, args, nargs, kw) #::PyObject end +""" +Handle the situation where a callee had previously called incref on the +arguments tuple that was passed to it. We need to hold the only reference to the +arguments tuple, since setting a tuple item is only allowed when there is just +one reference to the tuple (tuples are supposed to be immutable in Python in all +other cases). Note that this actually happens when creating new builtin +exceptions, ref: https://github.com/python/cpython/blob/480ab05d5fee2b8fa161f799af33086a4e68c7dd/Objects/exceptions.c#L48 +OTOH this py"def foo(*args): global z; z=args" doesn't trigger this. +Fortunately, this check for ob_refcnt is fast - only a few cpu clock cycles. +""" +function check_pyargsptr(nargs::Int) + if unsafe_load(pyarg_tuples[nargs+1]).ob_refcnt > 1 + pydecref_(pyarg_tuples[nargs+1]) + pyarg_tuples[nargs+1] = + @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) + end +end + """ Low-level version of `pycall!(ret, o, ...)` for when `kw` is already in python friendly format and you have the python tuple to hold the arguments (`pyargsptr`). @@ -51,7 +70,6 @@ Convert `args` to `PyObject`s, and set them as the elements of the Python tuple pointed to by `pyargsptr` """ function pysetargs!(pyargsptr::PyPtr, args, N::Int) - # println("nargs is $N") for i = 1:N pysetarg!(pyargsptr, args[i], i) end @@ -67,7 +85,6 @@ tuple pointed to by `pyargsptr` function pysetarg!(pyargsptr::PyPtr, arg, i::Integer=1) pyarg = PyObject(arg) pyincref(pyarg) # PyTuple_SetItem steals the reference - # @show unsafe_load(pyargsptr).ob_refcnt @pycheckz ccall((@pysym :PyTuple_SetItem), Cint, (PyPtr,Int,PyPtr), pyargsptr, i-1, pyarg) end @@ -211,8 +228,10 @@ pysetargs!(w::PyWrapFn{N, RT}, args) Set the arguments with which to call a Python function wrapped using `w = pywrapfn(pyfun, ...)` """ -pysetargs!(pf::PyWrapFn{N, RT}, args) where {N, RT} = +function pysetargs!(pf::PyWrapFn{N, RT}, args) where {N, RT} + check_pyargsptr(pf) pysetargs!(pf.pyargsptr, args, N) +end """ ``` @@ -221,5 +240,18 @@ 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, ...)` """ -pysetarg!(pf::PyWrapFn{N, RT}, arg, i::Integer=1) where {N, RT} = +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 From db6d00038d7ebdb77fdbe9ae7ec9251f93e3d6a4 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 18 Apr 2018 20:51:42 +1000 Subject: [PATCH 13/22] Fix bug in PyWrapFn and change to vararg --- benchmarks/callperf.jl | 4 ++-- src/pyfncall.jl | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index 732b9b7a..16e0144c 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -8,7 +8,7 @@ let nprand = np["random"]["rand"] ret = PyNULL() args_lens = (0,1,2,3,7,12,17) - # args_lens = (1,3,7) + # args_lens = (7,3,1) # args_lens = (3,) arr_sizes = (ntuple(i->1, len) for len in args_lens) @@ -34,7 +34,7 @@ let println("_pycall! $arr_size_str:\n"); display(results["_pycall! $arr_size_str"]) println("--------------------------------------------------") - results["nprand_pywrapfn $arr_size_str"] = @benchmark $nprand_pywrapfn($arr_size) + 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("--------------------------------------------------") diff --git a/src/pyfncall.jl b/src/pyfncall.jl index 1a06393a..cd4cc6d7 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -178,10 +178,11 @@ function PyWrapFn(o::Union{PyObject, PyPtr}, nargs::Int, returntype::Type=PyObje 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} = +(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} = From 88baa71149be3d47a1ccb2d817cc694ef2524207 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 18 Apr 2018 20:52:55 +1000 Subject: [PATCH 14/22] Fix pyargsptr check for empty tuple --- src/pyfncall.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyfncall.jl b/src/pyfncall.jl index cd4cc6d7..580c8f62 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -43,7 +43,7 @@ OTOH this py"def foo(*args): global z; z=args" doesn't trigger this. Fortunately, this check for ob_refcnt is fast - only a few cpu clock cycles. """ function check_pyargsptr(nargs::Int) - if unsafe_load(pyarg_tuples[nargs+1]).ob_refcnt > 1 + if nargs > 0 && unsafe_load(pyarg_tuples[nargs+1]).ob_refcnt > 1 pydecref_(pyarg_tuples[nargs+1]) pyarg_tuples[nargs+1] = @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), nargs) From 82335aa075cba5786e4c717a00e816cdeee6b11b Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 18 Apr 2018 20:53:07 +1000 Subject: [PATCH 15/22] Remove PyWrapFn --- src/pyfncall.jl | 90 ------------------------------------------------- 1 file changed, 90 deletions(-) diff --git a/src/pyfncall.jl b/src/pyfncall.jl index 580c8f62..b76d4e81 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -166,93 +166,3 @@ macro pycall(ex) T = ex.args[2] :(pycall($(map(esc,[kwargs; func; T; args])...))) end - -######################################################################### -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 From 8a52d6f2e9c87cf1c9566bb4eb2dc92a3a83493c Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 18 Apr 2018 21:56:49 +1000 Subject: [PATCH 16/22] Add `pycall!` tests. Remove pywrapfn tests --- test/test_pyfncall.jl | 59 +++++++++---------------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/test/test_pyfncall.jl b/test/test_pyfncall.jl index a816886f..84d1f729 100644 --- a/test/test_pyfncall.jl +++ b/test/test_pyfncall.jl @@ -1,61 +1,26 @@ using Compat.Test, PyCall -@testset "pywrapfn" begin +@testset "pycall!" begin np = pyimport("numpy") ops = pyimport("operator") eq = ops["eq"] npzeros = np["zeros"] + res = PyNULL() npzeros_pyo(sz, dtype="d", order="F") = pycall(npzeros, PyObject, sz, dtype, order) npzeros_pyany(sz, dtype="d", order="F") = pycall(npzeros, PyAny, sz, dtype, order) npzeros_pyarray(sz, dtype="d", order="F") = pycall(npzeros, PyArray, sz, dtype, order) - npzeros2dwrap_pyo = pywrapfn(npzeros, 3) # PyObject is default returntype - npzeros2dwrap_pyany = pywrapfn(npzeros, 3, PyAny) - npzeros2dwrap_pyarray = pywrapfn(npzeros, 3, PyArray) + npzeros_pyo!(ret, sz, dtype="d", order="F") = pycall!(ret, npzeros, PyObject, sz, dtype, order) + npzeros_pyany!(ret, sz, dtype="d", order="F") = pycall!(ret, npzeros, PyAny, sz, dtype, order) + npzeros_pyarray!(ret, sz, dtype="d", order="F") = pycall!(ret, npzeros, PyArray, sz, dtype, order) - arr_size = (2,2) + arr_size = (3, 4) - # all args - @test np["array_equal"](npzeros2dwrap_pyo((arr_size, "d", "C")), npzeros_pyo(arr_size)) - # args already set - @test np["array_equal"](npzeros2dwrap_pyo(), npzeros_pyo(arr_size)) - - @test all(npzeros2dwrap_pyany((arr_size, "d", "C")) .== npzeros_pyany(arr_size)) - @test all(npzeros2dwrap_pyany() .== npzeros_pyany(arr_size)) - - @test all(npzeros2dwrap_pyarray((arr_size, "d", "C")) .== npzeros_pyarray(arr_size)) - @test all(npzeros2dwrap_pyarray() .== npzeros_pyarray(arr_size)) - - @testset "pysetarg(s)!" begin - arr_size = (3,3) - # set arg 1, then call without args, old args should be unchanged - pysetarg!(npzeros2dwrap_pyo, arr_size, 1) - int32mat_pyo = npzeros2dwrap_pyo() - @test np["array_equal"](int32mat_pyo, npzeros_pyo(arr_size)) - @test int32mat_pyo["dtype"] == np["dtype"]("d") - @test int32mat_pyo["flags"]["c_contiguous"] == true - @test int32mat_pyo["shape"] == arr_size - - # set arg 2 (data type), then call without args - pysetarg!(npzeros2dwrap_pyo, "i", 2) - int32mat_pyo1 = npzeros2dwrap_pyo() - @test int32mat_pyo1["dtype"] == np["dtype"]("i") - @test int32mat_pyo1["flags"]["c_contiguous"] == true - @test int32mat_pyo1["shape"] == arr_size - - # set arg 3 (order - C or Fortran), then call without args - pysetarg!(npzeros2dwrap_pyo, "F", 3) - int32mat_pyo2 = npzeros2dwrap_pyo() - @test int32mat_pyo2["flags"]["f_contiguous"] == true - @test int32mat_pyo2["dtype"] == np["dtype"]("i") - @test int32mat_pyo2["shape"] == arr_size - - # set all args then call without args - arr_size = (4,4) - pysetargs!(npzeros2dwrap_pyo, (arr_size, "c", "F")) - cmplxmat_pyo = npzeros2dwrap_pyo() - @test cmplxmat_pyo["dtype"] == np["dtype"]("c") - @test cmplxmat_pyo["flags"]["f_contiguous"] == true - @test cmplxmat_pyo["shape"] == arr_size + @testset "basics" begin + @test np["array_equal"](npzeros_pyo!(res, arr_size), npzeros_pyo(arr_size)) + @test all(npzeros_pyany!(res, arr_size) .== npzeros_pyany(arr_size)) + @test all(npzeros_pyarray!(res, arr_size) .== npzeros_pyarray(arr_size)) + @test npzeros_pyany!(res, arr_size) isa Array + @test npzeros_pyarray!(res, arr_size) isa PyArray end end From b8a087f3b9f67c29bd816aafc3cc5e54c83dd0de Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Mon, 2 Jul 2018 15:19:44 +0300 Subject: [PATCH 17/22] Remove kwargs type in _pycall! Attempt to fix 0.7 issue --- src/pyfncall.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyfncall.jl b/src/pyfncall.jl index b76d4e81..8d32d862 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -7,7 +7,7 @@ const pyarg_tuples = PyPtr[] Low-level version of `pycall!(ret, o, ...; kwargs...)` Sets `ret.o` to the result of the call, and returns `ret::PyObject` """ -function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, kwargs::Vector{Any}) +function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, kwargs) if isempty(kwargs) kw = C_NULL else From c2c7c5aa73212cbcd7addd0c548a973985a5e042 Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Tue, 3 Jul 2018 15:02:22 +0300 Subject: [PATCH 18/22] Stop using NumPy in tests --- test/test_pyfncall.jl | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/test/test_pyfncall.jl b/test/test_pyfncall.jl index 84d1f729..be437dc9 100644 --- a/test/test_pyfncall.jl +++ b/test/test_pyfncall.jl @@ -1,26 +1,26 @@ using Compat.Test, PyCall +py""" +def mklist(*args): + return list(args) +""" @testset "pycall!" begin - np = pyimport("numpy") - ops = pyimport("operator") - eq = ops["eq"] - npzeros = np["zeros"] + pymklist = py"mklist" res = PyNULL() - npzeros_pyo(sz, dtype="d", order="F") = pycall(npzeros, PyObject, sz, dtype, order) - npzeros_pyany(sz, dtype="d", order="F") = pycall(npzeros, PyAny, sz, dtype, order) - npzeros_pyarray(sz, dtype="d", order="F") = pycall(npzeros, PyArray, sz, dtype, order) - npzeros_pyo!(ret, sz, dtype="d", order="F") = pycall!(ret, npzeros, PyObject, sz, dtype, order) - npzeros_pyany!(ret, sz, dtype="d", order="F") = pycall!(ret, npzeros, PyAny, sz, dtype, order) - npzeros_pyarray!(ret, sz, dtype="d", order="F") = pycall!(ret, npzeros, PyArray, sz, dtype, order) - - arr_size = (3, 4) + function pycall_checks(res, pyf, RetType, args...) + pycall_res = pycall(pyf, RetType, args...) + res_converted = pycall!(res, pyf, RetType, args...) + @test convert(RetType, res) == res_converted + @test res_converted == pycall_res + RetType != PyAny && @test res_converted isa RetType + end @testset "basics" begin - @test np["array_equal"](npzeros_pyo!(res, arr_size), npzeros_pyo(arr_size)) - @test all(npzeros_pyany!(res, arr_size) .== npzeros_pyany(arr_size)) - @test all(npzeros_pyarray!(res, arr_size) .== npzeros_pyarray(arr_size)) - @test npzeros_pyany!(res, arr_size) isa Array - @test npzeros_pyarray!(res, arr_size) isa PyArray + args = ("a", 2, 4.5) + for RetType in (PyObject, PyAny, Tuple) + pycall_checks(res, pymklist, RetType, args...) + gc() + end end end From c67166cfaf2bd93e9966caeb8336f1d4e96e60eb Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Tue, 3 Jul 2018 20:56:37 +0300 Subject: [PATCH 19/22] 0.7 Deprecations --- src/pyfncall.jl | 6 +++--- test/test_pyfncall.jl | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pyfncall.jl b/src/pyfncall.jl index 8d32d862..5cb463e3 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -22,7 +22,7 @@ friendly format but you don't have the python tuple to hold the arguments (`pyargsptr`). Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, - nargs::Int=length(args), kw::Union{Ptr{Void}, PyObject}=C_NULL) + nargs::Int=length(args), kw::Union{Ptr{Nothing}, PyObject}=C_NULL) # pyarg_tuples[i] is a pointer to a python tuple of length i-1 for n in length(pyarg_tuples):nargs push!(pyarg_tuples, @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), n)) @@ -57,7 +57,7 @@ Sets the tuple's values to the python version of your arguments, and calls the function. Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function _pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, - args, nargs::Int=length(args), kw::Union{Ptr{Void}, PyObject}=C_NULL) + args, nargs::Int=length(args), kw::Union{Ptr{Nothing}, PyObject}=C_NULL) pysetargs!(pyargsptr, args, nargs) return __pycall!(ret, pyargsptr, o, kw) #::PyObject end @@ -95,7 +95,7 @@ have all their args set to Python values, so we can just call the function `o`. Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function __pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, - kw::Union{Ptr{Void}, PyObject}) + kw::Union{Ptr{Nothing}, PyObject}) sigatomic_begin() try retptr = @pycheckn ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), o, diff --git a/test/test_pyfncall.jl b/test/test_pyfncall.jl index be437dc9..98885098 100644 --- a/test/test_pyfncall.jl +++ b/test/test_pyfncall.jl @@ -20,7 +20,7 @@ def mklist(*args): args = ("a", 2, 4.5) for RetType in (PyObject, PyAny, Tuple) pycall_checks(res, pymklist, RetType, args...) - gc() + GC.gc() end end end From ee4d6f78f3e545768939744c7cc09657a5e19e2e Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 4 Jul 2018 00:50:07 +0300 Subject: [PATCH 20/22] Add tests for kwargs --- test/test_pyfncall.jl | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/test/test_pyfncall.jl b/test/test_pyfncall.jl index 98885098..c5de7a3e 100644 --- a/test/test_pyfncall.jl +++ b/test/test_pyfncall.jl @@ -1,26 +1,37 @@ -using Compat.Test, PyCall +using Compat, Compat.Test, PyCall py""" -def mklist(*args): - return list(args) +def mklist(*args, **kwargs): + l = list(args) + l.extend(kwargs.items()) + return l """ @testset "pycall!" begin pymklist = py"mklist" - res = PyNULL() + ret = PyNULL() - function pycall_checks(res, pyf, RetType, args...) - pycall_res = pycall(pyf, RetType, args...) - res_converted = pycall!(res, pyf, RetType, args...) - @test convert(RetType, res) == res_converted + function pycall_checks(res, pyf, RetType, args...; kwargs...) + pycall_res = pycall(pyf, RetType, args...; kwargs...) + res_converted = pycall!(res, pyf, RetType, args...; kwargs...) @test res_converted == pycall_res + @test convert(RetType, res) == res_converted RetType != PyAny && @test res_converted isa RetType end @testset "basics" begin args = ("a", 2, 4.5) for RetType in (PyObject, PyAny, Tuple) - pycall_checks(res, pymklist, RetType, args...) + pycall_checks(ret, pymklist, RetType, args...) GC.gc() end end + + @testset "kwargs" begin + args = ("a", 2, 4.5) + for RetType in (PyObject, PyAny, Tuple) + pycall_checks(ret, pymklist, RetType, args...; b=19610, c="5") + GC.gc() + end + end + end From 8ed2888eb25b5a56a2285247b4f78283f398b76d Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 4 Jul 2018 18:34:29 +0300 Subject: [PATCH 21/22] Ptr{Nothing} to Ptr{Cvoid} --- src/pyfncall.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pyfncall.jl b/src/pyfncall.jl index 5cb463e3..e1fa265d 100644 --- a/src/pyfncall.jl +++ b/src/pyfncall.jl @@ -22,7 +22,7 @@ friendly format but you don't have the python tuple to hold the arguments (`pyargsptr`). Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function _pycall!(ret::PyObject, o::Union{PyObject,PyPtr}, args, - nargs::Int=length(args), kw::Union{Ptr{Nothing}, PyObject}=C_NULL) + nargs::Int=length(args), kw::Union{Ptr{Cvoid}, PyObject}=C_NULL) # pyarg_tuples[i] is a pointer to a python tuple of length i-1 for n in length(pyarg_tuples):nargs push!(pyarg_tuples, @pycheckn ccall((@pysym :PyTuple_New), PyPtr, (Int,), n)) @@ -57,7 +57,7 @@ Sets the tuple's values to the python version of your arguments, and calls the function. Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function _pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, - args, nargs::Int=length(args), kw::Union{Ptr{Nothing}, PyObject}=C_NULL) + args, nargs::Int=length(args), kw::Union{Ptr{Cvoid}, PyObject}=C_NULL) pysetargs!(pyargsptr, args, nargs) return __pycall!(ret, pyargsptr, o, kw) #::PyObject end @@ -95,7 +95,7 @@ have all their args set to Python values, so we can just call the function `o`. Sets `ret.o` to the result of the call, and returns `ret::PyObject`. """ function __pycall!(ret::PyObject, pyargsptr::PyPtr, o::Union{PyObject,PyPtr}, - kw::Union{Ptr{Nothing}, PyObject}) + kw::Union{Ptr{Cvoid}, PyObject}) sigatomic_begin() try retptr = @pycheckn ccall((@pysym :PyObject_Call), PyPtr, (PyPtr,PyPtr,PyPtr), o, From b7c07e199b79e9e8ebf50390733b58460521b71d Mon Sep 17 00:00:00 2001 From: Joel Mason Date: Wed, 11 Jul 2018 19:15:48 +0300 Subject: [PATCH 22/22] Move pycall_legacy and pywrapfn to benchmarks --- benchmarks/callperf.jl | 5 +- benchmarks/pycall_legacy.jl | 43 ++++++++++++++++++ benchmarks/pywrapfn.jl | 91 +++++++++++++++++++++++++++++++++++++ src/PyCall.jl | 41 ----------------- 4 files changed, 138 insertions(+), 42 deletions(-) create mode 100644 benchmarks/pycall_legacy.jl create mode 100644 benchmarks/pywrapfn.jl diff --git a/benchmarks/callperf.jl b/benchmarks/callperf.jl index 16e0144c..3e716b06 100644 --- a/benchmarks/callperf.jl +++ b/benchmarks/callperf.jl @@ -1,5 +1,8 @@ using PyCall, BenchmarkTools, DataStructures -using PyCall: _pycall!, pycall_legacy +using PyCall: _pycall! + +include("pywrapfn.jl") +include("pycall_legacy.jl") results = OrderedDict{String,Any}() diff --git a/benchmarks/pycall_legacy.jl b/benchmarks/pycall_legacy.jl new file mode 100644 index 00000000..652054cf --- /dev/null +++ b/benchmarks/pycall_legacy.jl @@ -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...)) diff --git a/benchmarks/pywrapfn.jl b/benchmarks/pywrapfn.jl new file mode 100644 index 00000000..58f94d68 --- /dev/null +++ b/benchmarks/pywrapfn.jl @@ -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 diff --git a/src/PyCall.jl b/src/PyCall.jl index 7a7ad4fa..829181d7 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -697,47 +697,6 @@ end ######################################################################### include("pyfncall.jl") -""" -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...)) - ######################################################################### # Once Julia lets us overload ".", we will use [] to access items, but # for now we can define "get".