bpo-38891: avoid quadratic item access performance of ShareableList (GH-18996)

Avoid linear runtime of ShareableList.__getitem__ and
ShareableList.__setitem__ by storing running allocated bytes in
ShareableList._allocated_bytes instead of the number of bytes for
a particular stored item.

Co-authored-by: Antoine Pitrou <antoine@python.org>
This commit is contained in:
Thomas Krennwallner 2020-04-19 11:19:24 -04:00 committed by GitHub
parent 1ac6e37929
commit c8f1715283
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 49 additions and 31 deletions

View File

@ -252,6 +252,15 @@ class ShareableList:
packing format for any storable value must require no more than 8 packing format for any storable value must require no more than 8
characters to describe its format.""" characters to describe its format."""
# The shared memory area is organized as follows:
# - 8 bytes: number of items (N) as a 64-bit integer
# - (N + 1) * 8 bytes: offsets of each element from the start of the
# data area
# - K bytes: the data area storing item values (with encoding and size
# depending on their respective types)
# - N * 8 bytes: `struct` format string for each element
# - N bytes: index into _back_transforms_mapping for each element
# (for reconstructing the corresponding Python value)
_types_mapping = { _types_mapping = {
int: "q", int: "q",
float: "d", float: "d",
@ -283,7 +292,8 @@ class ShareableList:
return 3 # NoneType return 3 # NoneType
def __init__(self, sequence=None, *, name=None): def __init__(self, sequence=None, *, name=None):
if sequence is not None: if name is None or sequence is not None:
sequence = sequence or ()
_formats = [ _formats = [
self._types_mapping[type(item)] self._types_mapping[type(item)]
if not isinstance(item, (str, bytes)) if not isinstance(item, (str, bytes))
@ -294,10 +304,14 @@ class ShareableList:
] ]
self._list_len = len(_formats) self._list_len = len(_formats)
assert sum(len(fmt) <= 8 for fmt in _formats) == self._list_len assert sum(len(fmt) <= 8 for fmt in _formats) == self._list_len
self._allocated_bytes = tuple( offset = 0
self._alignment if fmt[-1] != "s" else int(fmt[:-1]) # The offsets of each list element into the shared memory's
for fmt in _formats # data area (0 meaning the start of the data area, not the start
) # of the shared memory area).
self._allocated_offsets = [0]
for fmt in _formats:
offset += self._alignment if fmt[-1] != "s" else int(fmt[:-1])
self._allocated_offsets.append(offset)
_recreation_codes = [ _recreation_codes = [
self._extract_recreation_code(item) for item in sequence self._extract_recreation_code(item) for item in sequence
] ]
@ -308,13 +322,9 @@ class ShareableList:
self._format_back_transform_codes self._format_back_transform_codes
) )
else:
requested_size = 8 # Some platforms require > 0.
if name is not None and sequence is None:
self.shm = SharedMemory(name)
else:
self.shm = SharedMemory(name, create=True, size=requested_size) self.shm = SharedMemory(name, create=True, size=requested_size)
else:
self.shm = SharedMemory(name)
if sequence is not None: if sequence is not None:
_enc = _encoding _enc = _encoding
@ -323,7 +333,7 @@ class ShareableList:
self.shm.buf, self.shm.buf,
0, 0,
self._list_len, self._list_len,
*(self._allocated_bytes) *(self._allocated_offsets)
) )
struct.pack_into( struct.pack_into(
"".join(_formats), "".join(_formats),
@ -346,10 +356,12 @@ class ShareableList:
else: else:
self._list_len = len(self) # Obtains size from offset 0 in buffer. self._list_len = len(self) # Obtains size from offset 0 in buffer.
self._allocated_bytes = struct.unpack_from( self._allocated_offsets = list(
self._format_size_metainfo, struct.unpack_from(
self.shm.buf, self._format_size_metainfo,
1 * 8 self.shm.buf,
1 * 8
)
) )
def _get_packing_format(self, position): def _get_packing_format(self, position):
@ -371,7 +383,6 @@ class ShareableList:
def _get_back_transform(self, position): def _get_back_transform(self, position):
"Gets the back transformation function for a single value." "Gets the back transformation function for a single value."
position = position if position >= 0 else position + self._list_len
if (position >= self._list_len) or (self._list_len < 0): if (position >= self._list_len) or (self._list_len < 0):
raise IndexError("Requested position out of range.") raise IndexError("Requested position out of range.")
@ -388,7 +399,6 @@ class ShareableList:
"""Sets the packing format and back transformation code for a """Sets the packing format and back transformation code for a
single value in the list at the specified position.""" single value in the list at the specified position."""
position = position if position >= 0 else position + self._list_len
if (position >= self._list_len) or (self._list_len < 0): if (position >= self._list_len) or (self._list_len < 0):
raise IndexError("Requested position out of range.") raise IndexError("Requested position out of range.")
@ -408,9 +418,9 @@ class ShareableList:
) )
def __getitem__(self, position): def __getitem__(self, position):
position = position if position >= 0 else position + self._list_len
try: try:
offset = self._offset_data_start \ offset = self._offset_data_start + self._allocated_offsets[position]
+ sum(self._allocated_bytes[:position])
(v,) = struct.unpack_from( (v,) = struct.unpack_from(
self._get_packing_format(position), self._get_packing_format(position),
self.shm.buf, self.shm.buf,
@ -425,9 +435,10 @@ class ShareableList:
return v return v
def __setitem__(self, position, value): def __setitem__(self, position, value):
position = position if position >= 0 else position + self._list_len
try: try:
offset = self._offset_data_start \ item_offset = self._allocated_offsets[position]
+ sum(self._allocated_bytes[:position]) offset = self._offset_data_start + item_offset
current_format = self._get_packing_format(position) current_format = self._get_packing_format(position)
except IndexError: except IndexError:
raise IndexError("assignment index out of range") raise IndexError("assignment index out of range")
@ -435,13 +446,15 @@ class ShareableList:
if not isinstance(value, (str, bytes)): if not isinstance(value, (str, bytes)):
new_format = self._types_mapping[type(value)] new_format = self._types_mapping[type(value)]
else: else:
if len(value) > self._allocated_bytes[position]: allocated_length = self._allocated_offsets[position + 1] - item_offset
if len(value) > allocated_length:
raise ValueError("exceeds available storage for existing str") raise ValueError("exceeds available storage for existing str")
if current_format[-1] == "s": if current_format[-1] == "s":
new_format = current_format new_format = current_format
else: else:
new_format = self._types_mapping[str] % ( new_format = self._types_mapping[str] % (
self._allocated_bytes[position], allocated_length,
) )
self._set_packing_format_and_transform( self._set_packing_format_and_transform(
@ -463,33 +476,35 @@ class ShareableList:
@property @property
def format(self): def format(self):
"The struct packing format used by all currently stored values." "The struct packing format used by all currently stored items."
return "".join( return "".join(
self._get_packing_format(i) for i in range(self._list_len) self._get_packing_format(i) for i in range(self._list_len)
) )
@property @property
def _format_size_metainfo(self): def _format_size_metainfo(self):
"The struct packing format used for metainfo on storage sizes." "The struct packing format used for the items' storage offsets."
return f"{self._list_len}q" return "q" * (self._list_len + 1)
@property @property
def _format_packing_metainfo(self): def _format_packing_metainfo(self):
"The struct packing format used for the values' packing formats." "The struct packing format used for the items' packing formats."
return "8s" * self._list_len return "8s" * self._list_len
@property @property
def _format_back_transform_codes(self): def _format_back_transform_codes(self):
"The struct packing format used for the values' back transforms." "The struct packing format used for the items' back transforms."
return "b" * self._list_len return "b" * self._list_len
@property @property
def _offset_data_start(self): def _offset_data_start(self):
return (self._list_len + 1) * 8 # 8 bytes per "q" # - 8 bytes for the list length
# - (N + 1) * 8 bytes for the element offsets
return (self._list_len + 2) * 8
@property @property
def _offset_packing_formats(self): def _offset_packing_formats(self):
return self._offset_data_start + sum(self._allocated_bytes) return self._offset_data_start + self._allocated_offsets[-1]
@property @property
def _offset_back_transform_codes(self): def _offset_back_transform_codes(self):

View File

@ -0,0 +1,3 @@
Fix linear runtime behaviour of the `__getitem__` and `__setitem__` methods in
:class:`multiprocessing.shared_memory.ShareableList`. This avoids quadratic
performance when iterating a `ShareableList`. Patch by Thomas Krennwallner.