Author: Armin Rigo <[email protected]>
Branch:
Changeset: r84085:653a1c24d024
Date: 2016-05-01 08:36 +0200
http://bitbucket.org/pypy/pypy/changeset/653a1c24d024/
Log: hg merge share-cpyext-cpython-api
Share the ~one thousand @cpython_api function wrappers, according to
the signature. This reduces the number to ~200 or 250, and this
alone seems to give a more than 10% size win on the final pypy-c
(measured without the JIT). This should cancel the effect of the
size boost from 'cpyext-for-merge'.
diff --git a/pypy/module/cpyext/api.py b/pypy/module/cpyext/api.py
--- a/pypy/module/cpyext/api.py
+++ b/pypy/module/cpyext/api.py
@@ -10,6 +10,7 @@
from rpython.rtyper.lltypesystem import ll2ctypes
from rpython.rtyper.annlowlevel import llhelper
from rpython.rlib.objectmodel import we_are_translated, keepalive_until_here
+from rpython.rlib.objectmodel import dont_inline
from rpython.translator import cdir
from rpython.translator.tool.cbuild import ExternalCompilationInfo
from rpython.translator.gensupp import NameManager
@@ -255,7 +256,7 @@
class ApiFunction:
def __init__(self, argtypes, restype, callable, error=_NOT_SPECIFIED,
- c_name=None, gil=None, result_borrowed=False):
+ c_name=None, gil=None, result_borrowed=False,
result_is_ll=False):
self.argtypes = argtypes
self.restype = restype
self.functype = lltype.Ptr(lltype.FuncType(argtypes, restype))
@@ -276,6 +277,9 @@
assert len(self.argnames) == len(self.argtypes)
self.gil = gil
self.result_borrowed = result_borrowed
+ self.result_is_ll = result_is_ll
+ if result_is_ll: # means 'returns a low-level PyObject pointer'
+ assert is_PyObject(restype)
#
def get_llhelper(space):
return llhelper(self.functype, self.get_wrapper(space))
@@ -297,7 +301,7 @@
DEFAULT_HEADER = 'pypy_decl.h'
def cpython_api(argtypes, restype, error=_NOT_SPECIFIED, header=DEFAULT_HEADER,
- gil=None, result_borrowed=False):
+ gil=None, result_borrowed=False, result_is_ll=False):
"""
Declares a function to be exported.
- `argtypes`, `restype` are lltypes and describe the function signature.
@@ -336,7 +340,8 @@
c_name = func_name
api_function = ApiFunction(argtypes, restype, func, error,
c_name=c_name, gil=gil,
- result_borrowed=result_borrowed)
+ result_borrowed=result_borrowed,
+ result_is_ll=result_is_ll)
func.api_func = api_function
if error is _NOT_SPECIFIED:
@@ -612,6 +617,9 @@
def is_PyObject(TYPE):
if not isinstance(TYPE, lltype.Ptr):
return False
+ if TYPE == PyObject:
+ return True
+ assert not isinstance(TYPE.TO, lltype.ForwardReference)
return hasattr(TYPE.TO, 'c_ob_refcnt') and hasattr(TYPE.TO, 'c_ob_type')
# a pointer to PyObject
@@ -668,37 +676,158 @@
pypy_debug_catch_fatal_exception =
rffi.llexternal('pypy_debug_catch_fatal_exception', [], lltype.Void)
+
+# ____________________________________________________________
+
+
+class WrapperCache(object):
+ def __init__(self, space):
+ self.space = space
+ self.wrapper_gens = {} # {signature: WrapperGen()}
+ self.stats = [0, 0]
+
+class WrapperGen(object):
+ wrapper_second_level = None
+
+ def __init__(self, space, signature):
+ self.space = space
+ self.signature = signature
+ self.callable2name = []
+
+ def make_wrapper(self, callable):
+ self.callable2name.append((callable, callable.__name__))
+ if self.wrapper_second_level is None:
+ self.wrapper_second_level = make_wrapper_second_level(
+ self.space, self.callable2name, *self.signature)
+ wrapper_second_level = self.wrapper_second_level
+
+ def wrapper(*args):
+ # no GC here, not even any GC object
+ args += (callable,)
+ return wrapper_second_level(*args)
+
+ wrapper.__name__ = "wrapper for %r" % (callable, )
+ return wrapper
+
+
# Make the wrapper for the cases (1) and (2)
def make_wrapper(space, callable, gil=None):
"NOT_RPYTHON"
+ # This logic is obscure, because we try to avoid creating one
+ # big wrapper() function for every callable. Instead we create
+ # only one per "signature".
+
+ argnames = callable.api_func.argnames
+ argtypesw = zip(callable.api_func.argtypes,
+ [_name.startswith("w_") for _name in argnames])
+ error_value = getattr(callable.api_func, "error_value", CANNOT_FAIL)
+ if (isinstance(callable.api_func.restype, lltype.Ptr)
+ and error_value is not CANNOT_FAIL):
+ assert lltype.typeOf(error_value) == callable.api_func.restype
+ assert not error_value # only support error=NULL
+ error_value = 0 # because NULL is not hashable
+
+ if callable.api_func.result_is_ll:
+ result_kind = "L"
+ elif callable.api_func.result_borrowed:
+ result_kind = "B" # note: 'result_borrowed' is ignored if we also
+ else: # say 'result_is_ll=True' (in this case it's
+ result_kind = "." # up to you to handle refcounting anyway)
+
+ signature = (tuple(argtypesw),
+ callable.api_func.restype,
+ result_kind,
+ error_value,
+ gil)
+
+ cache = space.fromcache(WrapperCache)
+ cache.stats[1] += 1
+ try:
+ wrapper_gen = cache.wrapper_gens[signature]
+ except KeyError:
+ print signature
+ wrapper_gen = cache.wrapper_gens[signature] = WrapperGen(space,
+ signature)
+ cache.stats[0] += 1
+ print 'Wrapper cache [wrappers/total]:', cache.stats
+ return wrapper_gen.make_wrapper(callable)
+
+
+@dont_inline
+def deadlock_error(funcname):
+ fatalerror_notb("GIL deadlock detected when a CPython C extension "
+ "module calls '%s'" % (funcname,))
+
+@dont_inline
+def no_gil_error(funcname):
+ fatalerror_notb("GIL not held when a CPython C extension "
+ "module calls '%s'" % (funcname,))
+
+@dont_inline
+def not_supposed_to_fail(funcname):
+ raise SystemError("The function '%s' was not supposed to fail"
+ % (funcname,))
+
+@dont_inline
+def unexpected_exception(funcname, e, tb):
+ print 'Fatal error in cpyext, CPython compatibility layer,
calling',funcname
+ print 'Either report a bug or consider not using this particular extension'
+ if not we_are_translated():
+ if tb is None:
+ tb = sys.exc_info()[2]
+ import traceback
+ traceback.print_exc()
+ if sys.stdout == sys.__stdout__:
+ import pdb; pdb.post_mortem(tb)
+ # we can't do much here, since we're in ctypes, swallow
+ else:
+ print str(e)
+ pypy_debug_catch_fatal_exception()
+ assert False
+
+def make_wrapper_second_level(space, callable2name, argtypesw, restype,
+ result_kind, error_value, gil):
from rpython.rlib import rgil
- names = callable.api_func.argnames
- argtypes_enum_ui =
unrolling_iterable(enumerate(zip(callable.api_func.argtypes,
- [name.startswith("w_") for name in names])))
- fatal_value = callable.api_func.restype._defl()
+ argtypes_enum_ui = unrolling_iterable(enumerate(argtypesw))
+ fatal_value = restype._defl()
gil_acquire = (gil == "acquire" or gil == "around")
gil_release = (gil == "release" or gil == "around")
pygilstate_ensure = (gil == "pygilstate_ensure")
pygilstate_release = (gil == "pygilstate_release")
assert (gil is None or gil_acquire or gil_release
or pygilstate_ensure or pygilstate_release)
- deadlock_error = ("GIL deadlock detected when a CPython C extension "
- "module calls %r" % (callable.__name__,))
- no_gil_error = ("GIL not held when a CPython C extension "
- "module calls %r" % (callable.__name__,))
+ expected_nb_args = len(argtypesw) + pygilstate_ensure
- @specialize.ll()
- def wrapper(*args):
+ if isinstance(restype, lltype.Ptr) and error_value == 0:
+ error_value = lltype.nullptr(restype.TO)
+ if error_value is not CANNOT_FAIL:
+ assert lltype.typeOf(error_value) == lltype.typeOf(fatal_value)
+
+ def invalid(err):
+ "NOT_RPYTHON: translation-time crash if this ends up being called"
+ raise ValueError(err)
+ invalid.__name__ = 'invalid_%s' % (callable2name[0][1],)
+
+ def nameof(callable):
+ for c, n in callable2name:
+ if c is callable:
+ return n
+ return '<unknown function>'
+ nameof._dont_inline_ = True
+
+ def wrapper_second_level(*args):
from pypy.module.cpyext.pyobject import make_ref, from_ref, is_pyobj
from pypy.module.cpyext.pyobject import as_pyobj
# we hope that malloc removal removes the newtuple() that is
# inserted exactly here by the varargs specializer
+ callable = args[-1]
+ args = args[:-1]
# see "Handling of the GIL" above (careful, we don't have the GIL here)
tid = rthread.get_or_make_ident()
if gil_acquire:
if cpyext_glob_tid_ptr[0] == tid:
- fatalerror_notb(deadlock_error)
+ deadlock_error(nameof(callable))
rgil.acquire()
assert cpyext_glob_tid_ptr[0] == 0
elif pygilstate_ensure:
@@ -711,7 +840,7 @@
args += (pystate.PyGILState_UNLOCKED,)
else:
if cpyext_glob_tid_ptr[0] != tid:
- fatalerror_notb(no_gil_error)
+ no_gil_error(nameof(callable))
cpyext_glob_tid_ptr[0] = 0
rffi.stackcounter.stacks_counter += 1
@@ -722,8 +851,7 @@
try:
if not we_are_translated() and DEBUG_WRAPPER:
print >>sys.stderr, callable,
- assert len(args) == (len(callable.api_func.argtypes) +
- pygilstate_ensure)
+ assert len(args) == expected_nb_args
for i, (typ, is_wrapped) in argtypes_enum_ui:
arg = args[i]
if is_PyObject(typ) and is_wrapped:
@@ -757,41 +885,31 @@
failed = False
if failed:
- error_value = callable.api_func.error_value
if error_value is CANNOT_FAIL:
- raise SystemError("The function '%s' was not supposed to
fail"
- % (callable.__name__,))
+ raise not_supposed_to_fail(nameof(callable))
retval = error_value
- elif is_PyObject(callable.api_func.restype):
+ elif is_PyObject(restype):
if is_pyobj(result):
- retval = result
+ if result_kind != "L":
+ raise invalid("missing result_is_ll=True")
else:
- if result is not None:
- if callable.api_func.result_borrowed:
- retval = as_pyobj(space, result)
- else:
- retval = make_ref(space, result)
- retval = rffi.cast(callable.api_func.restype, retval)
+ if result_kind == "L":
+ raise invalid("result_is_ll=True but not ll PyObject")
+ if result_kind == "B": # borrowed
+ result = as_pyobj(space, result)
else:
- retval = lltype.nullptr(PyObject.TO)
- elif callable.api_func.restype is not lltype.Void:
- retval = rffi.cast(callable.api_func.restype, result)
+ result = make_ref(space, result)
+ retval = rffi.cast(restype, result)
+
+ elif restype is not lltype.Void:
+ retval = rffi.cast(restype, result)
+
except Exception, e:
- print 'Fatal error in cpyext, CPython compatibility layer,
calling', callable.__name__
- print 'Either report a bug or consider not using this particular
extension'
- if not we_are_translated():
- if tb is None:
- tb = sys.exc_info()[2]
- import traceback
- traceback.print_exc()
- if sys.stdout == sys.__stdout__:
- import pdb; pdb.post_mortem(tb)
- # we can't do much here, since we're in ctypes, swallow
- else:
- print str(e)
- pypy_debug_catch_fatal_exception()
- assert False
+ unexpected_exception(nameof(callable), e, tb)
+ return fatal_value
+
+ assert lltype.typeOf(retval) == restype
rffi.stackcounter.stacks_counter -= 1
# see "Handling of the GIL" above
@@ -808,9 +926,9 @@
cpyext_glob_tid_ptr[0] = tid
return retval
- callable._always_inline_ = 'try'
- wrapper.__name__ = "wrapper for %r" % (callable, )
- return wrapper
+
+ wrapper_second_level._dont_inline_ = True
+ return wrapper_second_level
def process_va_name(name):
return name.replace('*', '_star')
diff --git a/pypy/module/cpyext/bytesobject.py
b/pypy/module/cpyext/bytesobject.py
--- a/pypy/module/cpyext/bytesobject.py
+++ b/pypy/module/cpyext/bytesobject.py
@@ -124,7 +124,7 @@
#_______________________________________________________________________
-@cpython_api([CONST_STRING, Py_ssize_t], PyObject)
+@cpython_api([CONST_STRING, Py_ssize_t], PyObject, result_is_ll=True)
def PyString_FromStringAndSize(space, char_p, length):
if char_p:
s = rffi.charpsize2str(char_p, length)
diff --git a/pypy/module/cpyext/frameobject.py
b/pypy/module/cpyext/frameobject.py
--- a/pypy/module/cpyext/frameobject.py
+++ b/pypy/module/cpyext/frameobject.py
@@ -67,7 +67,8 @@
track_reference(space, py_obj, w_obj)
return w_obj
-@cpython_api([PyThreadState, PyCodeObject, PyObject, PyObject], PyFrameObject)
+@cpython_api([PyThreadState, PyCodeObject, PyObject, PyObject], PyFrameObject,
+ result_is_ll=True)
def PyFrame_New(space, tstate, w_code, w_globals, w_locals):
typedescr = get_typedescr(PyFrame.typedef)
py_obj = typedescr.allocate(space, space.gettypeobject(PyFrame.typedef))
diff --git a/pypy/module/cpyext/object.py b/pypy/module/cpyext/object.py
--- a/pypy/module/cpyext/object.py
+++ b/pypy/module/cpyext/object.py
@@ -34,11 +34,11 @@
def PyObject_Free(space, ptr):
lltype.free(ptr, flavor='raw')
-@cpython_api([PyTypeObjectPtr], PyObject)
+@cpython_api([PyTypeObjectPtr], PyObject, result_is_ll=True)
def _PyObject_New(space, type):
return _PyObject_NewVar(space, type, 0)
-@cpython_api([PyTypeObjectPtr, Py_ssize_t], PyObject)
+@cpython_api([PyTypeObjectPtr, Py_ssize_t], PyObject, result_is_ll=True)
def _PyObject_NewVar(space, type, itemcount):
w_type = from_ref(space, rffi.cast(PyObject, type))
assert isinstance(w_type, W_TypeObject)
@@ -63,7 +63,7 @@
if pto.c_tp_flags & Py_TPFLAGS_HEAPTYPE:
Py_DecRef(space, rffi.cast(PyObject, pto))
-@cpython_api([PyTypeObjectPtr], PyObject)
+@cpython_api([PyTypeObjectPtr], PyObject, result_is_ll=True)
def _PyObject_GC_New(space, type):
return _PyObject_New(space, type)
@@ -193,7 +193,7 @@
space.delitem(w_obj, w_key)
return 0
-@cpython_api([PyObject, PyTypeObjectPtr], PyObject)
+@cpython_api([PyObject, PyTypeObjectPtr], PyObject, result_is_ll=True)
def PyObject_Init(space, obj, type):
"""Initialize a newly-allocated object op with its type and initial
reference. Returns the initialized object. If type indicates that the
@@ -207,7 +207,7 @@
obj.c_ob_refcnt = 1
return obj
-@cpython_api([PyVarObject, PyTypeObjectPtr, Py_ssize_t], PyObject)
+@cpython_api([PyVarObject, PyTypeObjectPtr, Py_ssize_t], PyObject,
result_is_ll=True)
def PyObject_InitVar(space, py_obj, type, size):
"""This does everything PyObject_Init() does, and also initializes the
length information for a variable-size object."""
@@ -308,7 +308,7 @@
w_res = PyObject_RichCompare(space, ref1, ref2, opid)
return int(space.is_true(w_res))
-@cpython_api([PyObject], PyObject)
+@cpython_api([PyObject], PyObject, result_is_ll=True)
def PyObject_SelfIter(space, ref):
"""Undocumented function, this is what CPython does."""
Py_IncRef(space, ref)
diff --git a/pypy/module/cpyext/pystate.py b/pypy/module/cpyext/pystate.py
--- a/pypy/module/cpyext/pystate.py
+++ b/pypy/module/cpyext/pystate.py
@@ -168,8 +168,16 @@
state = space.fromcache(InterpreterState)
return state.get_thread_state(space)
-@cpython_api([], PyObject, error=CANNOT_FAIL)
+@cpython_api([], PyObject, result_is_ll=True, error=CANNOT_FAIL)
def PyThreadState_GetDict(space):
+ """Return a dictionary in which extensions can store thread-specific state
+ information. Each extension should use a unique key to use to store state
in
+ the dictionary. It is okay to call this function when no current thread
state
+ is available. If this function returns NULL, no exception has been raised
and
+ the caller should assume no current thread state is available.
+
+ Previously this could only be called when a current thread is active, and
NULL
+ meant that an exception was raised."""
state = space.fromcache(InterpreterState)
return state.get_thread_state(space).c_dict
diff --git a/pypy/module/cpyext/stubs.py b/pypy/module/cpyext/stubs.py
--- a/pypy/module/cpyext/stubs.py
+++ b/pypy/module/cpyext/stubs.py
@@ -1156,19 +1156,6 @@
PyInterpreterState_Clear()."""
raise NotImplementedError
-@cpython_api([], PyObject)
-def PyThreadState_GetDict(space):
- """Return a dictionary in which extensions can store thread-specific state
- information. Each extension should use a unique key to use to store state
in
- the dictionary. It is okay to call this function when no current thread
state
- is available. If this function returns NULL, no exception has been raised
and
- the caller should assume no current thread state is available.
-
- Previously this could only be called when a current thread is active, and
NULL
- meant that an exception was raised."""
- borrow_from()
- raise NotImplementedError
-
@cpython_api([lltype.Signed, PyObject], rffi.INT_real, error=CANNOT_FAIL)
def PyThreadState_SetAsyncExc(space, id, exc):
"""Asynchronously raise an exception in a thread. The id argument is the
thread
diff --git a/pypy/module/cpyext/test/test_pyerrors.py
b/pypy/module/cpyext/test/test_pyerrors.py
--- a/pypy/module/cpyext/test/test_pyerrors.py
+++ b/pypy/module/cpyext/test/test_pyerrors.py
@@ -365,6 +365,8 @@
assert "in test_PyErr_Display\n" in output
assert "ZeroDivisionError" in output
+ @pytest.mark.skipif(True, reason=
+ "XXX seems to pass, but doesn't: 'py.test -s' shows errors in
PyObject_Free")
def test_GetSetExcInfo(self):
import sys
if self.runappdirect and (sys.version_info.major < 3 or
diff --git a/pypy/module/cpyext/tupleobject.py
b/pypy/module/cpyext/tupleobject.py
--- a/pypy/module/cpyext/tupleobject.py
+++ b/pypy/module/cpyext/tupleobject.py
@@ -127,7 +127,7 @@
#_______________________________________________________________________
-@cpython_api([Py_ssize_t], PyObject)
+@cpython_api([Py_ssize_t], PyObject, result_is_ll=True)
def PyTuple_New(space, size):
return rffi.cast(PyObject, new_empty_tuple(space, size))
@@ -150,7 +150,8 @@
decref(space, old_ref)
return 0
-@cpython_api([PyObject, Py_ssize_t], PyObject, result_borrowed=True)
+@cpython_api([PyObject, Py_ssize_t], PyObject,
+ result_borrowed=True, result_is_ll=True)
def PyTuple_GetItem(space, ref, index):
if not tuple_check_ref(space, ref):
PyErr_BadInternalCall(space)
diff --git a/pypy/module/cpyext/typeobject.py b/pypy/module/cpyext/typeobject.py
--- a/pypy/module/cpyext/typeobject.py
+++ b/pypy/module/cpyext/typeobject.py
@@ -752,7 +752,7 @@
w_type2 = from_ref(space, rffi.cast(PyObject, b))
return int(abstract_issubclass_w(space, w_type1, w_type2)) #XXX correct?
-@cpython_api([PyTypeObjectPtr, Py_ssize_t], PyObject)
+@cpython_api([PyTypeObjectPtr, Py_ssize_t], PyObject, result_is_ll=True)
def PyType_GenericAlloc(space, type, nitems):
from pypy.module.cpyext.object import _PyObject_NewVar
return _PyObject_NewVar(space, type, nitems)
diff --git a/pypy/module/cpyext/unicodeobject.py
b/pypy/module/cpyext/unicodeobject.py
--- a/pypy/module/cpyext/unicodeobject.py
+++ b/pypy/module/cpyext/unicodeobject.py
@@ -328,7 +328,7 @@
return unicodeobject.encode_object(space, w_unicode, 'unicode-escape',
'strict')
-@cpython_api([CONST_WSTRING, Py_ssize_t], PyObject)
+@cpython_api([CONST_WSTRING, Py_ssize_t], PyObject, result_is_ll=True)
def PyUnicode_FromUnicode(space, wchar_p, length):
"""Create a Unicode Object from the Py_UNICODE buffer u of the given size.
u
may be NULL which causes the contents to be undefined. It is the user's
@@ -342,14 +342,14 @@
else:
return rffi.cast(PyObject, new_empty_unicode(space, length))
-@cpython_api([CONST_WSTRING, Py_ssize_t], PyObject)
+@cpython_api([CONST_WSTRING, Py_ssize_t], PyObject, result_is_ll=True)
def PyUnicode_FromWideChar(space, wchar_p, length):
"""Create a Unicode object from the wchar_t buffer w of the given size.
Return NULL on failure."""
# PyPy supposes Py_UNICODE == wchar_t
return PyUnicode_FromUnicode(space, wchar_p, length)
-@cpython_api([PyObject, CONST_STRING], PyObject)
+@cpython_api([PyObject, CONST_STRING], PyObject, result_is_ll=True)
def _PyUnicode_AsDefaultEncodedString(space, ref, errors):
# Returns a borrowed reference.
py_uni = rffi.cast(PyUnicodeObject, ref)
@@ -430,7 +430,7 @@
w_str = space.wrap(rffi.charp2str(s))
return space.call_method(w_str, 'decode', space.wrap("utf-8"))
-@cpython_api([CONST_STRING, Py_ssize_t], PyObject)
+@cpython_api([CONST_STRING, Py_ssize_t], PyObject, result_is_ll=True)
def PyUnicode_FromStringAndSize(space, s, size):
"""Create a Unicode Object from the char buffer u. The bytes will be
interpreted as being UTF-8 encoded. u may also be NULL which causes the
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit