04 · Performance Profiling & Optimization¶
Verification note
MATLAB was not available in the environment used to write this page. Profiler output structure and optimization techniques below are documented, version-stable MATLAB behaviors, hand-traced against the MATLAB documentation and general numerical-computing performance principles rather than measured in MATLAB itself; any timing numbers are illustrative orders of magnitude, not benchmarks.
Guessing where code is slow is unreliable — the actual bottleneck is often not where intuition points. This module covers MATLAB's Profiler for finding real bottlenecks, and the concrete optimization techniques (vectorization, preallocation, avoiding unnecessary copies) that typically fix what the Profiler finds.
The Profiler¶
The Profiler report ranks functions by self time (time spent in the function's own code, excluding calls to other functions) and total time (self time plus everything called underneath it) — self time is usually the more actionable number, since it points to the specific lines doing the actual work, rather than a wrapper function that merely calls something slow.
p = profile('info'); % programmatic access instead of the GUI
[~, idx] = sort([p.FunctionTable.TotalTime], 'descend');
for i = 1:5
fprintf('%s: %.3f s\n', p.FunctionTable(idx(i)).FunctionName, p.FunctionTable(idx(i)).TotalTime);
end
tic/toc for targeted measurement¶
tic;
resultA = approachA(data);
fprintf('Approach A: %.4f s\n', toc);
tic;
resultB = approachB(data);
fprintf('Approach B: %.4f s\n', toc);
For comparing two specific implementations of the same task,
tic/toc around each is faster to set up than the full Profiler and
sufficient when you already suspect which function is the bottleneck —
use the Profiler first to find the bottleneck across a whole program,
then tic/toc to compare candidate fixes for that specific
bottleneck.
% average over several runs — single-run timing is noisy (OS scheduling, JIT warm-up)
n = 100;
times = zeros(1, n);
for i = 1:n
tic;
approachA(data);
times(i) = toc;
end
fprintf('Median: %.4f s, std: %.4f s\n', median(times), std(times));
The first call to a function is often slower than subsequent calls (MATLAB's JIT compiler optimizes hot code paths after repeated execution) — always discard or account for a "warm-up" run when comparing timings.
Preallocation: the single biggest common fix¶
% BAD: array grows by one element every iteration
result = [];
for i = 1:10000
result(end+1) = i^2; % MATLAB reallocates and copies the WHOLE array, every iteration
end
% GOOD: allocate the final size up front
result = zeros(1, 10000);
for i = 1:10000
result(i) = i^2; % writes into existing memory, no reallocation
end
Growing an array one element at a time is quadratic overall (each growth step copies everything so far), even though each individual step "looks" cheap — for 10,000 elements this is the difference between roughly 10,000 reallocation-and-copy operations totaling tens of millions of element copies versus 10,000 direct writes into pre-existing memory. MATLAB's Code Analyzer (the editor's built-in linter) flags this pattern with an explicit "array is growing on every loop iteration" warning precisely because it's such a common, significant, and easy-to-fix cost.
The same principle applies to growing cell arrays, struct arrays, and appending rows to a table — preallocate whenever the final size is knowable before the loop starts.
Vectorization¶
% loop version
n = 1000000;
x = rand(1, n);
y = zeros(1, n);
for i = 1:n
y(i) = sin(x(i)) * cos(x(i));
end
Vectorized code avoids MATLAB interpreter loop overhead (bytecode
dispatch on every iteration) by delegating the actual work to
MATLAB's underlying compiled array-operation routines, which process
whole arrays with much less per-element overhead. For simple
elementwise math like this, vectorization is typically the single
largest speed-up available without leaving MATLAB entirely (larger than
parfor's multi-core speed-up, and compatible with combining both).
% logical indexing instead of a loop with an if-check
x = randn(1, 100000);
% BAD:
result = zeros(size(x));
for i = 1:numel(x)
if x(i) > 0
result(i) = x(i)^2;
end
end
% GOOD:
result = zeros(size(x));
positive = x > 0;
result(positive) = x(positive).^2;
Avoiding unnecessary copies¶
MATLAB uses copy-on-write for arrays — assigning b = a doesn't
actually copy data until either a or b is modified, at which point
MATLAB makes a private copy for whichever one is mutated. This is
usually invisible and beneficial, but a few patterns defeat it:
function modifyInPlace(x)
x(1) = 0; % triggers a private copy inside this function — caller's array is untouched
end
a = [1 2 3];
modifyInPlace(a);
disp(a); % still [1 2 3] — the function's local copy was modified, not the caller's
Passing a large array to a function that modifies it always triggers a
copy — if a function needs to mutate large data in place across many
calls (e.g. an iterative solver updating a big matrix each step), a
handle class (Module 01, Level 3) avoids the repeated copy, since
handle objects are passed by reference.
Function handles vs. eval / string-based dispatch¶
% SLOW and fragile: constructing and evaluating code as a string
funcName = 'sin';
y = eval([funcName '(x)']);
eval forces MATLAB to re-parse and interpret a string as code on every
call — far slower than a function handle, which is resolved once. Aside
from performance, eval on any string derived from external input is
also a code-injection risk; str2func/direct handles (@sin) are both
faster and safer wherever dynamic function dispatch is needed at all.
Preallocating and reusing figure/plot handles¶
% BAD: recreating a figure every iteration inside a live-update loop
for i = 1:100
figure;
plot(data(:,i));
pause(0.05);
end
% GOOD: create once, update data in place
h = plot(data(:,1));
for i = 1:100
set(h, 'YData', data(:,i));
drawnow limitrate; % throttles rendering, avoids redrawing faster than the screen refresh
end
drawnow limitrate matters for animation loops specifically — without
it, MATLAB may attempt to render every single frame regardless of how
fast the loop produces them, wasting time on frames the display can't
even show.
Combining techniques: a realistic before/after¶
% BEFORE: unvectorized, growing array, eval-based dispatch
function result = processBad(data, opName)
result = [];
for i = 1:length(data)
result(end+1) = eval([opName '(data(i))']);
end
end
% AFTER: preallocated, vectorized, function handle
function result = processGood(data, opFunc)
result = opFunc(data); % opFunc = @sin, or any vectorized function handle
end
The "after" version isn't just faster — it delegates the entire
computation to a single vectorized call, sidesteps eval entirely, and
removes the preallocation concern altogether by not looping at the
MATLAB level at all.
How It Actually Works¶
MATLAB's profile/Profiler works by instrumenting the interpreter's
execution, not by external sampling of the CPU — it hooks into the same
statement-by-statement dispatch loop covered in Module 03 (Level 2),
recording entry/exit timestamps and call counts for every function and,
optionally, every line, then aggregates that into "self time" (time spent
in a function excluding its callees) versus "total time" (including
callees). This instrumentation itself adds overhead, which is why
profiled run times are always somewhat inflated relative to un-profiled
execution — the profiler is a measurement tool for relative hot-spot
identification, not for absolute wall-clock benchmarking (that's what
tic/toc or timeit, which avoid instrumentation overhead, are for).
timeit specifically exists to work around a subtlety of MATLAB's JIT
compiler (Module 01, Level 1): a function's first call can include JIT
warm-up/compilation cost that later calls don't pay, so a single
tic/toc around one call can measure compilation time rather than
steady-state execution time — timeit runs the target function multiple
times, discards early "warm-up" iterations, and reports a median of the
steady-state timings specifically to give a number that reflects how the
function behaves once compiled, not its one-time startup cost.
Memory profiling reveals the same copy-on-write behavior from Module 02 (Level 1) from a different angle: a function that appears to only "read" a large array but subtly triggers a write somewhere (even an in-place-looking operation MATLAB can't safely alias) forces the deferred copy to finally happen, showing up as a memory-allocation spike the profiler can attribute to that specific line.
Note: based on MathWorks' documented profiler instrumentation and
timeit methodology; not executed in a real MATLAB session, which is
unavailable in this environment.
Practice¶
- Profile (conceptually — describe what you'd expect to see) a function that loads a large CSV inside a loop that runs 1000 times versus loading it once outside the loop; explain which line the Profiler's self-time ranking would surface as the bottleneck.
- Rewrite a hypothetical function that builds a growing
results{end+1} = ...cell array inside a 50,000-iteration loop to preallocate the cell array first, and explain the asymptotic difference in total copy operations. - Given
y(i) = x(i)^2 + 3*x(i) - 1inside aforloop over a 200,000-element vector, write the fully vectorized equivalent and explain why it avoids interpreter dispatch overhead per element. - Explain a scenario, using the copy-on-write behavior described above,
where switching a large-data-mutating function's class from a value
class to a
handleclass would measurably reduce memory churn, and what caution that introduces (referencing Level 3 Module 01's discussion of shared mutable state).