Matlab Optimize Performance
MathWorksOfficialCreated by MathWorks
- Skill
- Workflow
- Software development
- BSD-2-Clause
- No issues found
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a rigorous, data-driven methodology for identifying and resolving performance bottlenecks in MATLAB. It emphasizes profiling before modification, verifying performance gains through ablation, and ensuring numerical correctness throughout the optimization process. The workflow is designed to move beyond guesswork by using precise timing tools and established optimization patterns, ensuring that reported speedups are genuine and reproducible.
What it does
- Establishes performance baselines using timeit for accurate measurement.
- Identifies bottlenecks using the Sampling Profiler or standard MATLAB profiler.
- Provides a catalog of proven optimization patterns like vectorization and preallocation.
- Validates performance gains through ablation and interleaved paired timing.
- Ensures numerical correctness with tolerance-scaled comparisons.
- Generates structured performance reports for documentation and verification.
When to use it
- When MATLAB code execution is too slow and requires optimization.
- When you need to identify specific lines or functions causing performance degradation.
- When you need to benchmark or compare the performance of different code versions.
- When you need to verify that an optimization maintains numerical accuracy.
Advantages
- Reduces reliance on intuition by using empirical profiling data.
- Prevents false optimizations through the ablation measurement protocol.
- Ensures code remains correct while becoming faster.
- Provides a standardized approach for reporting performance improvements.
Usage examples
- “My MATLAB script is running slowly. How can I find the bottleneck?”
- The agent will guide you through establishing a baseline with timeit and using the Sampling Profiler to identify the specific lines of code consuming the most execution time.
- “I think vectorizing this loop will speed up my code. How do I verify it?”
- The agent will help you implement the vectorized version, perform an ablation test to confirm the speedup, and run a correctness check to ensure the output matches the original loop.
Install
1 of its files are not mirrored here, so install it from its source:
https://github.com/matlab/matlab-agentic-toolkit/tree/2a7f215e965597acf6ed495beb9e7b8aff45a3be/skills-catalog/matlab-software-development/matlab-optimize-performance
Documentation by MathWorks
MATLAB Performance Optimization Workflow
Systematic 7-step workflow for finding and fixing performance bottlenecks in MATLAB code.
When to Use
User asks to speed up MATLAB code, or to optimize it for execution time
User wants to find why their MATLAB code is slow
User has a function or script that takes too long to run
User asks to benchmark or time MATLAB code in the current session (a one-off timing or an A/B of two versions)
User wants to compare performance before and after a change
User asks about MATLAB performance best practices
When NOT to Use
Optimizing Simulink model simulation speed (use Simulink Profiler)
Speeding code up by compiling it with MATLAB Coder (
codegen, MEX), or tuning generated MEX (use thematlab-generate-codeskill)The problem is memory, not speed: out-of-memory errors or peak memory (use the
matlab-optimize-memoryskill)The performance issue is purely I/O-bound (file reads, network, database)
User wants to write performance tests, or track timings across builds or in CI (use the
matlab-write-performance-testsskill)Note: for exact invocation counts, or a sub-10 ms / MEX / parallel workload, use the MATLAB profiler (
profile) rather than the Sampling Profiler (Step 2) — that's a profiler choice, not a reason to skip the skill
The 7-Step Workflow
Step 1: Establish Baseline
Measure current performance so you have a number to improve against:
f = @() targetFunction(input1, input2);
baseline = timeit(f); % handles warm-up and repeats; prefer it to tic/tocFor GPU code use gputimeit. For a script, run it once to warm up, then time a second run with tic/toc. Templates: assets/measurement-templates.md.
Step 2: Profile and Analyze
Find where the time actually goes — don't guess, profile.
Pick the profiler first: check, don't assume. In R2026b+ the Sampling Profiler is the default: a
low-overhead statistical profiler that samples the execution thread at ~100 Hz. This skill's
sampling_profiler helper drives it, and sampling_profiler('available') tells you whether this MATLAB
has it. Use the MATLAB profiler (profile) only when available is false, when you need exact call
counts, or for a one-shot call too short to loop, or MEX or parallel code.
% Sampling Profiler — use when sampling_profiler('available') is true (clean up after)
addpath(fullfile(skillRoot, "scripts")); % skillRoot = directory containing this SKILL.md
assert(sampling_profiler('available')); % false -> use the MATLAB profiler block below
nReps = 1; % raise until the loop below runs several seconds (~10 s is ideal)
sampling_profiler('on');
sampling_profiler('pause');
targetFunction(input1, input2); % warm-up: pays first-call cost, not collected
sampling_profiler('clear');
sampling_profiler('resume');
for r = 1:nReps, targetFunction(input1, input2); end
[~, stacks] = sampling_profiler('off');
sampling_profiler('destroy'); % mandatory cleanup: skipping it corrupts later runs% MATLAB profiler — any release; use when the Sampling Profiler isn't available
profile on; targetFunction(input1, input2); profile off; profile viewer;Reading the sampler:
Run several seconds; ~10 s is ideal (~1000 samples; ~100 is the bare minimum). Loop a quick workload by raising
nReps, and keep the warm-up call. If it can't be looped, useprofile.The counts describe the profiled run (
stacks.TotalSamples/100is close to its wall clock) and include no invocation counts. For how many times something ran, useprofile, and record"call_count_source": "builtin_profile_numcalls","sampling_can_count_invocations": falseand"sampling_profiler_samples"inperf_report.json.Neither profiler sees inside a builtin or P-coded frame: the time lands on the call site with no line detail. Confirm such a hotspot by removing it and re-timing (Step 5).
Work from the profile to a change:
Rank functions by self time, then read the lines inside the top one. Per-line time includes callees, so a line that only makes a call can outrank the line doing the work.
Before changing anything, say from the code why that line is slow: a real mechanism (a reallocation in a loop, a builtin on a large array), not just a big percentage.
Step 5 is the proof: if re-timing doesn't move the clock, your explanation was wrong; profile again.
For the full Sampling Profiler API, results model, traps, and helpers, see references/sampling-profiler.md.
Step 3: Identify Optimization Opportunities
Based on profiling results, identify which patterns apply. Read references/optimization-patterns.md for the full catalog.
| Pattern | Typical Speedup | Look For |
|---|---|---|
| Vectorization | 2–200x | Loops doing element-wise math on arrays |
| Preallocation | 2–100x | Arrays growing inside loops (x = [x; newRow]) |
| Unnecessary recomputation | 2–50x | Same expensive expression computed multiple times |
discretize/histcounts | 2–50x | Loops binning or classifying data |
| Persistent caching | 1.5–95x | Repeated load() or expensive object creation |
| Logical indexing | 1.2–5x | Using find() just to index into an array |
arguments block | 1.1–1.8x | Functions using inputParser |
| Algebraic simplification | 1.5–3x | Redundant sqrt, abs, or matrix ops |
Before optimizing, verify the target is worth it:
Is it called in a tight loop? High call count × small time = big total.
Is it M-code or a built-in? You can't make a built-in faster, but you can often call it fewer times (e.g., pass a matrix to
filtfilt/filterinstead of looping over columns).
Step 4: Implement Optimizations
Apply the patterns from Step 3, starting with the highest-impact one. Before/after examples are in references/optimization-patterns.md.
Move invariant work out of loops (object creation, option parsing, constant expressions)
Replace element-wise loops with array operations where possible
Use purpose-built functions (
discretize,cumsum,hypot) instead of hand-written equivalentsFor large data, batch the vectorization to control memory (see Pattern 9 in catalog)
Try each new variant on a small slice of the input first: a wrong orientation in implicit expansion can build a huge array
Step 5: Measure Optimized Performance
Quick check, while you are still iterating: re-time with timeit as in Step 1 and compare with the baseline. A speedup of 1.2x or more is considered significant; below that, measurement noise makes it hard to be confident the change helped.
Verified result — required whenever the user asks for a number to report, commit, or share, when they gave you a hypothesis about what is slow, or when two profilers disagree. Follow references/measurement-protocol.md in full:
Measure the ceiling before optimizing — delete or hoist the suspected work, re-time end-to-end, and compute what fraction of total time it actually accounted for; profiler attribution has been observed to overstate it by more than 10x. (This removal-and-re-time step is called ablation, and is recorded as
adjudication_method: "removal_ablation".)Correctness gate against the original in the same session, with a tolerance scaled to the output (
100*eps(max(abs(original(:))))), plus an independent witness statistic of the output.Mutation test — perturb the input; the output must change. Catches code that is fast because it stopped doing the work.
Interleaved paired timing, one session, N ≥ 5,
median(tb ./ to). Never compare across sessions or in sequential blocks — machine drift lands entirely on one arm.Write
perf_report.json— every field from a variable a run of the current code computed, never a typed-in literal. Run each step in its own tool call andsaveits results to a.matas it finishes, so a fix to the report re-writes it from those files instead of re-running the steps.
The headline speedup_median is always end-to-end. A sub-kernel number goes in kernel_speedup. A refuted hypothesis is a result: report it with the honest speedup, including 1.0.
Step 6: Verify Correctness
Every optimization must produce the same results as the original:
original = originalFunction(input1, input2);
fast = optimizedFunction(input1, input2);
% Scale the tolerance to the output — a fixed 1e-10 is wrong in both directions
tol = 100 * eps(max(abs(original(:))));
maxErr = max(abs(original(:) - fast(:)));
fprintf('Max error: %.2e (tol %.2e)\n', maxErr, tol);
assert(isequal(isnan(original), isnan(fast)) && maxErr <= tol, 'Results differ beyond tolerance!'); % max skips NaNFor non-numeric outputs:
assert(isequal(original, fast), 'Results differ!');Error at or below eps(max(abs(original(:)))) is floating-point reordering noise — acceptable; document it. Error orders of magnitude above it is a bug, however clean the algebra looked. Watch for catastrophic cancellation: an algebraically exact shortcut (e.g. the running-sums variance formula) can be dramatically faster and still wrong on data with a large mean offset. Subtract the mean first, then check the result against the tolerance again.
An error of exactly 0 proves nothing on its own. Also record an independent statistic of the optimized output (min(fast(:)), a checksum) — see references/measurement-protocol.md.
Step 7: Report Results
Report the baseline, the optimized time, the speedup (for each change and end to end), the max error, and the pattern applied. If you followed the verified-result protocol in Step 5, perf_report.json is the deliverable and this is its human-readable summary.
Key Rules
Always profile before optimizing — never guess where the bottleneck is
Measure the ceiling before you optimize — remove or hoist the suspect and re-time end-to-end; that difference, not the profiler's
%column, is the ceiling on what a rewrite can win (this step is ablation)One change at a time — measure after each optimization to know what helped
Verify correctness — every optimization must produce equivalent output, with a tolerance scaled to the output
Report end-to-end — the headline number is the whole workload; a sub-kernel speedup is reported separately and labelled as such
Every reported number comes from a variable — computed by a run of the current code, never typed in from memory
1.2x threshold — speedups below 1.2x are not reliably distinguishable from noise
GPU timing — always
wait(gpuDevice)before and after timing GPU codeUse
timeit— it handles warmup and averaging; avoid rawtic/tocfor benchmarksOn R2026a,
clearwhat you profiled before you time anything — code run underprofileon R2026a stays ~100x slower for the rest of the session until youclearit, so a baseline taken after a profiled run flatters every "after" number. R2026a also has no Sampling Profiler.
Common Mistakes
| Mistake | Why It's Wrong | Do This Instead |
|---|---|---|
| Growing arrays in loops | Each append copies the entire array | Preallocate before the loop |
| Vectorizing huge arrays blindly | May exceed memory | Use chunked processing for large data |
| Assuming faster = correct | Bugs can make code fast (by skipping work) | Verify results match (Step 6) + mutation test |
Fixed assert(maxErr < 1e-10) | Rejects clean results on large-magnitude data; passes broken ones on small | Scale tolerance to the output: 100*eps(max(abs(original(:)))) |
| Quoting a sub-kernel speedup as the headline | 8x on 20% of runtime is ~1.2x end-to-end | Headline is end-to-end; kernel number reported separately |
| Manufacturing a win when the hypothesis was wrong | A refuted hypothesis is a valid result | Report speedup_median honestly, including 1.0 |
Reference Files
references/measurement-protocol.md— the verified-result protocol for Step 5 and everyperf_report.jsonfieldreferences/sampling-profiler.md— Sampling Profiler (R2026b+): commands, results model, traps, and thescripts/helper indexreferences/optimization-patterns.md— the pattern catalog, with before/after code and measured speedupsassets/measurement-templates.md— templates for atimeitbaseline, GPU timing, and a scaling testscripts/— MATLAB helpers for the Sampling Profiler;addpaththis folder to use them
Copyright 2026 The MathWorks, Inc.
Files
- ATTRIBUTION.mdAdded by AutoGineer1.1 KiB
- LICENSELicence file added by AutoGineer1.4 KiB
- SKILL.md13.0 KiB
- assets/measurement-templates.md1.9 KiB
- manifest.yaml554 B
- references/measurement-protocol.md14.7 KiB
- references/optimization-patterns.md7.4 KiB
- references/sampling-profiler.md11.1 KiB
- scripts/annotate_source_lines.m3.9 KiB
- scripts/assertSamplingProfiler.m913 B
- scripts/compare_profiles.m9.0 KiB
- scripts/filter_test_frames.m2.3 KiB
- scripts/find_hotspots.m2.5 KiB
- scripts/make_ab_commands.m4.2 KiB
- scripts/print_hotspot_report.m3.9 KiB
- scripts/profile_function.m4.4 KiB
- scripts/profile_script.m2.7 KiB
- scripts/profile_test.m5.4 KiB
- scripts/quick_profile.m2.6 KiB
- scripts/sampling_profiler.p(opens on GitHub)Not mirrored: opens on GitHub587 B
references/sampling-profiler.md
Sampling Profiler Reference (R2026b+)
Deep-dive material for Step 2 of the optimization workflow. The Sampling Profiler is a
statistical profiler that samples the single-threaded MATLAB execution stack at a fixed 100 Hz,
giving per-line and per-function time attribution at low overhead (0–12% typical; deep recursion up
to ~60%). Use the MATLAB profiler (profile) instead on releases before R2026b.
Prerequisite — R2026b+. The Sampling Profiler is available only in MATLAB R2026b or newer. This skill's
sampling_profilerhelper drives it —addpaththis skill'sscripts/folder, then probe availability with:hasSP = sampling_profiler('available'); % true/false; never errorsFall back to the MATLAB profiler (
profile) whenhasSPisfalse. Don't probe withexist— the helper file is on the path on every release, soexistsays yes even where the profiler can't run.
When to use (and when not)
Use it to find hotspots, compare two implementations (A/B), or attribute time to source lines at
low overhead. The code you profile must be called after 'on' — a function or script you call,
not code typed at the prompt or written inline next to 'on' (see Call the workload below).
Do NOT use it to:
count function invocations (there is no invocation count — use
profile)time a single short call (<~10 ms gets 0–1 samples) — loop it, or use
timeitprofile parallel workers (
parfor/parfeval/spmdrun off the sampled thread)see inside builtins or MEX (their time lands on the calling line)
The protocol: warm up, loop to enough samples, then clean up
Warm up first: one untimed call while the profiler is armed but paused pays the first-call cost
(JIT, first use of a class or file) without sampling it, and 'clear' discards anything left over.
Looping alone does not do this — measured on R2026b, first-call cost was 16–23% of samples at
nReps = 1 and still 2–4% at nReps = 10. If the code takes different paths for different
inputs, warm up on each kind of input the measured run will see; warming up after 'on' leaves
slightly less first-call cost than warming up before it. Then raise the repetition count until the
profiled loop runs several seconds — ~10 s ≈ 1000 samples is the sweet spot; ~1 s / ~100 samples
is the bare minimum and only resolves a single dominant hotspot.
nReps = 1; % raise until the loop below runs several seconds (~10 s ideal)
sampling_profiler('on');
sampling_profiler('pause'); % armed, not sampling
myBenchmark(args); % warm-up: first-call cost, not collected
sampling_profiler('clear'); % discard anything from the warm-up
sampling_profiler('resume'); % start collecting for real
tid = tic;
for r = 1:nReps, myBenchmark(args); end % measured run — a CALLEE, see below
wallTime = toc(tid); % saved timer ID: inner tic/toc can't corrupt it
[~, stacks] = sampling_profiler('off');
sampling_profiler('destroy'); % mandatory cleanup — skipping it corrupts later runsIf the workload can throw, make 'destroy' run anyway (onCleanup or catch), as the helpers do.
Call the workload, don't inline it. Only frames entered after 'on' are sampled. A loop body
written inline in the function that called 'on' is already on the stack, so its ticks all land in
the idle count and you get zero usable data. myBenchmark above,
quick_profile(@() myCode(args)) and profile_script('myScript.m') are all correct for this reason.
Commands
All commands go through sampling_profiler(command):
| Command | Description |
|---|---|
'available' | true if the Sampling Profiler can run on this release, else false; never errors |
'on' | Start the profiler and begin collecting |
'off' | Stop collecting and return data: [~, stacks] = ...('off') |
'pause' | Stop the sample clock but keep the profiler active |
'resume' | Resume collecting after pause |
'clear' | Discard all collected samples (profiler stays active) |
'destroy' | Tear down the profiler and release its resources (mandatory cleanup) |
'data' | Non-destructive peek: [~, stacks] = ...('data') |
'status' | Char string: 'on' | 'paused' | 'off' (lightweight state check) |
'info' | Struct: State, Collecting, NumFiles, HasTimer, TimerRunning, TotalSamples, SnapshotsBuffered |
Nothing is configurable — extra arguments are ignored. Fixed on R2026b: 100 Hz, at most 127 frames per snapshot, and a rolling buffer of the last ~120,000 snapshots (~20 min).
Results data model
The stacks struct returned by 'off' or 'data':
stacks.TotalSamples— total timer ticks (including idle).stacks.Snapshots— array of captured stack snapshots. Each hasTimestamp,Depth, andFrames(1:Depth):Frames(f):FileName(full path),StartLine/EndLine,StartColumn/EndColumn. There is NOFunctionNamefield — usefileparts(FileName); distinguish local functions by line range.Frames(Depth)= leaf frame (self time). Every distinctfile:linein the snapshot, counted once, gets inclusive time.
Implied time: stacks.TotalSamples / 100. For a correctly measured run this ≈ toc(tid),
independent of thread count. If it is far off, distrust the capture and re-run — unless you paused
around a wait, which makes implied < wall by design. It checks that the capture worked, not that the
code was warm: a cold run also reads ≈ wall.
Idle samples: TotalSamples - numel(stacks.Snapshots) = ticks attributed to no frame — usually
the inline-body trap above, or time at the prompt. A high idle fraction means "check frame
position", not "my code was blocked".
Analyzing results
Report wall time (
toc(tid)), implied time (TotalSamples/100), and total samples; verify implied ≈ wall.Rank functions by self time, then read the lines inside the top one (SKILL.md Key Rule 13).
find_hotspotsranks lines, so sumhot.samplesper function first (hot.file; split local functions by line range).Before claiming a bottleneck, check the hot line against the blind spots below — describe where the time really goes, not the frame label.
State uncertainty for any hotspot under ~20 samples — that is noise. Two sites whose counts are within noise of each other are a tie — report them as one, don't rank them.
Blind spots and traps
Builtins, MEX and blocking waits (
sort,eig, MATLAB'spause) have no frame of their own: the time is real and lands on the calling line as self-time. To leave a known wait out, bracket it with the profiler's'pause'/'resume'.Anonymous functions and
feval/arrayfun/cellfun— no frame of their own; the time lands on the calling line, and the call-graph shape through them is wrong.P-coded files (
ver,verLessThan, first-usedatetime/readtable/figure) — a.pwith no.mreports a file-level total at line 0 (measured: avercall on a hot path put 95.7% of samples in the P-file). The time is real and unactionable: confirm it by removing the call and re-timing (Step 5), profile the pure-compute code in isolation, and don't profile plotting code.A per-iteration allocation can read high on a hot loop. The genuine hotspot still dominates a well-sampled run; confirm a suspicious per-line split with removal ablation (Step 5).
Local function shadows a file — in-file calls resolve to the local function; map frames by the file's function line ranges.
Deep stacks (>127 frames) keep the innermost 127 (leaf kept, root dropped): at
Depth == 127,Frames(1)is a mid-stack frame, not the entry point.Runs over ~20 min overwrite the oldest snapshots;
TotalSamples > numel(Snapshots)with no idle explanation means the buffer wrapped — split the region.@() run('script.m')fails ("Attempt to add variable to static workspace") — useprofile_script.
A/B comparison and tests
Profile each version with profile_function (saves a .mat), using fixed work both sides
(for i=1:N, never while toc<T), then diff with compare_profiles. That compares where the
time went — relative shares, never raw sample counts (overhead and totals differ per config); the
speedup itself comes from Step 5's interleaved timing. Never run two profiling sessions at once.
make_ab_commands('implA(data)', 'implB(data)', 'Setup', 'data = …;') prints the two fresh-session
matlab -batch commands plus the matching compare_profiles call.
For matlab.unittest / matlab.perftest tests, use profile_test (runs withNoPlugins), then
filter_test_frames(stacks, 'tMyTest') — or compare_profiles(..., 'FilterTest', ...) — to drop
framework-only samples.
Script index — which script when
addpath this skill's scripts/ folder first. Fastest start: quick_profile(@() myCode(args)) —
it makes one measured call, so that call must already run several seconds; otherwise use
profile_function(@() myCode(args), 'MinTime', 10), which loops to ~10 s.
| Call → output | Use it for |
|---|---|
sampling_profiler(cmd): 'available' → logical; [~, stacks] = sampling_profiler('off'); 'status' → 'on'/'paused'/'off' | Driving the Sampling Profiler yourself (commands above) |
assertSamplingProfiler() → errors with the profile fallback if the Sampling Profiler is missing | A guard before any helper below |
[stacks, hot] = quick_profile(@() f(x), topN) → prints the top-N report | One measured call that already runs several seconds |
results = profile_function(@() f(x), 'MinTime', 10, 'Tag', 't') → .stacks, .baselineTime, .profileTime, .resultFile (sp_results/t.mat) | Short workloads (loops to MinTime s), a baselined run, or a saved result |
[stacks, hot] = profile_script('myScript.m', topN) → prints the report | A .m script file (can't use @() run(...)) |
results = profile_test('tMyTest.m', 'Tag', 't') → same fields as profile_function | matlab.unittest/perftest files, run withNoPlugins |
hot = find_hotspots(stacks, topN) → struct array location, file, line, samples, selfPct | Acting on the hotspots in code (prints nothing) |
print_hotspot_report(stacks, maxEntries) → prints self %, counts and caller chains | A human-readable summary |
compare_profiles('a.mat', 'b.mat', 'Label0', 'A', 'Label1', 'B') → prints, writes comparison_report.md next to a.mat | Diffing two saved results |
filtered = filter_test_frames(stacks, 'tMyTest') → stacks without framework-only samples | Removing test-framework overhead |
annotate_source_lines(stacks) → prints sampled lines with counts and 2 lines of context | Line-level annotation of hot files |
make_ab_commands('f(x)', 'g(x)', 'Setup', 'x = ...;') → prints two matlab -batch commands and the compare_profiles call | A/B across two fresh sessions |
Copyright 2026 The MathWorks, Inc.
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
No issues found by our static scan· No issues found
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- MathWorks
- Publisher
- MathWorks (Official)
- Source
- matlab/matlab-agentic-toolkit · skills-catalog/matlab-software-development/matlab-optimize-performance · commit 2a7f215
- Licence
- BSD 2-Clause License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE.md)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by MathWorks or MathWorks.
Get it from the source
1 of this skill’s files are not mirrored here, so there is no ZIP. Get the complete skill from its source: matlab/matlab-agentic-toolkit