Technical Articles

Tips for Accelerating MATLAB Performance

By Yair Altman, Undocumented MATLAB


My goal in this article is to show you that, by investing a small amount of time (compared to overall R&D time), you can accelerate MATLAB® programs by a large factor. I’ll demonstrate this claim with work that I completed for the Crustal Dynamics research group at Harvard University.

The Crustal Dynamics research group created an interactive MATLAB program that visualizes deformation and motion at plate boundary zones, recorded as GPS velocities (Figure 1).

Figure 1. Crustal dynamics visualization program.

Figure 1. Crustal dynamics visualization program.

The program served the researchers well for several years―until the program’s performance suffered when they tried to analyze larger problems with more data. In fact, it was getting so slow that it severely limited their ability to interrogate the results and work productively. This is when I stepped in.

Two performance issues stood out: Loading the data and displaying the textual labels of numeric slip rates.

I/O: Loading Data

I began by optimizing the data load time. This involved loading several different data files in custom textual formats.

I started by running a profiling session using the MATLAB Profiler. Profiling should never be bypassed: it helps us pinpoint and solve the most critical hotspots, which often turn out to be different from our initial assumptions. In this case, profiling quickly showed that parsing the text files was a performance bottleneck. Better still, the profiler pinpointed the bottleneck locations.

Two specific types of parsing were used by the code, and they were both quite slow: reading the input files using textread, and parsing the input data using str2num.

To read the data from the files, I replaced the textread calls with corresponding textscan calls (a replacement which is also recommended by the MATLAB Code Analyzer):

% Old (slow) code:
[Station.lon, Station.lat, Station.eastVel, Station.northVel, ...
 Station.eastSig, Station.northSig, Station.corr, ...
 Station.other1, Station.tog, Station.name] = textread(fileName, '%f%f%f%f%f%f%f%d%d%s');

% New (fast) code:
fid = fopen(fileName,'rt');
c = textscan(fid, '%f%f%f%f%f%f%f%d%d%s');
fclose(fid);
fn = {'lon', 'lat', 'eastVel', 'northVel', 'eastSig', 'northSig', 'corr', 'other1', 'tog', 'name'};
Station = cell2struct(c,fn,2);

To improve the performance of the str2num calls, I differentiated between two sub-cases.

In some cases, str2num was being used to simply round numeric input data to a certain numeric precision. I improved this by replacing the str2num calls with corresponding calls to round, which accepts an optional precision argument:

% Old (slow) code
Station.lon = str2num(num2str(Station.lon, '%3.3f'));

% New (fast) code
Station.lon = round(Station.lon,3);        % R2014b or newer
Station.lon = round(Station.lon*1e3)/1e3;  % R2014a or older

In other cases, str2num was used to convert strings into numeric values. This should normally be done using a textscan parameter, but in this specific case doing this was complicated due to the way the data was formatted, which required parsing iterative blocks. Still, converting strings into numbers is far faster using sscanf than str2num for the data used here (cell arrays of chars such as '136.235 35.509 136.133 35.711'). The down side is that str2num also works in certain edge-cases where sscanf doesn't. For this reason, I created a utility function (str2num_fast.m) that uses sscanf where possible and falls back to str2num in case of problems. I then simply replaced all calls to str2num in the code to str2num_fast:

% str2num_fast - faster alternative to str2num
function data = str2num_fast(str, numCols)
    try
        % Fast code:
        str = char(str);
        str(:,end+1) = ' ';
        data = sscanf(str','%f');
        if nargin>1 && ~isempty(numCols)
            data = reshape(data,numCols,[])';
        end
    catch
        % This is much simpler but also much slower...
        data = str2num(str);
    end
end

The overall result: loading a medium-sized data set, which used to take 5-6 minutes (much longer for larger data sets), now took less than 1 second, a speedup of x500. When you are continuously loading and comparing different data sets, speedup like this can mean the difference between a usable and an unusable program. Not bad for starters...

Displaying Data

I now turned my attention to the graphic visualization aspects of the program. As Figure 1 shows, multiple layers of textual labels, arrows, lines, and data points can be added to the chart.

The first rule for improving graphics performance is this: unless the objects need to be frequently toggled on/off, no graphic element should remain plotted if it is not visible. It turned out that, in the interest of improved performance, the checkboxes were designed to merely turn their visibility on or off. This does indeed improve performance in the specific use-case of checking and unchecking a particular checkbox. But in the general case, it compromises performance by adding numerous graphic objects to the plot. I found that checking just three of these checkboxes created 37,365 different graphic objects within the plot axes. That's a HUGE number, and it's no surprise that adding more visualization layers or zooming/panning the axes became excruciatingly slow, even when the layers were turned off (i.e., made invisible). This is because the MATLAB graphics engine needs to manage all these objects, even when they are not visible.

To fix this, the corresponding objects are deleted rather than made invisible when a visualization layer's checkbox is deselected (there is, of course, a throughput/latency tradeoff between the recurring object's creation time and the performance impact of keeping numerous invisible objects):

% hCheckbox is the handle of the selected/deselected checkbox 
% hPlotHandles is the list of corresponding plotted graphic handles
if get(hCheckbox, 'Value') == 0
   %set(hPlotHandles, 'Visible', 'off');  % Old (slow) code
   delete(hPlotHandles);  % Faster throughput in our use-case
else
   hPlotHandles = something
end

A related aspect is that if the axes is zoomed-in (as is often the case in this specific program), then there is no need to plot any graphic element that is outside the axes limits:

% Old (slow) code:
text(lons, lats, labels);

% Much faster: limit the labels only to the visible axes area
hAxes = handle(gca);
validIdx = within(lons, hAxes.XLim) & within(lats, hAxes.YLim);
text(lons(validIdx), lats(validIdx), labels(validIdx,:));

function validIdx = within(data,limits)
    validIdx = data >= limits(1) & data <= limits(2);
end

Finally, to reduce the number of displayed graphic handles we can unify the separate line segments into a single line object that has NaN (or Inf) values interspaced between its segments. Mike Garrity describes this important graphics technique and others in his blog. Employing it here reduces ~7000 separate line handles into a single line, which in turn improves both the line creation time and any subsequent axes action, such as zoom or pan. It is even faster than limiting the display to the axes limits (and yes, we could combine them by displaying a single line that has fewer data points that fit the axes limits, but in this case the extra performance benefit would be small compared to the computation time):

% Old (slow) code:
line([Segment.lon1'; Segment.lon2'], [Segment.lat1'; Segment.lat2']);

% Faster code: limit the display to the axes limits
hAxes = handle(gca);
lonLimits = hAxes.XLim;
latLimits = hAxes.YLim;
valid = (within(Segment.lon1,lonLimits) | within(Segment.lon2,lonLimits)) & ...
        (within(Segment.lat1,latLimits) | within(Segment.lat2,latLimits));
line([Segment.lon1(valid)', Segment.lon2(valid)'], ...
     [Segment.lat1(valid)', Segment.lat2(valid)']);

% New (fastest) code:
lons = [Segment.lon1'; Segment.lon2'; nan(1,numel(Segment.lon2))];
lats = [Segment.lat1'; Segment.lat2'; nan(1,numel(Segment.lat2))];
line(lons(:), lats(:));

The result: the time for displaying the slip-rate labels in the zoomed-in Australasia region in Figure 1 was reduced from 33 seconds to 0.6 secs, and displaying the residual velocity vectors was reduced from 1.63 secs to 0.02 secs―speedups of x50-100. Again, not bad at all... The program was now fast enough to enable true interactivity. In Prof. Brendan Meade's words:

The program is now tremendously fast with all arrow type visualizations! It's amazing and enables us to work with large scale data sets much more efficiently... This is really making a difference in terms of how fast we can do science!

Techniques for Improving Graphics Performance

Here are some techniques for speeding up graphic objects creation that I've found useful over the years. Naturally, the requirements of your specific application will change how relevant or helpful any one of these techniques is for your work.

  • Reduce the plotted data.
    • Avoid plotting overlapped elements, especially those occluded by non-transparent patches, or lines with the same coordinates.
    • Avoid plotting non-visible elements such as those outside the axis limits or with their Visible property turned 'off'. The associated performance cost have been greatly reduced since R2014b. Nonetheless, it can still be helpful to avoid creating and plotting objects that aren’t visible.
    • Reduce your use of plot markers and choose simpler markers such as '.' where appropriate. Starting in R2016b you can use MarkerIndices to specify which data points display markers.
    • Reduce the number of plotted points and lines, especially when charts and images don’t need zooming. Tucker McClure's reduce_plot utility on the MATLAB Central File Exchange may help. Starting in R2016b, MATLAB automatically thins nonvisible data, but even then, you can sometimes do better reducing the data on your own.
    • Cast image data to uint8 before using image or imagesc to display the image. Processing integer RGB data images is much faster than floating-point RGB or indexed images.
  • Consider how you create and use graphics objects.
    • Set the axes properties to static values before plotting (e.g. xlim([0 10]). This avoids MATLAB dynamically computing these values based on the data during runtime.
    • Avoid using the axes function to make an axes current. Instead, set the figure's CurrentAxes property or pass the axes handle directly to the plotting function. Most plotting functions accept the axes handle as their first input parameter, and the rest accept the axes handle via the 'Parent' property.
    • Avoid creating legends or colorbars initially―let the user create them by clicking the corresponding toolbar icon. If you want to include a legend programmatically, consider creating one statically instead of using the legend command, which dynamically computes properties values based on the data during runtime.
    • Use low-level rather than high-level plotting functions – i.e., line instead of scatter/plot/plot3 or surface instead of surf.
    • Update object properties when possible instead of creating a new object. For example, avoid recreating line objects when you can just update the x and y data.
    • Explore ways to reduce the number of graphics objects by bundling graphics data in a single object. For example, create multiple lines with the same color using a single line object with individual lines separated by NaNs.
  • Consider use of drawnow
    • Only call drawnow once you've finished plotting. Calling drawnow within the plotting loop can have a horrendous performance impact. Let MATLAB decide when to render the plot. And if you need to plot in a loop, consider using drawnow limitrate.

Conclusions

MATLAB JIT technology continues to improve with each release, providing better performance without requiring user effort. But there will always be room for human insight to optimize performance, and we should not neglect this. Perhaps it's a matter of setting our expectations: MATLAB solves our equations and models, but we know we must invest time to properly set up the problem first. The MATLAB engine can do a lot to speed up our code, but it can do much better when we spend some time optimizing our code first.

Luckily, there are numerous ways in which we can improve MATLAB performance. In fact, there are so many ways to achieve our performance goals that we can take a pick based on aesthetic preferences and subjective experience: Some people use vectorization, others like parallelization, some others prefer to invest in smarter algorithms or faster hardware, others trade memory for performance or latency for throughput, still others display a GUI that just provides a faster impression with dynamic feedback. Even if one technique fails or is inapplicable, there are many other alternatives to try. Just use the profiler and some common sense, and you are halfway there. Good luck!

Note: The views shared in this article are the author’s own and do not necessary reflect those of MathWorks.

About the Author

Yair Altman is an independent MATLAB consultant and author of the book Accelerating MATLAB Performance. In over 25 years of professional software development, Yair has developed programs on numerous platforms and MATLAB releases. He is widely regarded in the MATLAB community as an expert on advanced MATLAB programming, and is a top contributor on MATLAB Central.

Published 2017 - 93118v00

View Articles for Related Capabilities