Opened 3 years ago
Closed 3 years ago
#33355 closed Cleanup/optimization (fixed)
Optimize SQLite backend connection functions
Reported by: | Adam Johnson | Owned by: | Adam Johnson |
---|---|---|---|
Component: | Database layer (models, ORM) | Version: | dev |
Severity: | Normal | Keywords: | |
Cc: | Nick Pope, Carlton Gibson | Triage Stage: | Ready for checkin |
Has patch: | yes | Needs documentation: | no |
Needs tests: | no | Patch needs improvement: | no |
Easy pickings: | no | UI/UX: | no |
Description
The SQLite backend registers a bunch of data conversion functions when creating a new connection, with conn.create_function
. These functions use the @none_guard
decorator to check for None
s in arguments:
def none_guard(func): """ Decorator that returns None if any of the arguments to the decorated function are None. Many SQL functions return NULL if any of their arguments are NULL. This decorator simplifies the implementation of this for the custom functions registered below. """ @functools.wraps(func) def wrapper(*args, **kwargs): return None if None in args else func(*args, **kwargs) return wrapper
This decorator is wasteful. At call time, it makes Python coerce arguments to *args and kwargs and back , and as a decorator it forces two layers of functions where one would do.
We can save this time by explicitly "inlining" the None
checks. This saves a small amount of time per call, but considering that these functions can be called thousands of times in a single query, it adds up fast. (...perhaps millions on larger data sets or suboptimal queries!)
Additionally, time and memory is wasted on every new SQLite connection applying the decorator to create new copies of the same function. (And list_aggregate()
creates the exact same classes each time.) Considering that Django creates a new connection per request by default (and per test), this adds up.
A small benchmark with one of the shorter functions (Python 3.10.0):
In [1]: import functools In [2]: def none_guard(func): ...: """ ...: Decorator that returns None if any of the arguments to the decorated ...: function are None. Many SQL functions return NULL if any of their arguments ...: are NULL. This decorator simplifies the implementation of this for the ...: custom functions registered below. ...: """ ...: @functools.wraps(func) ...: def wrapper(*args, **kwargs): ...: return None if None in args else func(*args, **kwargs) ...: return wrapper ...: In [3]: @none_guard ...: def _sqlite_rpad(text, length, fill_text): ...: return (text + fill_text * length)[:length] ...: In [4]: def _sqlite_rpad_inlined(text, length, fill_text): ...: if text is None or length is None or fill_text is None: ...: return None ...: return (text + fill_text * length)[:length] ...: In [5]: %timeit _sqlite_rpad("Hello world", 20, " ") 534 ns ± 30.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) In [6]: %timeit _sqlite_rpad_inlined("Hello world", 20, " ") 328 ns ± 26.5 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
~40% reduction.
Change History (17)
comment:1 by , 3 years ago
Cc: | added |
---|
comment:3 by , 3 years ago
Cc: | added |
---|
comment:4 by , 3 years ago
Owner: | changed from | to
---|---|
Triage Stage: | Unreviewed → Accepted |
comment:7 by , 3 years ago
Triage Stage: | Accepted → Ready for checkin |
---|
Thanks Adam. I think this is ready.
comment:10 by , 3 years ago
Patch needs improvement: | set |
---|---|
Triage Stage: | Ready for checkin → Accepted |
comment:12 by , 3 years ago
Patch needs improvement: | unset |
---|---|
Triage Stage: | Accepted → Ready for checkin |
comment:17 by , 3 years ago
Resolution: | → fixed |
---|---|
Status: | assigned → closed |
This would force us to create small wrappers for many (~20) functions from the
math
module.