Skip to content

bpo-34151: Improve performance of some list operations#8332

Merged
zhangyangyu merged 7 commits into
python:masterfrom
sir-sigurd:list-malloc
Aug 11, 2018
Merged

bpo-34151: Improve performance of some list operations#8332
zhangyangyu merged 7 commits into
python:masterfrom
sir-sigurd:list-malloc

Conversation

@sir-sigurd

@sir-sigurd sir-sigurd commented Jul 18, 2018

Copy link
Copy Markdown
Contributor

Comment thread Objects/listobject.c Outdated
}

static inline PyObject *
list_new(Py_ssize_t size) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like: list_new_uninitialized()? Or maybe list_new_prealloc()?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Objects/listobject.c Outdated
}

static inline PyObject *
list_new(Py_ssize_t size) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like: list_new_uninitialized()? Or maybe list_new_prealloc()?

Comment thread Objects/listobject.c Outdated
if (size == 0 || op == NULL) {
return (PyObject *) op;
}
op->ob_item = (PyObject **) PyMem_Malloc(size * sizeof(PyObject *));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?).

@pitrou pitrou Jul 19, 2018

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially, in this case deallocating the list will decref uninitialized list items...

@sir-sigurd sir-sigurd Jul 19, 2018

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vstinner

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Objects/listobject.c Outdated
Py_DECREF(op);
return PyErr_NoMemory();
}
Py_SIZE(op) = size;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bedevere-bot

Copy link
Copy Markdown

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@vstinner

Copy link
Copy Markdown
Member

I have the same feeling here with @scoder . Putting the size setting on the caller's side doesn't save us.

It doesn't save us, but it's easier to spot bugs.

@zhangyangyu

zhangyangyu commented Jul 23, 2018

Copy link
Copy Markdown
Member

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 list_new_uninitialized.

@vstinner

Copy link
Copy Markdown
Member

But please add a comment warning length is not set. And I just as I've said, I prefer list_new_uninitialized.

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.

@pitrou

pitrou commented Jul 23, 2018

Copy link
Copy Markdown
Member

I prefer list_new_uninitialized. Just my 2 cents ;-)

@scoder

scoder commented Jul 23, 2018

Copy link
Copy Markdown
Contributor

+1 for "uninitialised", because that's the most important information that users need.

@sir-sigurd

sir-sigurd commented Jul 25, 2018

Copy link
Copy Markdown
Contributor Author

+1 to what @vstinner said and for list_new_prealloc.
uninitialized gives impression that list is inconsistent, because some/all fields are uninitialized i.e. have random values, but you can safely Py_DECREF() it or do any other operations like PyList_Append(), though it won't reallocate in this case.

@methane methane changed the title bpo-34151: Improved performance of some list operations by using malloc() instead of calloc(). bpo-34151: Improve performance of some list operations Jul 25, 2018

@methane methane left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add NEWS

@zhangyangyu zhangyangyu left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, despite my preference is different.

@methane

methane commented Jul 27, 2018

Copy link
Copy Markdown
Member

@vstinner would you take another look?

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm scared that _PyObject_GC_TRACK() is called too early.

cc @1st1 who had such super complex issue with GC in _asyncio.

Comment thread Objects/listobject.c
if (size == 0 || op == NULL) {
return (PyObject *) op;
}
op->ob_item = PyMem_New(PyObject *, size);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a comment why you don't use calloc() here, with a reference to the bpo number.

Comment thread Objects/listobject.c
return (PyObject *) op;
}

static PyObject *

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread Objects/listobject.c
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list_traverse doesn’t touch preallocated area.
Unless Py_SIZE(list) is updated before filling objects, there are no dangers, I suppose.

@bedevere-bot

Copy link
Copy Markdown

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 I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

@vstinner vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serhiy-storchaka: What do you think of the GC+traverse issue?

Comment thread Objects/listobject.c
if (op->ob_item == NULL) {
Py_DECREF(op);
return PyErr_NoMemory();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 vstinner left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

It seems like the GC+traverse issue is fine, so let's do this change.

@zhangyangyu
zhangyangyu merged commit 2fc4697 into python:master Aug 11, 2018
@zhangyangyu zhangyangyu added type-feature A feature request or enhancement performance Performance or resource usage labels Aug 11, 2018
@sir-sigurd
sir-sigurd deleted the list-malloc branch August 11, 2018 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance Performance or resource usage type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants