bpo-34151: Improve performance of some list operations#8332
Conversation
…oc() instead of calloc().
| } | ||
|
|
||
| static inline PyObject * | ||
| list_new(Py_ssize_t size) { |
There was a problem hiding this comment.
I'd give this function a name that makes it clear that the memory will not be initialised. Traditionally, [builtin]_new is the name of the tp_new() slot function, which this isn't.
There was a problem hiding this comment.
something like: list_new_uninitialized()? Or maybe list_new_prealloc()?
There was a problem hiding this comment.
I prefer list_new_uninitialized over list_new_prealloc. prealloc makes me think the function is to allocate more memory than needed now for later usage but it is not.
| } | ||
|
|
||
| static inline PyObject * | ||
| list_new(Py_ssize_t size) { |
There was a problem hiding this comment.
something like: list_new_uninitialized()? Or maybe list_new_prealloc()?
| if (size == 0 || op == NULL) { | ||
| return (PyObject *) op; | ||
| } | ||
| op->ob_item = (PyObject **) PyMem_Malloc(size * sizeof(PyObject *)); |
There was a problem hiding this comment.
You should check if size*sizeof() overflow or not.
You might allocate ob_item before creating op, since I dislike calling Py_DECREF() on a just created object (is it fully initialized or not?).
There was a problem hiding this comment.
Especially, in this case deallocating the list will decref uninitialized list items...
There was a problem hiding this comment.
You might allocate ob_item before creating op, since I dislike calling Py_DECREF() on a just created object (is it fully initialized or not?).
Could you explain the difference?
There was a problem hiding this comment.
Especially, in this case deallocating the list will decref uninitialized list items
No, it won't. Either the PyMem_Malloc() fails, then ob_item is NULL and the list size is 0, which is consistent and can be cleaned up safely. Or, if PyMem_Malloc() succeeds, then ob_item points to the item array, which will be filled up right afterwards.
I think this code is ok as it is.
| Py_DECREF(op); | ||
| return PyErr_NoMemory(); | ||
| } | ||
| Py_SIZE(op) = size; |
There was a problem hiding this comment.
I'm not sure about this line. I would feel more confortable if it's done by the caller to always keep the list object consistent. For example, the caller could only set the size once the list is fully initialized. Otherwise, Py_DECREF(op) can crash on a half initialized list.
There was a problem hiding this comment.
I partly agree with this, but also note that any subsequent list array filling will necessarily incref the items along the way, so after the item array allocation and while the list isn't fully filled yet, it will necessarily be in an inconsistent and unsafe state that cannot simply be cleaned up via Py_DECREF(op). Thus the careful selection of list operations to which this is applied: only those that do not risk raising exceptions within this state transition.
Letting the caller do the size adjustment does not save us here. But I agree that it slightly reduces the chance of programmer errors. So, +1. And that means we should also have a comment at this function that states this contract.
There was a problem hiding this comment.
I have the same feeling here with @scoder . Putting the size setting on the caller's side doesn't save us. If there is any exception raising cases inside the content setting logic, it's still painful since you have to maintain the current size. And the new helper function doesn't do all metadata setting now. I have not better solution now but I'd suggest add this note to the comment of the new helper function no matter which way it is in.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
It doesn't save us, but it's easier to spot bugs. |
I am fine with either way because I have no better suggestion. Putting length setting inside also has its disvantages and can't solve the exception raising cases elegantly either. But please add a comment warning length is not set. And just as I've said, I prefer |
I proposed a name like "list_prealloc" which IMHO avoids the need of a comment. The name means that allocated is set, memory is allocated, but the size doesn't change. Call it "list_new_prealloc()" and maybe add an assertion to make sure that items is NULL to prevent misusage of the function, if you want. |
|
I prefer |
|
+1 for "uninitialised", because that's the most important information that users need. |
|
+1 to what @vstinner said and for |
zhangyangyu
left a comment
There was a problem hiding this comment.
LGTM, despite my preference is different.
|
@vstinner would you take another look? |
| if (size == 0 || op == NULL) { | ||
| return (PyObject *) op; | ||
| } | ||
| op->ob_item = PyMem_New(PyObject *, size); |
There was a problem hiding this comment.
Please add a comment why you don't use calloc() here, with a reference to the bpo number.
| return (PyObject *) op; | ||
| } | ||
|
|
||
| static PyObject * |
There was a problem hiding this comment.
Can you please add a short comment to explain when you consider that this function is more appropriate than PyList_New()? I understand that it should be preferred when the caller will always immediately fill all allocated list items
| if (op->ob_item == NULL) { | ||
| Py_DECREF(op); | ||
| return PyErr_NoMemory(); | ||
| } |
There was a problem hiding this comment.
From this point, it is possible that a GC collection will access the list. It would be safer to not call _PyObject_GC_TRACK(op) before the list is fully initialized. I suggest to modify list_new_prealloc() to avoid _PyObject_GC_TRACK(op), but call in the caller.
Maybe you can rewrite the code so PyList_New() calls list_new_prealloc(), and not the opposite?
There was a problem hiding this comment.
Could you elaborate on what do you mean exactly by not "fully initialized" here?
I think I could rewrite the whole patch using existing list_resize() function (I didn't it initially because unlike PyList_New() it preallocates more space than needed), since its usage doesn't raise such questions.
There was a problem hiding this comment.
list_dealloc() will crash if it's called on the list just after list_new_prealloc(), since items are uninitialized, whereas list_dealloc() calls Py_XDECREF() on each item. But the main issue is list_traverse(). If the garbage collection is triggered, list_traverse() will dereference uninitialized object pointers and so crash.
There was a problem hiding this comment.
list_traverse doesn’t touch preallocated area.
Unless Py_SIZE(list) is updated before filling objects, there are no dangers, I suppose.
|
A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated. Once you have made the requested changes, please leave a comment on this pull request containing the phrase |
vstinner
left a comment
There was a problem hiding this comment.
@serhiy-storchaka: What do you think of the GC+traverse issue?
| if (op->ob_item == NULL) { | ||
| Py_DECREF(op); | ||
| return PyErr_NoMemory(); | ||
| } |
There was a problem hiding this comment.
list_dealloc() will crash if it's called on the list just after list_new_prealloc(), since items are uninitialized, whereas list_dealloc() calls Py_XDECREF() on each item. But the main issue is list_traverse(). If the garbage collection is triggered, list_traverse() will dereference uninitialized object pointers and so crash.
vstinner
left a comment
There was a problem hiding this comment.
LGTM.
It seems like the GC+traverse issue is fine, so let's do this change.
https://bugs.python.org/issue34151