Eric Blake
2022-Jun-09 13:34 UTC
[Libguestfs] [libnbd PATCH v3 0/5] python: Speed up [aio_]pread_structured
Less copying is always better. But I was quite surprised by some of
my test cases in trying to prove that I had speedups; there's a huge
difference between:
for i in range(size // m):
buf = h.pread_structured(m, m*i, f)
and
for i in range(size // m):
buf = h.pread_structured(m, m*i, f)
buf = None
The former takes around 4.5s with libnbd 1.12.3, the latter around
15s, even though I was expecting the latter to be faster. But with
more thought, I think what is happening is that Python's compilation
and garbage collection implementation looks for opportunities where it
can prove that a pool of allocated memory can be easily reused
(assigning buf to hold a new bytearray that occupies the same size as
the previous bytearray that is now down to zero references) vs. where
it allocates new memory (the intermediate state of buf going to None
throwing away the intermediate knowledge about relations between its
other values). But with either python script, this patch makes a
noticeable difference by exposing the subbuf in [aio_]pread_structured
as a slice of the original python object, rather than yet another
memory copy.
Eric Blake (5):
python: Simplify passing of mutable *error to callbacks
python: Alter lock for persistent buffer
python: Accept all buffer-like objects in aio_p{read,write}
python: Support len(nbd.Buffer(n))
python: Slice structured read callback buffer from original
generator/Python.ml | 54 ++++++++---------
python/handle.c | 19 ++++--
python/t/405-pread-structured.py | 95 +++++++++---------------------
python/t/500-aio-pread.py | 21 +++++++
python/t/505-aio-pread-callback.py | 93 ++++++++---------------------
python/t/510-aio-pwrite.py | 17 ++++++
python/t/580-aio-is-zero.py | 2 +
python/utils.c | 53 +++++++++++++++++
8 files changed, 186 insertions(+), 168 deletions(-)
--
2.36.1
Eric Blake
2022-Jun-09 13:34 UTC
[Libguestfs] [libnbd PATCH v3 1/5] python: Simplify passing of mutable *error to callbacks
Instead of open-coding our code to create a PyObject wrapper of the
mutable *error to be passed to each callback, extract this into a
helper function. We can then slightly optimize things to hang on to a
single pointer to the ctypes module, rather than looking it up on
every callback. The generated code shrinks by more than the added
code in utils.c:
| --- python/methods.h.bak 2022-06-08 14:35:00.454786844 -0500
| +++ python/methods.h 2022-06-08 14:43:24.681596853 -0500
| @@ -35,6 +35,7 @@
| extern PyObject *nbd_internal_py_get_aio_view (PyObject *, bool);
| extern int nbd_internal_py_init_aio_buffer (PyObject *);
| extern PyObject *nbd_internal_py_get_nbd_buffer_type (void);
| +extern PyObject *nbd_internal_py_wrap_errptr (int);
|
| static inline struct nbd_handle *
| get_handle (PyObject *obj)
| --- python/methods.c.bak 2022-06-08 14:35:00.450786838 -0500
| +++ python/methods.c 2022-06-08 14:43:24.696596877 -0500
| @@ -75,13 +75,7 @@ chunk_wrapper (void *user_data, const vo
| PyObject *py_args, *py_ret;
| PyObject *py_error = NULL;
|
| - PyObject *py_error_modname = PyUnicode_FromString ("ctypes");
| - if (!py_error_modname) { PyErr_PrintEx (0); goto out; }
| - PyObject *py_error_mod = PyImport_Import (py_error_modname);
| - Py_DECREF (py_error_modname);
| - if (!py_error_mod) { PyErr_PrintEx (0); goto out; }
| - py_error = PyObject_CallMethod (py_error_mod, "c_int",
"i", *error);
| - Py_DECREF (py_error_mod);
| + py_error = nbd_internal_py_wrap_errptr (*error);
| if (!py_error) { PyErr_PrintEx (0); goto out; }
|
| py_args = Py_BuildValue ("(y#KIO)", subbuf, (int) count, offset,
status,
| @@ -132,13 +126,7 @@ completion_wrapper (void *user_data, int
| PyObject *py_args, *py_ret;
| PyObject *py_error = NULL;
|
| - PyObject *py_error_modname = PyUnicode_FromString ("ctypes");
| - if (!py_error_modname) { PyErr_PrintEx (0); goto out; }
| - PyObject *py_error_mod = PyImport_Import (py_error_modname);
| - Py_DECREF (py_error_modname);
| - if (!py_error_mod) { PyErr_PrintEx (0); goto out; }
| - py_error = PyObject_CallMethod (py_error_mod, "c_int",
"i", *error);
| - Py_DECREF (py_error_mod);
| + py_error = nbd_internal_py_wrap_errptr (*error);
| if (!py_error) { PyErr_PrintEx (0); goto out; }
|
| py_args = Py_BuildValue ("(O)", py_error);
| @@ -239,13 +227,7 @@ extent_wrapper (void *user_data, const c
| if (!py_e_entries) { PyErr_PrintEx (0); goto out; }
| PyList_SET_ITEM (py_entries, i_entries, py_e_entries);
| }
| - PyObject *py_error_modname = PyUnicode_FromString ("ctypes");
| - if (!py_error_modname) { PyErr_PrintEx (0); goto out; }
| - PyObject *py_error_mod = PyImport_Import (py_error_modname);
| - Py_DECREF (py_error_modname);
| - if (!py_error_mod) { PyErr_PrintEx (0); goto out; }
| - py_error = PyObject_CallMethod (py_error_mod, "c_int",
"i", *error);
| - Py_DECREF (py_error_mod);
| + py_error = nbd_internal_py_wrap_errptr (*error);
| if (!py_error) { PyErr_PrintEx (0); goto out; }
|
| py_args = Py_BuildValue ("(sKOO)", metacontext, offset,
py_entries,
---
generator/Python.ml | 9 ++-------
python/utils.c | 19 +++++++++++++++++++
2 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/generator/Python.ml b/generator/Python.ml
index e94f37b..6980034 100644
--- a/generator/Python.ml
+++ b/generator/Python.ml
@@ -41,6 +41,7 @@ let
extern PyObject *nbd_internal_py_get_aio_view (PyObject *, bool);
extern int nbd_internal_py_init_aio_buffer (PyObject *);
extern PyObject *nbd_internal_py_get_nbd_buffer_type (void);
+extern PyObject *nbd_internal_py_wrap_errptr (int);
static inline struct nbd_handle *
get_handle (PyObject *obj)
@@ -184,13 +185,7 @@ let
| CBInt _
| CBInt64 _ -> ()
| CBMutable (Int n) ->
- pr " PyObject *py_%s_modname = PyUnicode_FromString
(\"ctypes\");\n" n;
- pr " if (!py_%s_modname) { PyErr_PrintEx (0); goto out; }\n"
n;
- pr " PyObject *py_%s_mod = PyImport_Import
(py_%s_modname);\n" n n;
- pr " Py_DECREF (py_%s_modname);\n" n;
- pr " if (!py_%s_mod) { PyErr_PrintEx (0); goto out; }\n" n;
- pr " py_%s = PyObject_CallMethod (py_%s_mod, \"c_int\",
\"i\", *%s);\n" n n n;
- pr " Py_DECREF (py_%s_mod);\n" n;
+ pr " py_%s = nbd_internal_py_wrap_errptr (*%s);\n" n n;
pr " if (!py_%s) { PyErr_PrintEx (0); goto out; }\n" n;
| CBString _
| CBUInt _
diff --git a/python/utils.c b/python/utils.c
index e0df181..cd44b90 100644
--- a/python/utils.c
+++ b/python/utils.c
@@ -173,3 +173,22 @@ nbd_internal_py_get_nbd_buffer_type (void)
}
return type;
}
+
+/* Helper to package callback *error into modifiable PyObject */
+PyObject *
+nbd_internal_py_wrap_errptr (int err)
+{
+ static PyObject *py_ctypes_mod;
+
+ if (!py_ctypes_mod) {
+ PyObject *py_modname = PyUnicode_FromString ("ctypes");
+ if (!py_modname)
+ return NULL;
+ py_ctypes_mod = PyImport_Import (py_modname);
+ Py_DECREF (py_modname);
+ if (!py_ctypes_mod)
+ return NULL;
+ }
+
+ return PyObject_CallMethod (py_ctypes_mod, "c_int", "i",
err);
+}
--
2.36.1
Eric Blake
2022-Jun-09 13:34 UTC
[Libguestfs] [libnbd PATCH v3 2/5] python: Alter lock for persistent buffer
As long as we were wrapping a C buffer and not leaking any Python
objects, we had to lock the nbd.Buffer object, to ensure that the
PyCapsule was not released prematurely, as there was nothing else to
hold on to. But now that the previous commit (4f15d0e9) swapped to
wrapping a Python bytearray object, we are better off locking the
PyMemoryView created from that object, for two reasons. First, if we
are going to allow aio_{pread,pwrite} to take an arbitrary buffer-like
object instead of requiring an nbd.Buffer object, then that's all we
have available to lock. Second, hanging on to a PyMemoryView provides
some nice guarantees that the underlying python buffer-like object
won't be reallocated behind our backs. The following snippets from
nbdsh use bad style (you should never access a ._* member outside of
its class), but demonstrate the same issue we would be faced with once
nbd.Buffer starts sharing rather than copying in the public
.{to,from}_bytearray:
Pre-patch:
nbd> b = nbd.Buffer(10)
nbd> c = h.aio_pread(b, 0)
nbd> b._o.pop()
0
nbd> b.size()
9
nbd> h.poll(-1)
Oops - we changed the size of the underlying bytearray prior to
receiving the server's reply. If that caused python to reallocate the
buffer underlying the bytearray, we've just clobbered random memory.
Post-patch:
nbd> b = nbd.Buffer(10)
nbd> c = h.aio_pread(b, 0)
nbd> b._o.pop()
Traceback (most recent call last):
File "/usr/lib64/python3.10/code.py", line 90, in runcode
exec(code, self.locals)
File "<console>", line 1, in <module>
BufferError: Existing exports of data: object cannot be re-sized
nbd> h.poll(-1)
nbd> h.aio_command_completed(c)
True
nbd> b._o.pop()
0
nbd> b.size()
9
There - now we are sure that as long as our read is pending, the
underlying bytearray cannot be reallocated; but as soon as it
completes, we have regained full use of the bytearray.
---
generator/Python.ml | 15 +++++----------
1 file changed, 5 insertions(+), 10 deletions(-)
diff --git a/generator/Python.ml b/generator/Python.ml
index 6980034..20d7e78 100644
--- a/generator/Python.ml
+++ b/generator/Python.ml
@@ -424,16 +424,12 @@ let
pr " %s_view = nbd_internal_py_get_aio_view (%s, true);\n" n
n;
pr " if (!%s_view) goto out;\n" n;
pr " py_%s = PyMemoryView_GET_BUFFER (%s_view);\n" n n;
- pr " /* Increment refcount since buffer may be saved by libnbd.
*/\n";
- pr " Py_INCREF (%s);\n" n;
- pr " completion_user_data->buf = %s;\n" n
+ pr " completion_user_data->view = %s_view;\n" n
| BytesPersistOut (n, _) ->
pr " %s_view = nbd_internal_py_get_aio_view (%s, false);\n" n
n;
pr " if (!%s_view) goto out;\n" n;
pr " py_%s = PyMemoryView_GET_BUFFER (%s_view);\n" n n;
- pr " /* Increment refcount since buffer may be saved by libnbd.
*/\n";
- pr " Py_INCREF (%s);\n" n;
- pr " completion_user_data->buf = %s;\n" n
+ pr " completion_user_data->view = %s_view;\n" n
| Closure { cbname } ->
pr " %s.user_data = %s_user_data = alloc_user_data ();\n"
cbname cbname;
pr " if (%s_user_data == NULL) goto out;\n" cbname;
@@ -538,8 +534,7 @@ let
pr " if (%s.obj)\n" n;
pr " PyBuffer_Release (&%s);\n" n
| BytesOut (n, _) -> pr " Py_XDECREF (%s);\n" n
- | BytesPersistIn (n, _) | BytesPersistOut (n, _) ->
- pr " Py_XDECREF (%s_view);\n" n
+ | BytesPersistIn (n, _) | BytesPersistOut (n, _) -> ()
| Closure { cbname } ->
pr " free_user_data (%s_user_data);\n" cbname
| Enum _ -> ()
@@ -588,7 +583,7 @@ let
pr " */\n";
pr "struct user_data {\n";
pr " PyObject *fn; /* Optional pointer to Python function.
*/\n";
- pr " PyObject *buf; /* Optional pointer to persistent buffer.
*/\n";
+ pr " PyObject *view; /* Optional PyMemoryView of persistent buffer.
*/\n";
pr "};\n";
pr "\n";
pr "static struct user_data *\n";
@@ -609,7 +604,7 @@ let
pr "\n";
pr " if (data) {\n";
pr " Py_XDECREF (data->fn);\n";
- pr " Py_XDECREF (data->buf);\n";
+ pr " Py_XDECREF (data->view);\n";
pr " free (data);\n";
pr " }\n";
pr "}\n";
--
2.36.1
Eric Blake
2022-Jun-09 13:34 UTC
[Libguestfs] [libnbd PATCH v3 3/5] python: Accept all buffer-like objects in aio_p{read, write}
After the work in the previous patches, it is now a trivial feature
addition to support any buffer-like object as the argument to
h.aio_{pread,pwrite}, and matching how h.pwrite already did that. For
example, you can do h.aio_pwrite(b'123', 0), instead of having to copy
into nbd.Buffer first. More importantly, whereas
nbd.Buffer.to_bytearray() currently copies out and
nbd.Buffer.from_bytearray(buffer) copies in, using native types means
you can reduce that copy overhead.
For a demonstration of the speedups possible, compare two scripts that
utilize a new buffer for every I/O (important if you are going to do
parallel operations - even though this demo is serialized), one using
nbd.Buffer, the other using native python types, with timing on my
machine:
$ export script1='
m=1024*1024
size=h.get_size()
for i in range(size // m):
b1 = nbd.Buffer(m)
c = h.aio_pread(b1, m*i)
while not h.aio_command_completed(c):
h.poll(-1)
b2 = nbd.Buffer.from_bytearray(b1.to_bytearray())
c = h.aio_pwrite(b2, m*i)
while not h.aio_command_completed(c):
h.poll(-1)
'
$ export script2='
m=1024*1024
size=h.get_size()
for i in range(size // m):
b1 = bytearray(m)
c = h.aio_pread(b1, m*i)
while not h.aio_command_completed(c):
h.poll(-1)
b2 = bytes(b1)
c = h.aio_pwrite(b2, m*i)
while not h.aio_command_completed(c):
h.poll(-1)
'
$ nbdkit -U - --filter=checkwrite pattern 10G \
--run 'nbdsh -u "$uri" -c
"h.set_pread_initialize(True)" -c "script1"'
takes ~31.5s (this setting for pread_initialize is default)
$ nbdkit -U - --filter=checkwrite pattern 10G \
--run 'nbdsh -u "$uri" -c
"h.set_pread_initialize(False)" -c "script1"'
takes ~30.8s (not re-zeroing the buffer during aio_pread helped)
$ nbdkit -U - --filter=checkwrite pattern 10G \
--run 'nbdsh -u "$uri" -c
"h.set_pread_initialize(True)" -c "script2"'
takes ~23.0s (less copying when going from b1 to b2 helped even more)
$ nbdkit -U - --filter=checkwrite pattern 10G \
--run 'nbdsh -u "$uri" -c
"h.set_pread_initialize(False)" -c "script2"'
takes ~22.8s (again, aio_pread is faster when not re-zeroing)
Of course, you can get better speed still by reusing the same buffer
for every operation, at which point the difference is in the noise;
but this approach is no longer parallelizable as written (you don't
want the same buffer in use by more than one aio operation at a time):
$ export script3='
m=1024*1024
size=h.get_size()
h.set_pread_initialize(False)
buf=nbd.Buffer(m) # or bytearray(m)
for i in range(size // m):
c = h.aio_pread(buf, m*i)
while not h.aio_command_completed(c):
h.poll(-1)
c = h.aio_pwrite(buf, m*i)
while not h.aio_command_completed(c):
h.poll(-1)
'
$ nbdkit -U - --filter=checkwrite pattern 10G \
--run 'nbdsh -u "$uri" -c "script3"'
takes about ~15.2s, regardless of whether I used nbd.Buffer(m) or
bytearray(m) when setting up buf.
---
generator/Python.ml | 8 ++++----
python/handle.c | 19 +++++++++++++------
python/t/500-aio-pread.py | 21 +++++++++++++++++++++
python/t/510-aio-pwrite.py | 17 +++++++++++++++++
4 files changed, 55 insertions(+), 10 deletions(-)
diff --git a/generator/Python.ml b/generator/Python.ml
index 20d7e78..cd7f0e3 100644
--- a/generator/Python.ml
+++ b/generator/Python.ml
@@ -38,7 +38,7 @@ let
extern void nbd_internal_py_free_string_list (char **);
extern int nbd_internal_py_get_sockaddr (PyObject *,
struct sockaddr_storage *, socklen_t *);
-extern PyObject *nbd_internal_py_get_aio_view (PyObject *, bool);
+extern PyObject *nbd_internal_py_get_aio_view (PyObject *, int);
extern int nbd_internal_py_init_aio_buffer (PyObject *);
extern PyObject *nbd_internal_py_get_nbd_buffer_type (void);
extern PyObject *nbd_internal_py_wrap_errptr (int);
@@ -288,7 +288,7 @@ let
pr " Py_ssize_t %s;\n" count
| BytesPersistIn (n, _)
| BytesPersistOut (n, _) ->
- pr " PyObject *%s; /* Instance of nbd.Buffer */\n" n;
+ pr " PyObject *%s; /* Buffer-like object or nbd.Buffer */\n"
n;
pr " PyObject *%s_view = NULL; /* PyMemoryView of %s */\n" n
n;
pr " Py_buffer *py_%s; /* buffer of %s_view */\n" n n
| Closure { cbname } ->
@@ -421,12 +421,12 @@ let
pr " %s = PyByteArray_FromStringAndSize (NULL, %s);\n" n
count;
pr " if (%s == NULL) goto out;\n" n
| BytesPersistIn (n, _) ->
- pr " %s_view = nbd_internal_py_get_aio_view (%s, true);\n" n
n;
+ pr " %s_view = nbd_internal_py_get_aio_view (%s,
PyBUF_READ);\n" n n;
pr " if (!%s_view) goto out;\n" n;
pr " py_%s = PyMemoryView_GET_BUFFER (%s_view);\n" n n;
pr " completion_user_data->view = %s_view;\n" n
| BytesPersistOut (n, _) ->
- pr " %s_view = nbd_internal_py_get_aio_view (%s, false);\n" n
n;
+ pr " %s_view = nbd_internal_py_get_aio_view (%s,
PyBUF_WRITE);\n" n n;
pr " if (!%s_view) goto out;\n" n;
pr " py_%s = PyMemoryView_GET_BUFFER (%s_view);\n" n n;
pr " completion_user_data->view = %s_view;\n" n
diff --git a/python/handle.c b/python/handle.c
index ca6c8e9..f72d200 100644
--- a/python/handle.c
+++ b/python/handle.c
@@ -96,16 +96,23 @@ nbd_internal_py_display_version (PyObject *self, PyObject
*args)
return Py_None;
}
-/* Return new reference to MemoryView wrapping aio_buffer contents */
+/* Return new reference to MemoryView wrapping buffer or aio_buffer contents.
+ * buffertype is PyBUF_READ or PyBUF_WRITE, for how the buffer will be used
+ * (remember, aio_pwrite READS the buffer, aio_pread WRITES into the buffer).
+ */
PyObject *
-nbd_internal_py_get_aio_view (PyObject *object, bool require_init)
+nbd_internal_py_get_aio_view (PyObject *object, int buffertype)
{
PyObject *buffer = NULL;
- if (PyObject_IsInstance (object, nbd_internal_py_get_nbd_buffer_type ())) {
+ if (PyObject_CheckBuffer (object))
+ buffer = object;
+ else if (PyObject_IsInstance (object,
+ nbd_internal_py_get_nbd_buffer_type ())) {
buffer = PyObject_GetAttrString (object, "_o");
- if (require_init && ! PyObject_HasAttrString (object,
"_init")) {
+ if (buffertype == PyBUF_READ &&
+ ! PyObject_HasAttrString (object, "_init")) {
assert (PyByteArray_Check (buffer));
memset (PyByteArray_AS_STRING (buffer), 0,
PyByteArray_GET_SIZE (buffer));
@@ -115,10 +122,10 @@ nbd_internal_py_get_aio_view (PyObject *object, bool
require_init)
}
if (buffer)
- return PyMemoryView_FromObject (buffer);
+ return PyMemoryView_GetContiguous (buffer, buffertype, 'A');
PyErr_SetString (PyExc_TypeError,
- "aio_buffer: expecting nbd.Buffer instance");
+ "aio_buffer: expecting buffer or nbd.Buffer
instance");
return NULL;
}
diff --git a/python/t/500-aio-pread.py b/python/t/500-aio-pread.py
index 781b709..6851928 100644
--- a/python/t/500-aio-pread.py
+++ b/python/t/500-aio-pread.py
@@ -45,3 +45,24 @@ if sys.byteorder == 'little':
arr.byteswap()
assert buf1 == memoryview(arr).cast('B')[:512]
assert buf2 == memoryview(arr).cast('B')[512:]
+
+# It is also possible to read directly into any writeable buffer-like object.
+# However, aio.Buffer(n) with h.set_pread_initialize(False) may be faster,
+# because it skips python's pre-initialization of bytearray(n).
+try:
+ h.aio_pread(bytes(512), 0)
+ assert False
+except BufferError:
+ pass
+buf3 = bytearray(512)
+cookie = h.aio_pread(buf3, 0)
+# While the read is pending, the buffer cannot be resized
+try:
+ buf3.pop()
+ assert False
+except BufferError:
+ pass
+while not h.aio_command_completed(cookie):
+ h.poll(-1)
+buf3.append(buf3.pop())
+assert buf3 == buf1
diff --git a/python/t/510-aio-pwrite.py b/python/t/510-aio-pwrite.py
index d09e249..4b4aac8 100644
--- a/python/t/510-aio-pwrite.py
+++ b/python/t/510-aio-pwrite.py
@@ -67,4 +67,21 @@ with open(datafile, "rb") as f:
assert nbd.Buffer.from_bytearray(content).is_zero()
+# It is also possible to write any buffer-like object.
+# While the write is pending, the buffer cannot be resized
+cookie = h.aio_pwrite(buf, 0, flags=nbd.CMD_FLAG_FUA)
+try:
+ buf.pop()
+ assert False
+except BufferError:
+ pass
+while not h.aio_command_completed(cookie):
+ h.poll(-1)
+buf.append(buf.pop())
+
+with open(datafile, "rb") as f:
+ content = f.read()
+
+assert buf == content
+
os.unlink(datafile)
--
2.36.1
Eric Blake
2022-Jun-09 13:34 UTC
[Libguestfs] [libnbd PATCH v3 4/5] python: Support len(nbd.Buffer(n))
b.size() is atypical, Python programmers generally expect to be able
to do len(b). Keep the old spelling for backwards compatibility.
---
generator/Python.ml | 4 ++++
python/t/580-aio-is-zero.py | 2 ++
2 files changed, 6 insertions(+)
diff --git a/generator/Python.ml b/generator/Python.ml
index cd7f0e3..975cab4 100644
--- a/generator/Python.ml
+++ b/generator/Python.ml
@@ -745,6 +745,10 @@ let
return bytearray(self._o)
def size(self):
+ '''Return the size of an AIO buffer.'''
+ return len(self)
+
+ def __len__(self):
'''Return the size of an AIO buffer.'''
return len(self._o)
diff --git a/python/t/580-aio-is-zero.py b/python/t/580-aio-is-zero.py
index 1a9a7d3..dd57e30 100644
--- a/python/t/580-aio-is-zero.py
+++ b/python/t/580-aio-is-zero.py
@@ -24,6 +24,7 @@ import nbd
ba = bytearray(2**20)
buf = nbd.Buffer.from_bytearray(ba)
assert buf.size() == 2**20
+assert len(buf) == 2**20
assert buf.is_zero()
# The above buffer is 2**20 (= 1MB), slices of it should also be zero.
@@ -74,5 +75,6 @@ assert buf.is_zero(2**21-1, 1)
# used with aio_pread; but it will be zeroed if accessed prematurely
buf = nbd.Buffer(1024)
assert buf.size() == 1024
+assert len(buf) == 1024
assert buf.is_zero()
assert nbd.Buffer.from_bytearray(buf.to_bytearray()).is_zero()
--
2.36.1
Eric Blake
2022-Jun-09 13:34 UTC
[Libguestfs] [libnbd PATCH v3 5/5] python: Slice structured read callback buffer from original
The Py_BuildValue "y#" format copies data; this is because Python
assumes our C memory can go out of scope, while the user's python
callback can stash off whatever bytearray object it got. But this
copying is inefficient. Now that we already have a Python buffer-like
object in scope for the duration of all structured reads
(pread_structured since commit f8c76c01, aio_pread_structured since
commit 4f15d0e9), we know that the C memory parameter to our
chunk_callback is already (a portion of) the underlying contiguous
memory of an original Python object. If we pass the Python callback a
memoryview object over the correct slice of that memory, then Python
will correctly handle the intricacies of what happens if the callback
saves our memoryview object for later use, all without any data
copying overhead, making use of the pread_structured form much faster.
We basically want to code up the C equivalent to the python expression
'view = memoryview(buf).toreadonly()[start:end]', where start and end
are determined by comparing subbuf/count against the original buf.
But in C, that involves creating PyObject integers to form a PySlice,
then using PyObject_GetItem to apply the slice to a PyMemoryView (I
don't know of any faster tricks; googling does not find any quick
hits). Extract this work into a new utils.c helper function, to avoid
repetition (and in case we figure out a way to do it in fewer lines by
directly manipulating Py_buffer objects).
The testsuite is updated to cover examples of the data persistence
aspect, demonstrating that our stashed slice is still tied to the
returned Python buffer, even though the callback is long since
completed (prior to this patch, stash was an independent bytes copy,
and while it persisted on after the callback, it is frozen in time to
the contents of the buffer at the time the callback was reached). The
fact that data is now shared is is an API change, but one unlikely to
cause problems - it is very unusual for a callback function to try and
stash sub-buffers for later use (neither libnbd nor nbdkit was
utilizing that in their testsuites before this patch).
To demonstrate the speedup now that there is less copying involved per
callback, I tested:
$ export script='
def f(b,o,s,e):
pass
m=1024*1024
size=h.get_size()
h.set_pread_initialize(False)
for i in range(size // m):
buf = h.pread_structured(m, m*i, f)
buf = None
'
$ time ./run nbdkit -U - memory 10G --run 'nbdsh -u "$uri" -c
"$script"'
On my machine, this took 16.9s with libnbd 1.12.3, 15.5s pre-patch,
and 3.6s post-patch, for more than 4x speedup in the best case. Much
of the differences boil down to how efficient the Python garbage
collector is able to spot liveness of various buffers, to reuse rather
than allocate new buffers on the Python side; removing the 'buf None'
line speeds up libnbd 1.12.3 to 4.5s (better reuse of the "y#"
memory in the callback when backed by C memory), 9.0s pre-patch
(copying "y#" from Python memory has scanning effects), and 3.0s
post-patch (zero-copying is always best).
A variation with aio_pread_structured reusing the same nbd.Buffer is
not quite as drastic: 3.9s with libnbd 1.12.3, 4.0s pre-patch, and
3.4s with this patch, but still shows that it is beneficial.
The corresponding diff to generated code is:
| --- python/methods.h.bak 2022-06-09 07:23:51.657260039 -0500
| +++ python/methods.h 2022-06-09 07:23:57.629266878 -0500
| @@ -36,6 +36,7 @@
| extern int nbd_internal_py_init_aio_buffer (PyObject *);
| extern PyObject *nbd_internal_py_get_nbd_buffer_type (void);
| extern PyObject *nbd_internal_py_wrap_errptr (int);
| +extern PyObject *nbd_internal_py_get_subview (PyObject *, const char *,
size_t);
|
| static inline struct nbd_handle *
| get_handle (PyObject *obj)
| --- python/methods.c.bak 2022-06-09 07:23:51.652260034 -0500
| +++ python/methods.c 2022-06-09 07:23:57.644266895 -0500
| @@ -73,13 +73,15 @@ chunk_wrapper (void *user_data, const vo
|
| PyGILState_STATE py_save = PyGILState_UNLOCKED;
| PyObject *py_args, *py_ret;
| + PyObject *py_subbuf = NULL;
| PyObject *py_error = NULL;
|
| + py_subbuf = nbd_internal_py_get_subview (data->view, subbuf, count);
| + if (!py_subbuf) { PyErr_PrintEx (0); goto out; }
| py_error = nbd_internal_py_wrap_errptr (*error);
| if (!py_error) { PyErr_PrintEx (0); goto out; }
|
| - py_args = Py_BuildValue ("(y#KIO)", subbuf, (int) count, offset,
status,
| - py_error);
| + py_args = Py_BuildValue ("(OKIO)", py_subbuf, offset, status,
py_error);
| if (!py_args) { PyErr_PrintEx (0); goto out; }
|
| py_save = PyGILState_Ensure ();
| @@ -106,6 +108,7 @@ chunk_wrapper (void *user_data, const vo
| };
|
| out:
| + Py_XDECREF (py_subbuf);
| if (py_error) {
| PyObject *py_error_ret = PyObject_GetAttrString (py_error,
"value");
| *error = PyLong_AsLong (py_error_ret);
| @@ -2231,6 +2234,7 @@ nbd_internal_py_pread_structured (PyObje
| /* Increment refcount since pointer may be saved by libnbd. */
| Py_INCREF (py_chunk_fn);
| chunk_user_data->fn = py_chunk_fn;
| + chunk_user_data->view = nbd_internal_py_get_aio_view (buf, PyBUF_WRITE);
|
| ret = nbd_pread_structured (h, PyByteArray_AS_STRING (buf), count,
| offset_u64, chunk, flags_u32);
| @@ -3116,6 +3120,7 @@ nbd_internal_py_aio_pread_structured (Py
| /* Increment refcount since pointer may be saved by libnbd. */
| Py_INCREF (py_chunk_fn);
| chunk_user_data->fn = py_chunk_fn;
| + chunk_user_data->view = nbd_internal_py_get_aio_view (buf, PyBUF_WRITE);
|
| if (nbd_internal_py_init_aio_buffer (buf) < 0) goto out;
| ret = nbd_aio_pread_structured (h, py_buf->buf, py_buf->len,
offset_u64,
---
generator/Python.ml | 18 ++++--
python/t/405-pread-structured.py | 95 +++++++++---------------------
python/t/505-aio-pread-callback.py | 93 ++++++++---------------------
python/utils.c | 34 +++++++++++
4 files changed, 99 insertions(+), 141 deletions(-)
diff --git a/generator/Python.ml b/generator/Python.ml
index 975cab4..c424239 100644
--- a/generator/Python.ml
+++ b/generator/Python.ml
@@ -42,6 +42,7 @@ let
extern int nbd_internal_py_init_aio_buffer (PyObject *);
extern PyObject *nbd_internal_py_get_nbd_buffer_type (void);
extern PyObject *nbd_internal_py_wrap_errptr (int);
+extern PyObject *nbd_internal_py_get_subview (PyObject *, const char *,
size_t);
static inline struct nbd_handle *
get_handle (PyObject *obj)
@@ -163,8 +164,8 @@ let
pr " PyObject *py_args, *py_ret;\n";
List.iter (
function
- | CBArrayAndLen (UInt32 n, _) ->
- pr " PyObject *py_%s = NULL;\n" n
+ | CBArrayAndLen (UInt32 n, _)
+ | CBBytesIn (n, _)
| CBMutable (Int n) ->
pr " PyObject *py_%s = NULL;\n" n
| _ -> ()
@@ -181,7 +182,9 @@ let
pr " if (!py_e_%s) { PyErr_PrintEx (0); goto out; }\n" n;
pr " PyList_SET_ITEM (py_%s, i_%s, py_e_%s);\n" n n n;
pr " }\n"
- | CBBytesIn _
+ | CBBytesIn (n, len) ->
+ pr " py_%s = nbd_internal_py_get_subview (data->view, %s,
%s);\n" n n len;
+ pr " if (!py_%s) { PyErr_PrintEx (0); goto out; }\n" n
| CBInt _
| CBInt64 _ -> ()
| CBMutable (Int n) ->
@@ -198,7 +201,7 @@ let
List.map (
function
| CBArrayAndLen (UInt32 n, _) -> "O", sprintf
"py_%s" n
- | CBBytesIn (n, len) -> "y#", sprintf "%s, (int)
%s" n len
+ | CBBytesIn (n, _) -> "O", sprintf "py_%s" n
| CBInt n -> "i", n
| CBInt64 n -> "L", n
| CBMutable (Int n) -> "O", sprintf "py_%s" n
@@ -250,6 +253,8 @@ let
function
| CBArrayAndLen (UInt32 n, _) ->
pr " Py_XDECREF (py_%s);\n" n
+ | CBBytesIn (n, _) ->
+ pr " Py_XDECREF (py_%s);\n" n
| CBMutable (Int n) ->
pr " if (py_%s) {\n" n;
pr " PyObject *py_%s_ret = PyObject_GetAttrString (py_%s,
\"value\");\n" n n;
@@ -257,7 +262,6 @@ let
pr " Py_DECREF (py_%s_ret);\n" n;
pr " Py_DECREF (py_%s);\n" n;
pr " }\n"
- | CBBytesIn _
| CBInt _ | CBInt64 _
| CBString _
| CBUInt _ | CBUInt64 _ -> ()
@@ -440,7 +444,9 @@ let
pr " }\n";
pr " /* Increment refcount since pointer may be saved by libnbd.
*/\n";
pr " Py_INCREF (py_%s_fn);\n" cbname;
- pr " %s_user_data->fn = py_%s_fn;\n" cbname cbname
+ pr " %s_user_data->fn = py_%s_fn;\n" cbname cbname;
+ if cbname = "chunk" then
+ pr " chunk_user_data->view = nbd_internal_py_get_aio_view
(buf, PyBUF_WRITE);\n"
| Enum _ -> ()
| Flags (n, _) -> pr " %s_u32 = %s;\n" n n
| Fd _ | Int _ -> ()
diff --git a/python/t/405-pread-structured.py b/python/t/405-pread-structured.py
index afd1e7e..9d3df68 100644
--- a/python/t/405-pread-structured.py
+++ b/python/t/405-pread-structured.py
@@ -1,5 +1,5 @@
# libnbd Python bindings
-# Copyright (C) 2010-2019 Red Hat Inc.
+# Copyright (C) 2010-2022 Red Hat Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,76 +17,19 @@
import nbd
import errno
+import sys
+from array import array
h = nbd.NBD()
h.connect_command(["nbdkit", "-s",
"--exit-with-parent", "-v",
"pattern", "size=512"])
-expected = (b'\x00\x00\x00\x00\x00\x00\x00\x00'
- + b'\x00\x00\x00\x00\x00\x00\x00\x08'
- + b'\x00\x00\x00\x00\x00\x00\x00\x10'
- + b'\x00\x00\x00\x00\x00\x00\x00\x18'
- + b'\x00\x00\x00\x00\x00\x00\x00 '
- + b'\x00\x00\x00\x00\x00\x00\x00('
- + b'\x00\x00\x00\x00\x00\x00\x000'
- + b'\x00\x00\x00\x00\x00\x00\x008'
- + b'\x00\x00\x00\x00\x00\x00\x00@'
- + b'\x00\x00\x00\x00\x00\x00\x00H'
- + b'\x00\x00\x00\x00\x00\x00\x00P'
- + b'\x00\x00\x00\x00\x00\x00\x00X'
- + b'\x00\x00\x00\x00\x00\x00\x00`'
- + b'\x00\x00\x00\x00\x00\x00\x00h'
- + b'\x00\x00\x00\x00\x00\x00\x00p'
- + b'\x00\x00\x00\x00\x00\x00\x00x'
- + b'\x00\x00\x00\x00\x00\x00\x00\x80'
- + b'\x00\x00\x00\x00\x00\x00\x00\x88'
- + b'\x00\x00\x00\x00\x00\x00\x00\x90'
- + b'\x00\x00\x00\x00\x00\x00\x00\x98'
- + b'\x00\x00\x00\x00\x00\x00\x00\xa0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xa8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xb0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xb8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xc0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xc8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xd0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xd8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xe0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xe8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xf0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xf8'
- + b'\x00\x00\x00\x00\x00\x00\x01\x00'
- + b'\x00\x00\x00\x00\x00\x00\x01\x08'
- + b'\x00\x00\x00\x00\x00\x00\x01\x10'
- + b'\x00\x00\x00\x00\x00\x00\x01\x18'
- + b'\x00\x00\x00\x00\x00\x00\x01 '
- + b'\x00\x00\x00\x00\x00\x00\x01('
- + b'\x00\x00\x00\x00\x00\x00\x010'
- + b'\x00\x00\x00\x00\x00\x00\x018'
- + b'\x00\x00\x00\x00\x00\x00\x01@'
- + b'\x00\x00\x00\x00\x00\x00\x01H'
- + b'\x00\x00\x00\x00\x00\x00\x01P'
- + b'\x00\x00\x00\x00\x00\x00\x01X'
- + b'\x00\x00\x00\x00\x00\x00\x01`'
- + b'\x00\x00\x00\x00\x00\x00\x01h'
- + b'\x00\x00\x00\x00\x00\x00\x01p'
- + b'\x00\x00\x00\x00\x00\x00\x01x'
- + b'\x00\x00\x00\x00\x00\x00\x01\x80'
- + b'\x00\x00\x00\x00\x00\x00\x01\x88'
- + b'\x00\x00\x00\x00\x00\x00\x01\x90'
- + b'\x00\x00\x00\x00\x00\x00\x01\x98'
- + b'\x00\x00\x00\x00\x00\x00\x01\xa0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xa8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xb0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xb8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xc0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xc8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xd0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xd8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xe0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xe8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xf0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xf8')
-
+# The nbdkit pattern plugin exposes 64-bit numbers in bigendian order
+arr = array('q', range(0, 512, 8))
+if sys.byteorder == 'little':
+ arr.byteswap()
+expected = memoryview(arr).cast('B')
+stash = None
def f(user_data, buf2, offset, s, err):
assert err.value == 0
@@ -94,8 +37,15 @@ def f(user_data, buf2, offset, s, err):
if user_data != 42:
raise ValueError('unexpected user_data')
assert buf2 == expected
+ try:
+ buf2[0] = 1
+ assert False
+ except TypeError:
+ pass
assert offset == 0
assert s == nbd.READ_DATA
+ global stash
+ stash = buf2
buf = h.pread_structured(512, 0, lambda *args: f(42, *args))
@@ -104,6 +54,19 @@ print("%r" % buf)
assert buf == expected
+# The callback can stash its slice; as long as that is live, we can't
+# resize buf but can view changes in buf through the slice
+try:
+ buf.pop()
+ assert False
+except BufferError:
+ pass
+buf[0] ^= 1
+assert buf == stash
+stash = None
+buf.pop()
+
+# Tests of error handling
buf = h.pread_structured(512, 0, lambda *args: f(42, *args),
nbd.CMD_FLAG_DF)
diff --git a/python/t/505-aio-pread-callback.py
b/python/t/505-aio-pread-callback.py
index 1773aee..5c7b250 100644
--- a/python/t/505-aio-pread-callback.py
+++ b/python/t/505-aio-pread-callback.py
@@ -1,5 +1,5 @@
# libnbd Python bindings
-# Copyright (C) 2010-2019 Red Hat Inc.
+# Copyright (C) 2010-2022 Red Hat Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -17,76 +17,19 @@
import nbd
import errno
+import sys
+from array import array
h = nbd.NBD()
h.connect_command(["nbdkit", "-s",
"--exit-with-parent", "-v",
"pattern", "size=512"])
-expected = (b'\x00\x00\x00\x00\x00\x00\x00\x00'
- + b'\x00\x00\x00\x00\x00\x00\x00\x08'
- + b'\x00\x00\x00\x00\x00\x00\x00\x10'
- + b'\x00\x00\x00\x00\x00\x00\x00\x18'
- + b'\x00\x00\x00\x00\x00\x00\x00 '
- + b'\x00\x00\x00\x00\x00\x00\x00('
- + b'\x00\x00\x00\x00\x00\x00\x000'
- + b'\x00\x00\x00\x00\x00\x00\x008'
- + b'\x00\x00\x00\x00\x00\x00\x00@'
- + b'\x00\x00\x00\x00\x00\x00\x00H'
- + b'\x00\x00\x00\x00\x00\x00\x00P'
- + b'\x00\x00\x00\x00\x00\x00\x00X'
- + b'\x00\x00\x00\x00\x00\x00\x00`'
- + b'\x00\x00\x00\x00\x00\x00\x00h'
- + b'\x00\x00\x00\x00\x00\x00\x00p'
- + b'\x00\x00\x00\x00\x00\x00\x00x'
- + b'\x00\x00\x00\x00\x00\x00\x00\x80'
- + b'\x00\x00\x00\x00\x00\x00\x00\x88'
- + b'\x00\x00\x00\x00\x00\x00\x00\x90'
- + b'\x00\x00\x00\x00\x00\x00\x00\x98'
- + b'\x00\x00\x00\x00\x00\x00\x00\xa0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xa8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xb0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xb8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xc0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xc8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xd0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xd8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xe0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xe8'
- + b'\x00\x00\x00\x00\x00\x00\x00\xf0'
- + b'\x00\x00\x00\x00\x00\x00\x00\xf8'
- + b'\x00\x00\x00\x00\x00\x00\x01\x00'
- + b'\x00\x00\x00\x00\x00\x00\x01\x08'
- + b'\x00\x00\x00\x00\x00\x00\x01\x10'
- + b'\x00\x00\x00\x00\x00\x00\x01\x18'
- + b'\x00\x00\x00\x00\x00\x00\x01 '
- + b'\x00\x00\x00\x00\x00\x00\x01('
- + b'\x00\x00\x00\x00\x00\x00\x010'
- + b'\x00\x00\x00\x00\x00\x00\x018'
- + b'\x00\x00\x00\x00\x00\x00\x01@'
- + b'\x00\x00\x00\x00\x00\x00\x01H'
- + b'\x00\x00\x00\x00\x00\x00\x01P'
- + b'\x00\x00\x00\x00\x00\x00\x01X'
- + b'\x00\x00\x00\x00\x00\x00\x01`'
- + b'\x00\x00\x00\x00\x00\x00\x01h'
- + b'\x00\x00\x00\x00\x00\x00\x01p'
- + b'\x00\x00\x00\x00\x00\x00\x01x'
- + b'\x00\x00\x00\x00\x00\x00\x01\x80'
- + b'\x00\x00\x00\x00\x00\x00\x01\x88'
- + b'\x00\x00\x00\x00\x00\x00\x01\x90'
- + b'\x00\x00\x00\x00\x00\x00\x01\x98'
- + b'\x00\x00\x00\x00\x00\x00\x01\xa0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xa8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xb0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xb8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xc0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xc8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xd0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xd8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xe0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xe8'
- + b'\x00\x00\x00\x00\x00\x00\x01\xf0'
- + b'\x00\x00\x00\x00\x00\x00\x01\xf8')
-
+# The nbdkit pattern plugin exposes 64-bit numbers in bigendian order
+arr = array('q', range(0, 512, 8))
+if sys.byteorder == 'little':
+ arr.byteswap()
+expected = memoryview(arr).cast('B')
+stash = None
def chunk(user_data, buf2, offset, s, err):
print("in chunk, user_data %d" % user_data)
@@ -97,6 +40,8 @@ def chunk(user_data, buf2, offset, s, err):
assert buf2 == expected
assert offset == 0
assert s == nbd.READ_DATA
+ global stash
+ stash = buf2
def callback(user_data, err):
@@ -111,19 +56,29 @@ def callback(user_data, err):
# First try: succeed in both callbacks
-buf = nbd.Buffer(512)
+buf = bytearray(512)
cookie = h.aio_pread_structured(buf, 0,
lambda *args: chunk(42, *args),
lambda *args: callback((42, 42), *args))
while not h.aio_command_completed(cookie):
h.poll(-1)
-buf = buf.to_bytearray()
-
print("%r" % buf)
assert buf == expected
+# The callback can stash its slice; as long as that is live, we can't
+# resize buf but can view changes in buf through the slice
+try:
+ buf.pop()
+ assert False
+except BufferError:
+ pass
+buf[0] ^= 1
+assert buf == stash
+stash = None
+buf.pop()
+
# Second try: fail only during callback
buf = nbd.Buffer(512)
cookie = h.aio_pread_structured(buf, 0,
diff --git a/python/utils.c b/python/utils.c
index cd44b90..d5851f7 100644
--- a/python/utils.c
+++ b/python/utils.c
@@ -192,3 +192,37 @@ nbd_internal_py_wrap_errptr (int err)
return PyObject_CallMethod (py_ctypes_mod, "c_int", "i",
err);
}
+
+/* Helper to compute view.toreadonly()[start:end] in chunk callback */
+PyObject *
+nbd_internal_py_get_subview (PyObject *view, const char *subbuf, size_t count)
+{
+ Py_buffer *orig;
+ const char *base;
+ PyObject *start, *end, *slice;
+ PyObject *ret;
+
+ assert (PyMemoryView_Check (view));
+ orig = PyMemoryView_GET_BUFFER (view);
+ assert (PyBuffer_IsContiguous (orig, 'A'));
+ base = orig->buf;
+ assert (subbuf >= base && count <= orig->len &&
+ subbuf + count <= base + orig->len);
+ start = PyLong_FromLong (subbuf - base);
+ if (!start) return NULL;
+ end = PyLong_FromLong (subbuf - base + count);
+ if (!end) { Py_DECREF (start); return NULL; }
+ slice = PySlice_New (start, end, NULL);
+ Py_DECREF (start);
+ Py_DECREF (end);
+ if (!slice) return NULL;
+ ret = PyObject_GetItem (view, slice);
+ Py_DECREF (slice);
+ /* memoryview.toreadonly() was only added in Python 3.8.
+ * PyMemoryView_GetContiguous (ret, PyBuf_READ, 'A') doesn't
force readonly.
+ * So we mess around directly with the Py_buffer.
+ */
+ if (ret)
+ PyMemoryView_GET_BUFFER (ret)->readonly = 1;
+ return ret;
+}
--
2.36.1