-
-
Notifications
You must be signed in to change notification settings - Fork 18.5k
ENH: Optimize putmask implementation for CoW #51268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fdedea4
2ce28c6
94cd34a
1411b6c
c4bd3a7
beb70b0
ce084ad
c5c3a2e
01bf0b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -946,7 +946,7 @@ def _unstack( | |
|
||
# --------------------------------------------------------------------- | ||
|
||
def setitem(self, indexer, value) -> Block: | ||
def setitem(self, indexer, value, using_cow: bool = False) -> Block: | ||
""" | ||
Attempt self.values[indexer] = value, possibly creating a new array. | ||
|
||
|
@@ -956,6 +956,8 @@ def setitem(self, indexer, value) -> Block: | |
The subset of self.values to set | ||
value : object | ||
The value being set | ||
using_cow: bool, default False | ||
Signaling if CoW is used. | ||
|
||
Returns | ||
------- | ||
|
@@ -991,10 +993,17 @@ def setitem(self, indexer, value) -> Block: | |
# checking lib.is_scalar here fails on | ||
# test_iloc_setitem_custom_object | ||
casted = setitem_datetimelike_compat(values, len(vi), casted) | ||
|
||
if using_cow and self.refs.has_reference(): | ||
values = values.copy() | ||
self = self.make_block_same_class( | ||
values.T if values.ndim == 2 else values | ||
) | ||
|
||
values[indexer] = casted | ||
return self | ||
|
||
def putmask(self, mask, new) -> list[Block]: | ||
def putmask(self, mask, new, using_cow: bool = False) -> list[Block]: | ||
""" | ||
putmask the data to the block; it is possible that we may create a | ||
new dtype of block | ||
|
@@ -1022,11 +1031,21 @@ def putmask(self, mask, new) -> list[Block]: | |
new = extract_array(new, extract_numpy=True) | ||
|
||
if noop: | ||
if using_cow: | ||
return [self.copy(deep=False)] | ||
return [self] | ||
Comment on lines
+1034
to
1036
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It also doesn't necessarily harm to always do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think so, but it could also mess up something I am not thinking about right now. I‘d like to keep as is if you don’t feel strongly about it. Could our caching mechanism be a problem (only theoretically, because we should clear the cache when getting here I think), but there might be edge cases we are missing right now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's fine to leave as is for now. It will also be an easy clean-up once CoW is enforced. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep exactly my thinking as well |
||
|
||
try: | ||
casted = np_can_hold_element(values.dtype, new) | ||
|
||
if using_cow and self.refs.has_reference(): | ||
# Do this here to avoid copying twice | ||
values = values.copy() | ||
self = self.make_block_same_class(values) | ||
|
||
putmask_without_repeat(values.T, mask, casted) | ||
if using_cow: | ||
return [self.copy(deep=False)] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need to shallow copy if we already did a hard copy above? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, but the intermediate block will go out of scope, so making a shallow copy here will still only have a single ref in that case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly, it does not hurt, that’s why I wanted to avoid making the check more complex |
||
return [self] | ||
except LossySetitemError: | ||
|
||
|
@@ -1038,7 +1057,7 @@ def putmask(self, mask, new) -> list[Block]: | |
return self.coerce_to_target_dtype(new).putmask(mask, new) | ||
else: | ||
indexer = mask.nonzero()[0] | ||
nb = self.setitem(indexer, new[indexer]) | ||
nb = self.setitem(indexer, new[indexer], using_cow=using_cow) | ||
return [nb] | ||
|
||
else: | ||
|
@@ -1053,7 +1072,7 @@ def putmask(self, mask, new) -> list[Block]: | |
n = new[:, i : i + 1] | ||
|
||
submask = orig_mask[:, i : i + 1] | ||
rbs = nb.putmask(submask, n) | ||
rbs = nb.putmask(submask, n, using_cow=using_cow) | ||
res_blocks.extend(rbs) | ||
return res_blocks | ||
|
||
|
@@ -1448,7 +1467,7 @@ class EABackedBlock(Block): | |
|
||
values: ExtensionArray | ||
|
||
def setitem(self, indexer, value): | ||
def setitem(self, indexer, value, using_cow: bool = False): | ||
""" | ||
Attempt self.values[indexer] = value, possibly creating a new array. | ||
|
||
|
@@ -1461,6 +1480,8 @@ def setitem(self, indexer, value): | |
The subset of self.values to set | ||
value : object | ||
The value being set | ||
using_cow: bool, default False | ||
Signaling if CoW is used. | ||
|
||
Returns | ||
------- | ||
|
@@ -1567,7 +1588,7 @@ def where(self, other, cond, _downcast: str | bool = "infer") -> list[Block]: | |
nb = self.make_block_same_class(res_values) | ||
return [nb] | ||
|
||
def putmask(self, mask, new) -> list[Block]: | ||
def putmask(self, mask, new, using_cow: bool = False) -> list[Block]: | ||
""" | ||
See Block.putmask.__doc__ | ||
""" | ||
|
@@ -1585,8 +1606,16 @@ def putmask(self, mask, new) -> list[Block]: | |
mask = self._maybe_squeeze_arg(mask) | ||
|
||
if not mask.any(): | ||
if using_cow: | ||
return [self.copy(deep=False)] | ||
return [self] | ||
|
||
if using_cow and self.refs.has_reference(): | ||
values = values.copy() | ||
self = self.make_block_same_class( # type: ignore[assignment] | ||
values.T if values.ndim == 2 else values | ||
) | ||
|
||
try: | ||
# Caller is responsible for ensuring matching lengths | ||
values._putmask(mask, new) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't comment on it, but a few lines above we have:
Is it theoretically possible that the dtype cannot store the value, but that the astype operation can still be done with a view? And should we in that case pass through
using_cow=using_cow
to the recursivesetitem
call?I can't directly think of a case (maybe an EA that is backed by object array but can't hold the object you are setting?), but maybe the safest option is to just pass it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm I was thinking about it this as well, but I could not think of a case where we would get a LossySetitemError and then would not make a copy in the astype. Can add it if you prefer anyways