Skip to content

The plot function of the matplotlib 2 and 3 versions is much slower than 1.5.3 #12542

Closed

Description

@materia9

Bug report

Bug summary

The plot function of the matplotlib 2 and 3 versions is much slower than the plot function of matplotlib 1.5.3 if one plots many lines (and other objects) in an Axes.

Code for reproduction

I have tested it on an Ubuntu server (CLI only), version 16.04 with python 3.5.2 (through virtualenv), matplotlib 1.5.3 with backend agg set by matplotlibrc.

setup="""import matplotlib.pyplot as plt
a=[1,2]"""
test="plt.plot(a,a,'-')"
import timeit
timeit.timeit(test,setup,number=1000)
# output: 0.6324263589922339
timeit.timeit(test,setup,number=1000)
# output: 0.6100882450118661
timeit.timeit(test,setup,number=1000)
# output: 0.6113728249911219

=> around 0.6-0.7 sec, it is stable for a few dozen times.

On the same machine & operating system, python 3.5.2 (through virtualenv), matplotlib 2.2.2 with backend agg.

setup="""import matplotlib.pyplot as plt
a=[1,2]"""
test="plt.plot(a,a,'-')"
import timeit
timeit.timeit(test,setup,number=1000)
# output: 0.9981246230890974
timeit.timeit(test,setup,number=1000)
# output: 1.3041282850317657
timeit.timeit(test,setup,number=1000)
# output: 1.6257964670658112
timeit.timeit(test,setup,number=1000)
# output: 1.9830634299432859

=> getting slower and slower...

Note that matplotlib 2.0.0, 2.0.1, 2.1.0, 2.1.2, 2.2.2, or 3.0.0 has the same slowdown behavior. If I set svg as backend, the result is the same as agg (I confirmed backend="svg" only for matplotlib 2.0.0 on the Ubuntu server).

This behavior is also confirmed on a Windows distribution: WinPython-64bit-3.4.4.6Qt5 with matplotlib 2.0.0 and default backend TkAgg, but not for WinPython-64bit-3.4.4.5Qt5 with matplotlib version 1.5.3 and TkAgg, though the situation is much more complex. In addition, WinPython-64bit-3.5.1.1 and matplotlib 2.1.0 (packaged by Christoph Gohlke) causes the same slowdown behavior.

Another windows distribution anaconda 3 version 4.4.0(64bit) also has a similar behavior. For the anaconda 3 environment, all the packages are default of the 4.4.0 distribution (python 3.6 and matplotlib 2.0.2 with backend qt5agg), and it runs on windows7-x64.

If I close the figure by pyplot.close(), the slowed plot() is returned to the initial state.

Expected outcome

The expected outcome is "no slowdown for the plot function of matplotlib 3" as the matplotlib version 1.5.3.

Matplotlib version

  • Operating system: Ubuntu 1604 (CLI only)
  • Matplotlib version: matplotlib 2.2.2
  • Matplotlib backend (print(matplotlib.get_backend())): agg
  • Python version: 3.5.2
  • Jupyter version (if applicable):
  • Other libraries:

Other note

I have used a script to write huge maps with overlayed many lines drawn by plot(). When I'm with matplotlib 1.5.3, it takes several minutes to output a bunch of png files, but no fatal problem is there. However, with 2.0.0-3.0.0 it takes several hours or more, and I cannot use the script at all.

I believe this slowdown behavior can occur for most of the matplotlib environments. I think this is not an OS/system dependent bug/specification.

Activity

  1. jklymak commented on Oct 17, 2018

    @jklymak
    Member

    I agree your script slows down for me.

    OTOH, initializing a figure in your setup doesn't exhibit this slow down. I'm not sure why this happens, but I'm not sure what your use case is.

    In general, I'd suggest not trying to use the state-based interface for the best performance...

    setup="""import matplotlib.pyplot as plt
    a=[1,2]
    fig = plt.figure()
    """
    test="plt.plot(a, a, '-')"
    import timeit
    timeit.timeit(test,setup,number=1000)
    timeit.timeit(test,setup,number=1000)
    timeit.timeit(test,setup,number=1000)
    timeit.timeit(test,setup,number=1000)
    timeit.timeit(test,setup,number=1000)
    timeit.timeit(test,setup,number=1000)
  2. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    OTOH, initializing a figure in your setup doesn't exhibit this slow down. I'm not sure why this happens, but I'm not sure what your use case is.

    But I think that's the point, since:

    If I close the figure by pyplot.close(), the slowed plot() is returned to the initial state.

    On the other hand, drawing 4000 lines vs 1000 lines seems like it should be slower to me, but is it too much slower per line? I'm not sure, possibly, but I think a more interesting test is this:

    >>> setup="""import matplotlib.pyplot as plt
    ... a=[1,2]
    ... plt.close('all')
    ... """
    >>> 
    >>> test="plt.plot(a,a,'-')"
    >>> import timeit
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=1)
    >>> timeit.timeit(test,setup,number=10)
    >>> timeit.timeit(test,setup,number=100)
    >>> timeit.timeit(test,setup,number=1000)
    >>> timeit.timeit(test,setup,number=10000)
    >>> timeit.timeit(test,setup,number=100000)
    >>> timeit.timeit(test,setup,number=200000)

    which does not seem to scale so well in recent versions:
    figure_1-2
    Extrapolating says it should take 400s for count=100000 on 3.0.0, but I'm still waiting ~60 minutes later...

  3. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    Oh, actually, 100000 count finished, so (this is loglog):
    figure_1-3
    Looking at time per line (just time / number) (this is only semilogy because the count shouldn't matter):
    figure_1-4
    Ignoring 1 and 10 which are pretty noisy, 10000 lines is an order of magnitude slower, but at 100000 lines, it's two orders of magnitude slower per line. I don't know what's going on there.

  4. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    More straightforward test, I think?

    import timeit
    
    setup = """
    import matplotlib.pyplot as plt
    plt.close('all')
    a = [1, 2]
    """
    test = "plt.plot(a,a,'-')"
    
    # Prime stuff
    timeit.timeit(test, setup, number=10)
    
    t1 = timeit.timeit(test, setup, number=1000)
    t2 = timeit.timeit(test, setup, number=10000)
    print(t1, t2, t2 / t1)

    with results:

    version	time1000		time10000		ratio
    1.5.3	1.665212828986114	8.014447881025262	4.812867005057222
    2.0.0	2.089658019016497	44.532892381976126	21.311091086059935
    2.0.1	2.085433025000384	44.81260865801596	21.488395033931962
    2.0.2	2.05447192498832	42.90871796300053	20.88552169591924
    2.1.0	2.251952402002644	40.38666930099134	17.934068795182256
    2.1.1	1.9482705270056613	38.53039746900322	19.776718343227937
    2.1.2	2.0048703539941926	40.01708639998105	19.959937220008875
    2.2.0	2.104869660019176	40.214108515006956	19.105272539602566
    2.2.1	1.962975592003204	39.36644780100323	20.05447645980663
    2.2.2	1.9735110830224585	39.55972639701213	20.04535304480068
    2.2.3	1.9041270589805208	38.6958993919834	20.322120422311198
    3.0.0	2.072705873986706	39.154758671997115	18.890648771446596
    

    The base time went up slightly, but the main point is that the ratio is much higher now for some reason.

  5. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    Ran an automated bisect with ratio=13 as good/bad threshold and it points to 9617dfe as the first bad commit

    commit 9617dfef1f377760645b7986d3081cca34aa0805
    Author: Thomas A Caswell <[email protected]>
    Date:   Tue Dec 8 13:09:10 2015 -0500
    
        Merge pull request #5583 from mdboom/padding
        
        API: Use data limits plus a little padding by default
    
    :040000 040000 b905ec739efee2f8b0c4ebb14b46760497c30e6d a7b4c85793d0d175bee8582c91c7c625dc4d909e M	lib
    :100644 100644 b4e602b846c5c1b360fede4fc3c2b16fd095b57d 604b1036d911e9522d67d35489dad897b94f4a85 M	matplotlibrc.template
    

    which I guess might make sense as automated margins would require iterating through all artists.

  6. anntzer commented on Oct 17, 2018

    @anntzer
    Contributor

    Thanks for figuring this out.
    This points once again to #7413, namely @efiring's first point:

    It looks very inefficient: every plotting method in _axes adds an artist to the axes and then calls autoscale_view, occasionally with arguments. autoscale_view then does a complete autoscaling operation, going through all of the artists that have been added up to that point. Logically, it seems like the autoscaling should be done only before a draw operation, not every time an artist is added.

    We're hitting quadratic behavior here :/

  7. dstansby commented on Oct 17, 2018

    @dstansby
    Member
  8. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    I'm still waiting for them to accept the PR to enable it on NumPy's bench system...

  9. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    A pretty generous speed up can be had by disabling autoscale and sticky edges before plotting, and then re-enabling them after:

    import matplotlib.pyplot as plt
    
    a = [1, 2]
    
    fig, ax = plt.subplots()
    for _ in range(10000):
        ax.plot(a, a, '-')
    $ time python ../mpl-tests/issue12542a.py 
    real	0m39.860s
    user	0m38.035s
    sys	0m0.186s

    vs.

    import matplotlib.pyplot as plt
    
    a = [1, 2]
    
    fig, ax = plt.subplots()
    ax.autoscale(False)
    ax.use_sticky_edges = False
    for _ in range(10000):
        ax.plot(a, a, '-')
    ax.use_sticky_edges = True
    ax.autoscale(True)
    $ time python ../mpl-tests/issue12542a.py 
    real	0m7.476s
    user	0m5.423s
    sys	0m0.168s
  10. anntzer commented on Oct 17, 2018

    @anntzer
    Contributor

    See #12546 for a small improvement.

  11. QuLogic commented on Oct 17, 2018

    @QuLogic
    Member

    #12547 is a related change insomuch as it helps if you've already disable autoscale, but it's orthogonal to @anntzer's change.

  12. jklymak commented on Oct 17, 2018

    @jklymak
    Member

    Ooops, I see what the difference is now in the tests. Sorry for the noise.

    I think a todo here, asside from makig the autolim algorithm faster, is to have a Performance Tweaks page where we list potential performance blocks that will slow things down for folks who want to have thousands of artists, or a very responsive GUI. Off the top of my head:

    • autolimits
    • "best" legend location
    • the layout managers

    No doubt there are more.

  13. ImportanceOfBeingErnest commented on Oct 17, 2018

    @ImportanceOfBeingErnest
    Member

    Adding to the list (especially with respect to the actual topic here)

    • Using collections instead of individual artists. I.e. a LineCollection instead of many plots, a PathCollection or PolyCollection or even Path.make_compound_path_from_polys instead of a bar with many bars (like here), etc.
  14. QuLogic commented on Oct 18, 2018

    @QuLogic
    Member

    OK, interestingly, though we definitely would want to avoid autoscale being called too much, looking at a profiler, it's actually accumulating sticky edges that takes the most time (even with @anntzer's optimization there):

    Total time: 57.1916 s
    File: .../matplotlib/lib/matplotlib/axes/_base.py
    Function: autoscale_view at line 2382
    
    Line #      Hits         Time  Per Hit   % Time  Line Contents
    ==============================================================
      2382                                               @profile
      2383                                               def autoscale_view(self, tight=None, scalex=True, scaley=True):
      2384                                                   """
      2385                                                   Autoscale the view limits using the data limits.
      2386                                           
      2387                                                   You can selectively autoscale only a single axis, e.g., the xaxis by
      2388                                                   setting *scaley* to *False*.  The autoscaling preserves any
      2389                                                   axis direction reversal that has already been done.
      2390                                           
      2391                                                   If *tight* is *False*, the axis major locator will be used
      2392                                                   to expand the view limits if rcParams['axes.autolimit_mode']
      2393                                                   is 'round_numbers'.  Note that any margins that are in effect
      2394                                                   will be applied first, regardless of whether *tight* is
      2395                                                   *True* or *False*.  Specifying *tight* as *True* or *False*
      2396                                                   saves the setting as a private attribute of the Axes; specifying
      2397                                                   it as *None* (the default) applies the previously saved value.
      2398                                           
      2399                                                   The data limits are not updated automatically when artist data are
      2400                                                   changed after the artist has been added to an Axes instance.  In that
      2401                                                   case, use :meth:`matplotlib.axes.Axes.relim` prior to calling
      2402                                                   autoscale_view.
      2403                                                   """
      2404     10000      24447.0      2.4      0.0          if tight is not None:
      2405                                                       self._tight = bool(tight)
      2406                                           
      2407     10000      29351.0      2.9      0.1          if self.use_sticky_edges and (
      2408     10000      24853.0      2.5      0.0                  (self._xmargin and scalex and self._autoscaleXon) or
      2409                                                           (self._ymargin and scaley and self._autoscaleYon)):
      2410     10000   27187493.0   2718.7     47.5              stickies = [artist.sticky_edges for artist in self.get_children()]
      2411     10000    8970882.0    897.1     15.7              x_stickies = np.array([x for sticky in stickies for x in sticky.x])
      2412     10000    9718730.0    971.9     17.0              y_stickies = np.array([y for sticky in stickies for y in sticky.y])
      2413     10000      46397.0      4.6      0.1              if self.get_xscale().lower() == 'log':
      2414                                                           x_stickies = x_stickies[x_stickies > 0]
      2415     10000      30285.0      3.0      0.1              if self.get_yscale().lower() == 'log':
      2416                                                           y_stickies = y_stickies[y_stickies > 0]
      2417                                                   else:  # Small optimization.
      2418                                                       x_stickies, y_stickies = [], []
      2419                                           
      2420     10000     257034.0     25.7      0.4          @profile
      2421                                                   def handle_single_axis(scale, autoscaleon, shared_axes, interval,
      2422                                                                          minpos, axis, margin, stickies, set_bound):
      2423                                           
      2424                                                       if not (scale and autoscaleon):
      2425                                                           return  # nothing to do...
      2426                                           
      2427                                                       shared = shared_axes.get_siblings(self)
      2428                                                       dl = [ax.dataLim for ax in shared]
      2429                                                       # ignore non-finite data limits if good limits exist
      2430                                                       finite_dl = [d for d in dl if np.isfinite(d).all()]
      2431                                                       if len(finite_dl):
      2432                                                           # if finite limits exist for atleast one axis (and the
      2433                                                           # other is infinite), restore the finite limits
      2434                                                           x_finite = [d for d in dl
      2435                                                                       if (np.isfinite(d.intervalx).all() and
      2436                                                                           (d not in finite_dl))]
      2437                                                           y_finite = [d for d in dl
      2438                                                                       if (np.isfinite(d.intervaly).all() and
      2439                                                                           (d not in finite_dl))]
      2440                                           
      2441                                                           dl = finite_dl
      2442                                                           dl.extend(x_finite)
      2443                                                           dl.extend(y_finite)
      2444                                           
      2445                                                       bb = mtransforms.BboxBase.union(dl)
      2446                                                       x0, x1 = getattr(bb, interval)
      2447                                                       locator = axis.get_major_locator()
      2448                                                       try:
      2449                                                           # e.g., DateLocator has its own nonsingular()
      2450                                                           x0, x1 = locator.nonsingular(x0, x1)
      2451                                                       except AttributeError:
      2452                                                           # Default nonsingular for, e.g., MaxNLocator
      2453                                                           x0, x1 = mtransforms.nonsingular(
      2454                                                               x0, x1, increasing=False, expander=0.05)
      2455                                           
      2456                                                       # Add the margin in figure space and then transform back, to handle
      2457                                                       # non-linear scales.
      2458                                                       minpos = getattr(bb, minpos)
      2459                                                       transform = axis.get_transform()
      2460                                                       inverse_trans = transform.inverted()
      2461                                                       # We cannot use exact equality due to floating point issues e.g.
      2462                                                       # with streamplot.
      2463                                                       do_lower_margin = not np.any(np.isclose(x0, stickies))
      2464                                                       do_upper_margin = not np.any(np.isclose(x1, stickies))
      2465                                                       x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minpos)
      2466                                                       x0t, x1t = transform.transform([x0, x1])
      2467                                           
      2468                                                       if (np.isfinite(x1t) and np.isfinite(x0t)):
      2469                                                           delta = (x1t - x0t) * margin
      2470                                                       else:
      2471                                                           # If at least one bound isn't finite, set margin to zero
      2472                                                           delta = 0
      2473                                           
      2474                                                       if do_lower_margin:
      2475                                                           x0t -= delta
      2476                                                       if do_upper_margin:
      2477                                                           x1t += delta
      2478                                                       x0, x1 = inverse_trans.transform([x0t, x1t])
      2479                                           
      2480                                                       if not self._tight:
      2481                                                           x0, x1 = locator.view_limits(x0, x1)
      2482                                                       set_bound(x0, x1)
      2483                                                       # End of definition of internal function 'handle_single_axis'.
      2484                                           
      2485     10000      19034.0      1.9      0.0          handle_single_axis(
      2486     10000      24963.0      2.5      0.0              scalex, self._autoscaleXon, self._shared_x_axes, 'intervalx',
      2487     10000    5609615.0    561.0      9.8              'minposx', self.xaxis, self._xmargin, x_stickies, self.set_xbound)
      2488     10000      20417.0      2.0      0.0          handle_single_axis(
      2489     10000      20418.0      2.0      0.0              scaley, self._autoscaleYon, self._shared_y_axes, 'intervaly',
      2490     10000    5207680.0    520.8      9.1              'minposy', self.yaxis, self._ymargin, y_stickies, self.set_ybound)
    
    Total time: 9.55789 s
    File: .../matplotlib/lib/matplotlib/axes/_base.py
    Function: handle_single_axis at line 2420
    
    Line #      Hits         Time  Per Hit   % Time  Line Contents
    ==============================================================
      2420                                                   @profile
      2421                                                   def handle_single_axis(scale, autoscaleon, shared_axes, interval,
      2422                                                                          minpos, axis, margin, stickies, set_bound):
      2423                                           
      2424     20000      25491.0      1.3      0.3              if not (scale and autoscaleon):
      2425                                                           return  # nothing to do...
      2426                                           
      2427     20000     131620.0      6.6      1.4              shared = shared_axes.get_siblings(self)
      2428     20000      37372.0      1.9      0.4              dl = [ax.dataLim for ax in shared]
      2429                                                       # ignore non-finite data limits if good limits exist
      2430     20000     361819.0     18.1      3.8              finite_dl = [d for d in dl if np.isfinite(d).all()]
      2431     20000      32171.0      1.6      0.3              if len(finite_dl):
      2432                                                           # if finite limits exist for atleast one axis (and the
      2433                                                           # other is infinite), restore the finite limits
      2434     20000     184566.0      9.2      1.9                  x_finite = [d for d in dl
      2435                                                                       if (np.isfinite(d.intervalx).all() and
      2436                                                                           (d not in finite_dl))]
      2437     20000     129965.0      6.5      1.4                  y_finite = [d for d in dl
      2438                                                                       if (np.isfinite(d.intervaly).all() and
      2439                                                                           (d not in finite_dl))]
      2440                                           
      2441     20000      25877.0      1.3      0.3                  dl = finite_dl
      2442     20000      27806.0      1.4      0.3                  dl.extend(x_finite)
      2443     20000      23528.0      1.2      0.2                  dl.extend(y_finite)
      2444                                           
      2445     20000    1819300.0     91.0     19.0              bb = mtransforms.BboxBase.union(dl)
      2446     20000     115457.0      5.8      1.2              x0, x1 = getattr(bb, interval)
      2447     20000      47695.0      2.4      0.5              locator = axis.get_major_locator()
      2448     20000      22764.0      1.1      0.2              try:
      2449                                                           # e.g., DateLocator has its own nonsingular()
      2450     20000      43310.0      2.2      0.5                  x0, x1 = locator.nonsingular(x0, x1)
      2451     20000      27332.0      1.4      0.3              except AttributeError:
      2452                                                           # Default nonsingular for, e.g., MaxNLocator
      2453     20000      26835.0      1.3      0.3                  x0, x1 = mtransforms.nonsingular(
      2454     20000     294789.0     14.7      3.1                      x0, x1, increasing=False, expander=0.05)
      2455                                           
      2456                                                       # Add the margin in figure space and then transform back, to handle
      2457                                                       # non-linear scales.
      2458     20000      47082.0      2.4      0.5              minpos = getattr(bb, minpos)
      2459     20000     113594.0      5.7      1.2              transform = axis.get_transform()
      2460     20000      33573.0      1.7      0.4              inverse_trans = transform.inverted()
      2461                                                       # We cannot use exact equality due to floating point issues e.g.
      2462                                                       # with streamplot.
      2463     20000    1383600.0     69.2     14.5              do_lower_margin = not np.any(np.isclose(x0, stickies))
      2464     20000    1092805.0     54.6     11.4              do_upper_margin = not np.any(np.isclose(x1, stickies))
      2465     20000      50382.0      2.5      0.5              x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minpos)
      2466     20000     159674.0      8.0      1.7              x0t, x1t = transform.transform([x0, x1])
      2467                                           
      2468     20000      99877.0      5.0      1.0              if (np.isfinite(x1t) and np.isfinite(x0t)):
      2469     20000      35382.0      1.8      0.4                  delta = (x1t - x0t) * margin
      2470                                                       else:
      2471                                                           # If at least one bound isn't finite, set margin to zero
      2472                                                           delta = 0
      2473                                           
      2474     20000      23609.0      1.2      0.2              if do_lower_margin:
      2475     20000      27287.0      1.4      0.3                  x0t -= delta
      2476     20000      23276.0      1.2      0.2              if do_upper_margin:
      2477     20000      27478.0      1.4      0.3                  x1t += delta
      2478     20000     111685.0      5.6      1.2              x0, x1 = inverse_trans.transform([x0t, x1t])
      2479                                           
      2480     20000      31767.0      1.6      0.3              if not self._tight:
      2481     20000     343003.0     17.2      3.6                  x0, x1 = locator.view_limits(x0, x1)
      2482     20000    2576115.0    128.8     27.0              set_bound(x0, x1)
    
  15. anntzer commented on Oct 18, 2018

    @anntzer
    Contributor

    One can squeeze out another ~30%(!) or so by replacing the property access (artist.sticky_edges, sticky.x, sticky.y) by direct attribute/tuple access (artist._sticky_edges, sticky[0], sticky[1] (sticky is a namedtuple)), which I guess is a reasonable patch (with a comment explaining the performance implications) but really just a band-aid over a problematic design.

  16. materia9 commented on Oct 18, 2018

    @materia9
    Author

    Thank you everyone. I now realize that the root cause of the problem at least for my case. Now I'm trying to use the options: autoscale(False) and use_sticky_edges = False. I'm a lazy programmer and thus I found this behavior (maybe a good programmer can solve the speed limitation completely and never realizes the problem). I think a performance tweak page will be a very good start point even for me.

  17. jklymak commented on Oct 18, 2018

    @jklymak
    Member

    So is ti possible to delay autoscale_view to draw time? Seems a straightforward solution.

    If that is not possible, could we accumulate x_stickies and y_stickies in add_artist and remove them in artist.remove? Unless I'm misunderstanding, the problem is that making these lists from scratch every time a new artist is added is inefficient.

  18. WeatherGod commented on Oct 18, 2018

    @WeatherGod
    Member
  19. tacaswell commented on Oct 19, 2018

    @tacaswell
    Member

    If we cache the sticky-ness, we need to track if it is updated on the artists.

  20. added this to the v3.2.0 milestone on Mar 17, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions