# Optimize code with AI-driven profiling feedback

## In this learning path

- [Introduction](https://learn.arm.com/learning-paths/servers-and-cloud-computing/performix-mcp-agent/)
- [Understand AI-driven profiling with the Arm Performix MCP server](https://learn.arm.com/learning-paths/servers-and-cloud-computing/performix-mcp-agent/1-overview/)
- [Build the Mandelbrot example on Arm Neoverse](https://learn.arm.com/learning-paths/servers-and-cloud-computing/performix-mcp-agent/2-setup/)
- [Run Code Hotspots with an AI agent](https://learn.arm.com/learning-paths/servers-and-cloud-computing/performix-mcp-agent/3-run-hotspot/)
- [Optimize code with AI-driven profiling feedback](https://learn.arm.com/learning-paths/servers-and-cloud-computing/performix-mcp-agent/4-optimize/)
- [Next Steps](https://learn.arm.com/learning-paths/servers-and-cloud-computing/performix-mcp-agent/_next-steps/)

## 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{r^2 + i^2}\) via `hypotf64` — a full square root on every iteration. The escape condition \( \text{abs}(z) > T \) is mathematically equivalent to \( r^2 + i^2 > T^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 expands algebraically to:

\[
z_{n+1} = z_n^2 + 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.

| Step                                                         | Profile samples | Sample-count reduction vs baseline |
|--------------------------------------------------------------|-----------------|-----------------------------------|
| Baseline (`-O0`, `std::complex`, `abs` check)               | ~48,750         | 0%                                |
| 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.
