From: Inada Naoki Date: Mon, 7 Apr 2025 23:55:48 +0000 (-0400) Subject: optimize `@util.decorator` X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=370f13fe88ec5e4ee2400e23717db1e13df102bf;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git optimize `@util.decorator` ### Description util.decorator uses code generation + eval to create signature matching wrapper. It consumes some CPU because we can not use pyc cache. Additionally, each wrapped function has own globals for function annotations. By stripping function annotations from eval-ed code, compile time and memory usage are saved. ```python from sqlalchemy.util import decorator from sqlalchemy import * import timeit import tracemalloc import sqlalchemy.orm._orm_constructors @decorator def with_print(fn, *args, **kwargs): res = fn(*args, **kwargs) print(f"{fn.__name__}(*{args}, **{kwargs}) => {res}") return res # test PI = 3.14 def f(): @with_print def add(x: int|float, *, y: int|float=PI) -> int|float: return x + y return add add = f() add(1) print(add.__annotations__) # benchmark print(timeit.timeit(f, number=1000)*1000, "us") # memory tracemalloc.start(1) [f() for _ in range(1000)] mem, peak = tracemalloc.get_traced_memory() tracemalloc.stop() print(f"{mem=}, {peak=}") ``` Result: ``` $ .venv/bin/python -VV Python 3.14.0a6 (main, Mar 17 2025, 21:27:10) [Clang 20.1.0 ] $ .venv/bin/python sample.py add(*(1,), **{'y': 3.14}) => 4.140000000000001 {'x': int | float, 'y': int | float, 'return': int | float} 35.93937499681488 us mem=9252896, peak=9300808 $ git switch - Switched to branch 'opt-decorator' $ .venv/bin/python sample.py add(*(1,), **{'y': 3.14}) => 4.140000000000001 {'x': int | float, 'y': int | float, 'return': int | float} 23.32574996398762 us mem=1439032, peak=1476423 ``` ### Checklist This pull request is: - [ ] A documentation / typographical / small typing error fix - Good to go, no issue or tests are needed - [x] A short code fix - please include the issue number, and create an issue if none exists, which must include a complete example of the issue. one line code fixes without an issue and demonstration will not be accepted. - Please include: `Fixes: #` in the commit message - please include tests. one line code fixes without tests will not be accepted. - [ ] A new feature implementation - please include the issue number, and create an issue if none exists, which must include a complete example of how the feature would look. - Please include: `Fixes: #` in the commit message - please include tests. Closes: #12502 Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/12502 Pull-request-sha: 34409cbbfd2dee65bf86a85a87e415c9af47dc62 Change-Id: I88b88eb6eb018608bc2881459f58564881d06641 --- diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index f7879d55c0..6c98504445 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -244,10 +244,30 @@ def decorator(target: Callable[..., Any]) -> Callable[[_Fn], _Fn]: if not inspect.isfunction(fn) and not inspect.ismethod(fn): raise Exception("not a decoratable function") - spec = compat.inspect_getfullargspec(fn) - env: Dict[str, Any] = {} + # Python 3.14 defer creating __annotations__ until its used. + # We do not want to create __annotations__ now. + annofunc = getattr(fn, "__annotate__", None) + if annofunc is not None: + fn.__annotate__ = None # type: ignore[union-attr] + try: + spec = compat.inspect_getfullargspec(fn) + finally: + fn.__annotate__ = annofunc # type: ignore[union-attr] + else: + spec = compat.inspect_getfullargspec(fn) - spec = _update_argspec_defaults_into_env(spec, env) + # Do not generate code for annotations. + # update_wrapper() copies the annotation from fn to decorated. + # We use dummy defaults for code generation to avoid having + # copy of large globals for compiling. + # We copy __defaults__ and __kwdefaults__ from fn to decorated. + empty_defaults = (None,) * len(spec.defaults or ()) + empty_kwdefaults = dict.fromkeys(spec.kwonlydefaults or ()) + spec = spec._replace( + annotations={}, + defaults=empty_defaults, + kwonlydefaults=empty_kwdefaults, + ) names = ( tuple(cast("Tuple[str, ...]", spec[0])) @@ -292,43 +312,23 @@ def decorator(target: Callable[..., Any]) -> Callable[[_Fn], _Fn]: % metadata ) - mod = sys.modules[fn.__module__] - env.update(vars(mod)) - env.update({targ_name: target, fn_name: fn, "__name__": fn.__module__}) + env: Dict[str, Any] = { + targ_name: target, + fn_name: fn, + "__name__": fn.__module__, + } decorated = cast( types.FunctionType, _exec_code_in_env(code, env, fn.__name__), ) - decorated.__defaults__ = getattr(fn, "__func__", fn).__defaults__ - - decorated.__wrapped__ = fn # type: ignore[attr-defined] + decorated.__defaults__ = fn.__defaults__ + decorated.__kwdefaults__ = fn.__kwdefaults__ # type: ignore return update_wrapper(decorated, fn) # type: ignore[return-value] return update_wrapper(decorate, target) # type: ignore[return-value] -def _update_argspec_defaults_into_env(spec, env): - """given a FullArgSpec, convert defaults to be symbol names in an env.""" - - if spec.defaults: - new_defaults = [] - i = 0 - for arg in spec.defaults: - if type(arg).__module__ not in ("builtins", "__builtin__"): - name = "x%d" % i - env[name] = arg - new_defaults.append(name) - i += 1 - else: - new_defaults.append(arg) - elem = list(spec) - elem[3] = tuple(new_defaults) - return compat.FullArgSpec(*elem) - else: - return spec - - def _exec_code_in_env( code: Union[str, types.CodeType], env: Dict[str, Any], fn_name: str ) -> Callable[..., Any]: