Apply AI-suggested optimizations

In the previous section, the agent identified three optimization opportunities:

  1. Replace std::abs with a squared-magnitude comparison to eliminate the sqrt in the hot path
  2. Replace std::complex<double> with raw double arithmetic to remove all complex operator overhead
  3. Build with -O3 to enable inlining, loop unrolling, and auto-vectorization

You can ask the agent to apply each change when it has access to your source and build environment. Otherwise, apply and rebuild the change through your normal remote development workflow. The Arm Performix MCP server profiles the configured target and analyzes saved runs; it doesn’t by itself provide remote source-editing or deployment tools. You’ll validate each change by asking the agent to compare the new profiling results against the previous run before moving on.

Note

The agent will typically surface these optimizations itself based on the profiling results, without you needing to prompt it explicitly. The following prompts are for explicit reference. You can use them if the agent hasn’t already proposed the change, or to direct it to a specific optimization.

Eliminate the sqrt in the escape check

The inner loop in Mandelbrot::getIterations calls std::abs(z) on every iteration to check whether the point has escaped. std::abs for std::complex<double> computes $\sqrt{re^2 + im^2}$ via hypotf64 — a full square root on every iteration. The escape condition $abs(z) > THRESHOLD$ is mathematically equivalent to $re^2 + im^2 > THRESHOLD^2$, so the square root is never needed.

Ask the agent to apply the fix, rebuild, and re-profile in one step. If the agent hasn’t already proposed this change, use the following prompt:

    

        
        
Replace the abs(z) > THRESHOLD escape check in
getIterations with a squared-magnitude comparison using a precomputed
threshold_sq = THRESHOLD * THRESHOLD. Rebuild the debug binary with
`make clean && make single_thread DEBUG=1`. Then use the Arm Performix MCP
server to re-run the Code Hotspots recipe on target "<target-name>" with
workload "/home/ec2-user/Mandelbrot-Example/build/mandelbrot_single_thread_debug".
Generate an AI insight for the new run and compare it with run ID "<previous-run-id>".
Has the proportion of samples in __complex_abs and hypotf64 changed?

    

Replace <target-name> and <previous-run-id> before sending the prompt. The agent runs the Code Hotspots recipe again and returns the comparison. The std::__complex_abs and hypotf64 symbols disappear from the hotspot list entirely. Both functions are gone because the squared-magnitude check never calls them.

The hotspot distribution shifts: getIterations drops from 28.5% to 18.4% self-time, and the freed CPU budget is now visible in std::complex operator symbols. The overall sample count is slightly lower, but the profile structure reveals that std::complex operator overhead is now the next bottleneck to address.

Replace std::complex<double> with raw double arithmetic

With hypotf64 and __complex_abs removed, the profile now shows std::complex operator symbols (operator+, operator*=, operator*, operator+=, __muldc3, __rep) collectively consuming the majority of CPU time. These are all function-call overhead: the debug build disables inlining, so every arithmetic operation on std::complex<double> dispatches through the C++ standard library machinery.

The fix is to replace std::complex<double> in getIterations with plain double variables for the real and imaginary parts. The Mandelbrot iteration $z_{n+1} = z_n^2 + c$ expands algebraically to:

$$re_{new} = re_z^2 - im_z^2 + re_c$$ $$im_{new} = 2 \cdot re_z \cdot im_z + im_c$$

The fix eliminates every std::complex method call from the inner loop. If the agent hasn’t already proposed this change, use the following prompt to direct it:

    

        
        
Rewrite the getIterations function in
src/mandelbrot_single_thread.cpp to use plain double variables zr and zi
instead of std::complex<double>, expanding z*z + c algebraically.
Rebuild with `make clean && make single_thread DEBUG=1`. Then use the Arm
Performix MCP server to re-run the Code Hotspots recipe on target
"<target-name>" with the same workload. Generate an AI insight for the new
run and compare it with run ID "<previous-run-id>". Have the std::complex
operator symbols disappeared from the hotspot list?

    

Replace the placeholders with the target name and the run ID from the previous step. The agent runs the Code Hotspots recipe and returns the comparison. Every std::complex function—__muldc3, operator*=, operator+=, operator+, operator*, __rep—is gone from the profile.

Total profile sample count drops from approximately 48,750 (baseline) to approximately 11,457, a reduction of about 76%.

Enable compiler optimizations with -O3

Both previous changes were applied to the debug binary, compiled with -O0 (no optimization). At -O0, the compiler doesn’t inline any function calls, which is why std::complex operators appeared separately in the profile even after the algorithmic fix.

Building with -O3 lets the compiler inline getIterations into draw, unroll the inner loop, and auto-vectorize the scalar double arithmetic using the Arm NEON/ASIMD unit.

Ask the agent to rebuild with the release target and re-profile. If it hasn’t already suggested this step, use the following prompt:

    

        
        
Rebuild the application without the DEBUG flag using
`make clean && make single_thread`, then run the Code Hotspots recipe on
target "<target-name>" with workload
"/home/ec2-user/Mandelbrot-Example/build/mandelbrot_single_thread". Generate
an AI insight for the new run and compare it with run ID "<previous-run-id>".
How have the hotspot distribution and total profile sample count changed?

    

Replace the placeholders before sending the prompt. The agent runs the Code Hotspots recipe on the new binary path and returns the result. The getIterations function no longer appears as a separate hotspot because the compiler has inlined it completely into draw. Total profile sample count drops to approximately 3,997, about 8% of the original baseline of approximately 48,750 samples. This is a sample-count reduction of approximately 92%.

The only remaining hotspot is Mandelbrot::draw itself at ~98.6% of samples, which now includes both the iteration and colorizing passes. The colorizing pass calls pow(255, hue) per pixel — visible as powf64 at ~0.7% — but this is a small fraction of total time at this scale.

What you’ve accomplished

You’ve now applied AI-suggested optimizations — such as replacing std::complex<double> with plain double arithmetic, enabling -O3 for compiler optimizations, and eliminating sqrt in the escape check — to the Mandelbrot application.

Across the three rounds of code changes, the profile sample count decreases from approximately 48,750 baseline samples to approximately 3,997, a reduction of about 92%. Each change is followed by another profile run so you can inspect how the hotspot distribution and sample count change.

StepProfile samplesSample-count reduction vs baseline
Baseline (-O0, std::complex, abs check)~48,7500%
After squared-magnitude check~47,535~2%
After raw double arithmetic~11,457~76%
After -O3~3,997~92%

Profile sample counts depend on the collection configuration and sampling conditions, so these reductions are profiling evidence rather than direct runtime measurements. Measure elapsed time separately under controlled conditions before reporting a runtime speedup.

The same pattern applies to any C++ application on Arm Neoverse. Run the Code Hotspots recipe to locate the hottest functions, let the agent cross-reference the source, apply the suggested changes, and re-profile to confirm. This evidence-driven loop is faster and less error-prone than manual profiling because the AI maintains context across all steps and keeps the profiling data visible alongside the code throughout.

Back
Next