Feature Request: Expose QPainter methods that accept pointers

Ognyan Moore ognyan.moore at gmail.com
Mon Jun 6 17:00:20 BST 2022


Hi Phil,

We started testing on the updated snapshot from this morning, we're seeing
improvements but need to alter our own benchmark code to properly capture
the benefits, so I'll be sending an update in a few days hopefully.

One thing to note is that we did come across a major performance regression
on accident (think 80% performance hit) with
QPainter.drawPolyline(QPolygonF).  While pyqtgraph does not use this
method, PythonQwt does make extensive use of it and that library would
likely be significantly impacted by this penalty (which is why I added
Pierra to this email).

An example of our use case

# create data
x = np. arrange(100)
y = np.zeros(100)

# create qpolygonf instance
size = x.size
polyline = QtGui.QPolygonF()
polyline.fill(QtCore.QPointF(), size)

# get buffer and fill it with original data
nbytes = 2 * len(polyline) * 8
buffer = polyline.data()
buffer.setsize(nbytes)
memory = np.frombuffer(buffer, np.double).reshape((-1, 2))
memory[:, 0] = x
memory[:, 1] = y

# draw it
painter.drawPolyline(polyline)

 If you would like a more complete example you can run let me know.

Ogi

On Thu, Jun 2, 2022 at 8:54 AM Phil Thompson <phil at riverbankcomputing.com>
wrote:

> Whoops - should be fixed in the next snapshot.
>
> I'm seeing about a 70% speedup.
>
> Thanks,
> Phil
>
> On 01/06/2022 04:50, Ognyan Moore wrote:
> > Hi Phil,
> >
> > In sip_core.c line 4883 there is a call to sip_array_can_convert()
> > which
> > appears to return False regardless if we are passing sip.array objects
> > or
> > normal python lists:
> >
> > Here's the code we inserted:
> >
> > if (arg != NULL)
> > {
> >     printf("sip_array_can_convert %d\n", sip_array_can_convert(arg,
> > td));
> >     if (sip_array_can_convert(arg, td))
> >     {
> >         sip_array_convert(arg, array, nr_elem);
> >         *is_temp = FALSE;
> >     }
> >     else if (convertFromSequence(arg, td, array, nr_elem))
> >
> > Here is some example code that we used:
> >
> > from PyQt6 import QtCore, QtGui
> > import PyQt6.sip as sip
> > import numpy as np
> >
> > import itertools
> > from time import perf_counter
> >
> > class LineSegments:
> >     def __init__(self, use_sip_array):
> >         self.use_sip_array = hasattr(sip, 'array') and use_sip_array
> >         self.alloc(0)
> >
> >     def alloc(self, size):
> >         if self.use_sip_array:
> >             self.objs = sip.array(QtCore.QLineF, size)
> >             vp = sip.voidptr(self.objs, len(self.objs)*4*8)
> >             self.arr = np.frombuffer(vp, dtype=np.float64).reshape((-1,
> > 4))
> >         else:
> >             self.arr = np.empty((size, 4), dtype=np.float64)
> >             self.objs = list(map(sip.wrapinstance,
> >                 itertools.count(self.arr.ctypes.data,
> > self.arr.strides[0]),
> >                 itertools.repeat(QtCore.QLineF, self.arr.shape[0])))
> >
> >     def get(self, size):
> >         if size != self.arr.shape[0]:
> >             self.alloc(size)
> >         return self.objs, self.arr
> >
> > def generate_pattern(nsegs, size):
> >     rng = np.random.default_rng()
> >     x = rng.random(nsegs) * size
> >     y = rng.random(nsegs) * size
> >     arr = np.empty((nsegs, 4), dtype=np.float64)
> >     arr[:, 0] = x
> >     arr[:, 1] = y
> >     arr[:, 2] = x + 2
> >     arr[:, 3] = y + 2
> >     return arr
> >
> > nsegs = 10_000
> > size = 500
> > nframes = 100
> > pattern = generate_pattern(nsegs, size)
> >
> > def run(use_sip_array):
> >     # generate lines once
> >     segments = LineSegments(use_sip_array)
> >     lines, memory = segments.get(nsegs)
> >     memory[:] = pattern
> >
> >     # draw multiple frames using the same lines array
> >     t0 = perf_counter()
> >     for _ in range(nframes):
> >         qimg = QtGui.QImage(size, size,
> > QtGui.QImage.Format.Format_RGB32)
> >         qimg.fill(QtCore.Qt.GlobalColor.transparent)
> >         painter = QtGui.QPainter(qimg)
> >         painter.setPen(QtCore.Qt.GlobalColor.cyan)
> >         painter.drawLines(lines)
> >         painter.end()
> >     t1 = perf_counter()
> >     return t1 - t0
> >
> > for use_sip_array in [False, True]:
> >     duration = run(use_sip_array)
> >     fps = int(nframes / duration)
> >     print(f'{use_sip_array=} {duration=:.3f} {fps=}')
> >
> > On Sat, May 28, 2022 at 8:17 AM Ognyan Moore <ognyan.moore at gmail.com>
> > wrote:
> >
> >> Right; will adjust our benchmarking and keep testing.  Thanks!
> >>
> >> On Sat, May 28, 2022 at 01:18 Phil Thompson
> >> <phil at riverbankcomputing.com>
> >> wrote:
> >>
> >>> On 28/05/2022 06:39, Ognyan Moore wrote:
> >>> > Hi Phil,
> >>> >
> >>> > We started doing some benchmarking using the following script:
> >>> >
> >>> > from PyQt6 import QtCore, QtGui
> >>> > import PyQt6.sip as sip
> >>> > import numpy as np
> >>> >
> >>> > import itertools
> >>> > from time import perf_counter
> >>> >
> >>> > class LineSegments:
> >>> >     def __init__(self, use_sip_array):
> >>> >         self.use_sip_array = hasattr(sip, 'array') and use_sip_array
> >>> >         self.alloc(0)
> >>> >
> >>> >     def alloc(self, size):
> >>> >         if self.use_sip_array:
> >>> >             self.objs = sip.array(QtCore.QLineF, size)
> >>> >             vp = sip.voidptr(self.objs, len(self.objs)*4*8)
> >>> >             self.arr = np.frombuffer(vp,
> dtype=np.float64).reshape((-1,
> >>> > 4))
> >>> >         else:
> >>> >             self.arr = np.empty((size, 4), dtype=np.float64)
> >>> >             self.objs = list(map(sip.wrapinstance,
> >>> >                 itertools.count(self.arr.ctypes.data,
> >>> > self.arr.strides[0]),
> >>> >                 itertools.repeat(QtCore.QLineF, self.arr.shape[0])))
> >>> >
> >>> >     def get(self, size):
> >>> >         if size != self.arr.shape[0]:
> >>> >             self.alloc(size)
> >>> >         return self.objs, self.arr
> >>> >
> >>> > def run(size, use_sip_array):
> >>> >     qimg = QtGui.QImage(640, 480, QtGui.QImage.Format.Format_RGB32)
> >>> >     qimg.fill(QtCore.Qt.GlobalColor.transparent)
> >>> >     segments = LineSegments(use_sip_array)
> >>> >     objs, arr = segments.get(size)
> >>> >
> >>> >     arr[:, 0] = 0
> >>> >     arr[:, 1] = 0
> >>> >     arr[:, 2] = qimg.width()
> >>> >     arr[:, 3] = qimg.height()
> >>> >
> >>> >     painter = QtGui.QPainter(qimg)
> >>> >     painter.setPen(QtCore.Qt.GlobalColor.cyan)
> >>> >
> >>> >     draw_t0 = perf_counter()
> >>> >     painter.drawLines(objs)
> >>> >     draw_t1 = perf_counter()
> >>> >
> >>> >     painter.end()
> >>> >     return draw_t1 - draw_t0
> >>> >
> >>> > size = int(1e6)
> >>> > for use_sip_array in [True, False]:
> >>> >     dt = run(size, use_sip_array)
> >>> >     print(f'{use_sip_array=} {dt:.3f}')
> >>> >
> >>> >
> >>> > Here we noticed that using sip.array was slightly faster than not on
> >>> > macOS,
> >>> > but on Windows it was slightly slower.  Then when we changed the
> >>> > endpoint
> >>> > of the line to (10, 10) instead of the width and height of the QImage
> >>> > as
> >>> > such:
> >>> >
> >>> > arr[:, 2] = 10
> >>> > arr[:, 3] = 10
> >>> >
> >>> > We started seeing significantly higher penalties for using sip.array
> >>> >
> >>> > On macOS:
> >>> >
> >>> > use_sip_array=True 0.513
> >>> > use_sip_array=False 0.171
> >>> >
> >>> >
> >>> > On Windows:
> >>> >
> >>> > use_sip_array=True 0.351
> >>> > use_sip_array=False 0.218
> >>> >
> >>> >
> >>> > Also for sanity sake, the output of pip list:
> >>> >
> >>> > Package    Version             Editable project location
> >>> > ---------- ------------------- ------------------------------
> >>> > numpy      1.22.4
> >>> > pip        22.1.1
> >>> > PyQt6      6.3.1.dev2205201737
> >>> > PyQt6-Qt6  6.3.0
> >>> > PyQt6-sip  13.4.0
> >>> > pyqtgraph  0.12.4.dev0         /Users/ogi/Developer/pyqtgraph
> >>> > setuptools 58.1.0
> >>> > wheel      0.37.1
> >>> >
> >>> > Let me know if there is another use case I should test.
> >>>
> >>> That's what I would expect with that use case. You are creating the
> >>> array each time you want to do a draw so there is no benefit. The
> >>> benefit is when you create the array once and draw many times. In a
> >>> previous email you said that approach was usable.
> >>>
> >>> Phil
> >>>
> >>
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://www.riverbankcomputing.com/pipermail/pyqt/attachments/20220606/302548e3/attachment.htm>


More information about the PyQt mailing list