summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
authorFederico Caselli <cfederico87@gmail.com>2023-04-19 18:39:18 -0400
committermike bayer <mike_mp@zzzcomputing.com>2023-04-26 19:48:00 +0000
commit105f18be353965b064750726597b63334fc0716b (patch)
tree29ee8a339ea86dbb37fc07b2e654d11c5922a419 /lib/sqlalchemy
parentff198e35f0e04b8d38df25df234e72259069b4d1 (diff)
downloadsqlalchemy-105f18be353965b064750726597b63334fc0716b.tar.gz
Performance improvement in Row
Various performance improvements to Row instanciation - avoid passing processors if they are all None - improve processor logic in cython - improve tuplegetter using slices when contiguous indexes are used Some timing follow. In particular [base_]row_new_proc that tests using processors has a 25% improvement compared to before in cython. Looking at the [b]row_new_proc_none that test a list of processors all None, this has 50% improvement in cython when passing the none list, but in this patch it would usually be disabled by passing None, so the performance gain is actually 90%, since it would run the case [base_]row_new. Tuplegetter is a bit faster in the single item get and when getting sequential indexes (like indexes 1,2,3,4) at the cost of a bit longer creation time in python, cython is mostly the same. Current times | python | cython | cy / py | base_row_new | 0.639817400 | 0.118265500 | 0.184842582 | row_new | 0.680355100 | 0.129714600 | 0.190657202 | base_row_new_proc | 3.076538900 | 1.488428600 | 0.483799701 | row_new_proc | 3.119700100 | 1.532197500 | 0.491136151 | brow_new_proc_none | 1.917702300 | 0.475511500 | 0.247958977 | row_new_proc_none | 1.956253300 | 0.497803100 | 0.254467609 | tuplegetter_one | 0.152512600 | 0.148523900 | 0.973846751 | tuplegetter_many | 0.184394100 | 0.184511500 | 1.000636680 | tuplegetter_seq | 0.154832800 | 0.156270100 | 1.009282917 | tuplegetter_new_one | 0.523730000 | 0.343402200 | 0.655685563 | tuplegetter_new_many| 0.738924400 | 0.420961400 | 0.569694816 | tuplegetter_new_seq | 1.062036900 | 0.495462000 | 0.466520514 | Parent commit times | python | cython | cy / py | base_row_new | 0.643890800 | 0.113548300 | 0.176347138 | row_new | 0.674885900 | 0.124391800 | 0.184315304 | base_row_new_proc | 3.072020400 | 2.017367000 | 0.656690626 | row_new_proc | 3.109943400 | 2.048359400 | 0.658648450 | brow_new_proc_none | 1.967133700 | 1.006326000 | 0.511569702 | row_new_proc_none | 1.960814900 | 1.025217800 | 0.522852922 | tuplegetter_one | 0.197359900 | 0.205999000 | 1.043773330 | tuplegetter_many | 0.196575900 | 0.194888500 | 0.991416038 | tuplegetter_seq | 0.192723900 | 0.205635000 | 1.066992729 | tuplegetter_new_one | 0.534644500 | 0.414311700 | 0.774929322 | tuplegetter_new_many| 0.479376500 | 0.417448100 | 0.870814694 | tuplegetter_new_seq | 0.481580200 | 0.412697900 | 0.856966088 | Change-Id: I2ca1f49dca2beff625c283f1363c29c8ccc0c3f7
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/cyextension/resultproxy.pyx38
-rw-r--r--lib/sqlalchemy/engine/_py_row.py15
-rw-r--r--lib/sqlalchemy/engine/cursor.py44
-rw-r--r--lib/sqlalchemy/engine/default.py10
-rw-r--r--lib/sqlalchemy/engine/result.py29
-rw-r--r--lib/sqlalchemy/engine/row.py10
6 files changed, 86 insertions, 60 deletions
diff --git a/lib/sqlalchemy/cyextension/resultproxy.pyx b/lib/sqlalchemy/cyextension/resultproxy.pyx
index c358f043f..0d7eeece9 100644
--- a/lib/sqlalchemy/cyextension/resultproxy.pyx
+++ b/lib/sqlalchemy/cyextension/resultproxy.pyx
@@ -13,12 +13,7 @@ cdef class BaseRow:
self._key_to_index = key_to_index
if processors:
- self._data = tuple(
- [
- proc(value) if proc else value
- for proc, value in zip(processors, data)
- ]
- )
+ self._data = _apply_processors(processors, data)
else:
self._data = tuple(data)
@@ -64,6 +59,20 @@ cdef class BaseRow:
def __getattr__(self, name):
return self._get_by_key_impl(name, 1)
+ def _to_tuple_instance(self):
+ return self._data
+
+
+cdef tuple _apply_processors(proc, data):
+ res = []
+ for i in range(len(proc)):
+ p = proc[i]
+ if p is None:
+ res.append(data[i])
+ else:
+ res.append(p(data[i]))
+ return tuple(res)
+
def rowproxy_reconstructor(cls, state):
obj = cls.__new__(cls)
@@ -71,10 +80,17 @@ def rowproxy_reconstructor(cls, state):
return obj
-def tuplegetter(*indexes):
- it = operator.itemgetter(*indexes)
+cdef int is_contiguous(tuple indexes):
+ cdef int i
+ for i in range(1, len(indexes)):
+ if indexes[i-1] != indexes[i] -1:
+ return 0
+ return 1
+
- if len(indexes) > 1:
- return it
+def tuplegetter(*indexes):
+ if len(indexes) == 1 or is_contiguous(indexes) != 0:
+ # slice form is faster but returns a list if input is list
+ return operator.itemgetter(slice(indexes[0], indexes[-1] + 1))
else:
- return lambda row: (it(row),)
+ return operator.itemgetter(*indexes)
diff --git a/lib/sqlalchemy/engine/_py_row.py b/lib/sqlalchemy/engine/_py_row.py
index 4a9acec9b..3358abd78 100644
--- a/lib/sqlalchemy/engine/_py_row.py
+++ b/lib/sqlalchemy/engine/_py_row.py
@@ -99,6 +99,9 @@ class BaseRow:
pass
self._parent._key_not_found(name, True)
+ def _to_tuple_instance(self) -> Tuple[Any, ...]:
+ return self._data
+
# This reconstructor is necessary so that pickles with the Cy extension or
# without use the same Binary format.
@@ -111,9 +114,9 @@ def rowproxy_reconstructor(
def tuplegetter(*indexes: int) -> _TupleGetterType:
- it = operator.itemgetter(*indexes)
-
- if len(indexes) > 1:
- return it
- else:
- return lambda row: (it(row),)
+ if len(indexes) != 1:
+ for i in range(1, len(indexes)):
+ if indexes[i - 1] != indexes[i] - 1:
+ return operator.itemgetter(*indexes)
+ # slice form is faster but returns a list if input is list
+ return operator.itemgetter(slice(indexes[0], indexes[-1] + 1))
diff --git a/lib/sqlalchemy/engine/cursor.py b/lib/sqlalchemy/engine/cursor.py
index bd46f30ac..7491afc3e 100644
--- a/lib/sqlalchemy/engine/cursor.py
+++ b/lib/sqlalchemy/engine/cursor.py
@@ -14,6 +14,7 @@ from __future__ import annotations
import collections
import functools
+import operator
import typing
from typing import Any
from typing import cast
@@ -1440,39 +1441,46 @@ class CursorResult(Result[_T]):
# getter assuming no transformations will be called as this
# is the most common case
- if echo:
- log = self.context.connection._log_debug
-
- def _log_row(row):
- log("Row %r", sql_util._repr_row(row))
- return row
-
- self._row_logging_fn = log_row = _log_row
- else:
- log_row = None
-
metadata = self._init_metadata(context, cursor_description)
_make_row = functools.partial(
Row,
metadata,
- metadata._processors,
+ metadata._effective_processors,
metadata._key_to_index,
)
- if log_row:
+
+ if context._num_sentinel_cols:
+ sentinel_filter = operator.itemgetter(
+ slice(-context._num_sentinel_cols)
+ )
+
+ def _sliced_row(raw_data):
+ return _make_row(sentinel_filter(raw_data))
+
+ sliced_row = _sliced_row
+ else:
+ sliced_row = _make_row
+
+ if echo:
+ log = self.context.connection._log_debug
+
+ def _log_row(row):
+ log("Row %r", sql_util._repr_row(row))
+ return row
+
+ self._row_logging_fn = _log_row
def _make_row_2(row):
- made_row = _make_row(row)
- assert log_row is not None
- log_row(made_row)
- return made_row
+ return _log_row(sliced_row(row))
make_row = _make_row_2
else:
- make_row = _make_row
+ make_row = sliced_row
self._set_memoized_attribute("_row_getter", make_row)
else:
+ assert context._num_sentinel_cols == 0
self._metadata = self._no_result_metadata
def _init_metadata(self, context, cursor_description):
diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py
index 8992334ee..0b884d621 100644
--- a/lib/sqlalchemy/engine/default.py
+++ b/lib/sqlalchemy/engine/default.py
@@ -1881,11 +1881,11 @@ class DefaultExecutionContext(ExecutionContext):
strategy = _cursor._NO_CURSOR_DML
elif self._num_sentinel_cols:
assert self.execute_style is ExecuteStyle.INSERTMANYVALUES
- if cursor_description:
- # strip out the sentinel columns from cursor description
- cursor_description = cursor_description[
- 0 : -(self._num_sentinel_cols)
- ]
+ # strip out the sentinel columns from cursor description
+ # a similar logic is done to the rows only in CursorResult
+ cursor_description = cursor_description[
+ 0 : -self._num_sentinel_cols
+ ]
result: _cursor.CursorResult[Any] = _cursor.CursorResult(
self, strategy, cursor_description
diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py
index cc6d26c88..cf34c195a 100644
--- a/lib/sqlalchemy/engine/result.py
+++ b/lib/sqlalchemy/engine/result.py
@@ -41,6 +41,7 @@ from ..sql.base import _generative
from ..sql.base import HasMemoized
from ..sql.base import InPlaceGenerative
from ..util import HasMemoized_ro_memoized_attribute
+from ..util import NONE_SET
from ..util._has_cy import HAS_CYEXTENSION
from ..util.typing import Literal
from ..util.typing import Self
@@ -84,7 +85,7 @@ across all the result types
_InterimSupportsScalarsRowType = Union[Row, Any]
_ProcessorsType = Sequence[Optional["_ResultProcessorType[Any]"]]
-_TupleGetterType = Callable[[Sequence[Any]], Tuple[Any, ...]]
+_TupleGetterType = Callable[[Sequence[Any]], Sequence[Any]]
_UniqueFilterType = Callable[[Any], Any]
_UniqueFilterStateType = Tuple[Set[Any], Optional[_UniqueFilterType]]
@@ -205,6 +206,13 @@ class ResultMetaData:
else:
self._key_fallback(key, None)
+ @property
+ def _effective_processors(self) -> Optional[_ProcessorsType]:
+ if not self._processors or NONE_SET.issuperset(self._processors):
+ return None
+ else:
+ return self._processors
+
class RMKeyView(typing.KeysView[Any]):
__slots__ = ("_parent", "_keys")
@@ -390,7 +398,7 @@ def result_tuple(
) -> Callable[[Iterable[Any]], Row[Any]]:
parent = SimpleResultMetaData(fields, extra)
return functools.partial(
- Row, parent, parent._processors, parent._key_to_index
+ Row, parent, parent._effective_processors, parent._key_to_index
)
@@ -454,7 +462,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]):
def process_row( # type: ignore
metadata: ResultMetaData,
- processors: _ProcessorsType,
+ processors: Optional[_ProcessorsType],
key_to_index: Mapping[_KeyType, int],
scalar_obj: Any,
) -> Row[Any]:
@@ -468,7 +476,7 @@ class ResultInternal(InPlaceGenerative, Generic[_R]):
metadata = self._metadata
key_to_index = metadata._key_to_index
- processors = metadata._processors
+ processors = metadata._effective_processors
tf = metadata._tuplefilter
if tf and not real_result._source_supports_scalars:
@@ -489,21 +497,12 @@ class ResultInternal(InPlaceGenerative, Generic[_R]):
process_row, metadata, processors, key_to_index
)
- fns: Tuple[Any, ...] = ()
-
if real_result._row_logging_fn:
- fns = (real_result._row_logging_fn,)
- else:
- fns = ()
-
- if fns:
+ _log_row = real_result._row_logging_fn
_make_row = make_row
def make_row(row: _InterimRowType[Row[Any]]) -> _R:
- interim_row = _make_row(row)
- for fn in fns:
- interim_row = fn(interim_row)
- return interim_row # type: ignore
+ return _log_row(_make_row(row)) # type: ignore
return make_row
diff --git a/lib/sqlalchemy/engine/row.py b/lib/sqlalchemy/engine/row.py
index 4b767da09..da781334a 100644
--- a/lib/sqlalchemy/engine/row.py
+++ b/lib/sqlalchemy/engine/row.py
@@ -40,7 +40,7 @@ else:
if TYPE_CHECKING:
from .result import _KeyType
from .result import RMKeyView
- from ..sql.type_api import _ResultProcessorType
+ from .result import _ProcessorsType
_T = TypeVar("_T", bound=Any)
_TP = TypeVar("_TP", bound=Tuple[Any, ...])
@@ -131,9 +131,9 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]):
return RowMapping(self._parent, None, self._key_to_index, self._data)
def _filter_on_values(
- self, filters: Optional[Sequence[Optional[_ResultProcessorType[Any]]]]
+ self, processor: Optional[_ProcessorsType]
) -> Row[Any]:
- return Row(self._parent, filters, self._key_to_index, self._data)
+ return Row(self._parent, processor, self._key_to_index, self._data)
if not TYPE_CHECKING:
@@ -163,9 +163,9 @@ class Row(BaseRow, Sequence[Any], Generic[_TP]):
def _op(self, other: Any, op: Callable[[Any, Any], bool]) -> bool:
return (
- op(tuple(self), tuple(other))
+ op(self._to_tuple_instance(), other._to_tuple_instance())
if isinstance(other, Row)
- else op(tuple(self), other)
+ else op(self._to_tuple_instance(), other)
)
__hash__ = BaseRow.__hash__