keepcalculator

06 Numbers and Powers

How to Profile and Optimize Code for HPC

Card No. 06·55415 · Updated · Standard formula, shown below

This guide walks you through profiling and optimizing code for high-performance computing (HPC). You’ll learn how to identify performance bottlenecks, apply targeted optimizations, and verify improvements. By the end, you’ll have a repeatable workflow that turns slow code into efficient, scalable HPC-ready applications.

What You’ll Need

  • A working HPC system or a multi-core Linux machine (for tools like perf, gprof, or Intel VTune).
  • A compiler suite (GCC, Clang, or Intel oneAPI) with optimization flags enabled.
  • Your source code, preferably in C, C++, or Fortran (though Python with profiling tools like cProfile also works).
  • Basic familiarity with the command line and your code’s execution flow.

Step-by-Step

  1. Establish a baseline. Compile your code with default settings (no optimization flags) and measure its runtime on a representative input. Use time or a job scheduler’s timing. Record this number—you’ll compare every change against it.
  2. Enable compiler optimizations. Recompile with -O2 (or -O3 if you’re confident). This alone often yields a 10–50% speedup. For HPC, also consider -march=native to use your CPU’s specific instruction set (e.g., AVX-512).
  3. Profile with a sampling profiler. Run perf record ./your_program followed by perf report to see which functions consume the most CPU time. Focus on the top 5–10 functions—these are your hotspots.
  4. Drill into hotspots. For a single function, use perf annotate to see which lines are expensive. Look for loops, memory accesses, and function calls that dominate.
  5. Check for algorithmic inefficiencies. If a hotspot is a loop, ask: Is this O(n²) when it could be O(n log n)? Could a hash map replace a linear search? Often, a better algorithm beats micro-optimizations.
  6. Optimize memory access patterns. HPC performance is often limited by memory bandwidth. Ensure your loops iterate over contiguous data (e.g., row-major order in C). Use #pragma omp simd or compiler auto-vectorization flags to vectorize loops.
  7. Add parallelization. If you have independent loops, use OpenMP (#pragma omp parallel for) to distribute work across cores. For distributed memory, consider MPI. Start with a small number of threads/processes and scale up, measuring speedup each time.
  8. Reduce communication overhead. In MPI, minimize the number of messages and their size. Use non-blocking communication (MPI_Isend/MPI_Irecv) to overlap communication with computation.
  9. Use profiling tools for I/O. If your program reads/writes large files, use strace -c or perf stat to see system call times. Consider using parallel I/O libraries like HDF5 or NetCDF.
  10. Re-profile after each change. After applying an optimization, re-run the profiler and compare the time. If a change doesn’t help, revert it. Keep a log of what you tried and the resulting speedup.
  11. Test correctness. Optimizations can introduce bugs. Run your test suite after every change to ensure results are still correct (within floating-point tolerance).
  12. Scale up and measure efficiency. Run your optimized code on 1, 2, 4, 8… cores. Plot speedup vs. core count. If speedup plateaus, you have a serial bottleneck or communication overhead. Aim for near-linear speedup.

Common Mistakes

  • Optimizing without profiling. Guessing which part is slow wastes time. Always profile first.
  • Ignoring compiler warnings. Warnings often hint at missed optimizations (e.g., unused variables, aliasing issues).
  • Over-optimizing small functions. Inlining everything can bloat the code and hurt cache performance. Focus on hotspots.
  • Using -O3 blindly. Sometimes -O3 is slower than -O2 due to code bloat. Test both.
  • Parallelizing the wrong loops. Loops with dependencies (e.g., a reduction that must be ordered) can’t be parallelized without care. Use reduction clauses in OpenMP.
  • Not checking for race conditions. When using threads, data races can cause incorrect results that are hard to debug. Use tools like ThreadSanitizer.

Tips

  • Use -pg with GCC to compile for gprof, but prefer sampling profilers like perf—they have lower overhead.
  • For GPU acceleration, profile with NVIDIA Nsight or AMD ROCm tools, and focus on memory transfers between host and device.
  • Use valgrind --tool=cachegrind to analyze cache misses—a common hidden bottleneck.
  • Keep your data structures cache-friendly: use arrays of structs (AoS) or structs of arrays (SoA) depending on access patterns.
  • Consider using -ffast-math only if you don’t need IEEE-compliant floating-point behavior (it can speed up math but changes results).
  • Automate benchmarking with a script that runs your program multiple times and reports median runtime to reduce noise.

Troubleshooting

  • Profiler shows no hotspots. If CPU time is evenly spread, the bottleneck might be memory bandwidth or I/O. Use perf stat to check cache misses and branch mispredictions.
  • Parallelization slows things down. This often means overhead (thread creation, synchronization) outweighs the benefit. Increase the work per thread (e.g., use larger datasets or coarser granularity).
  • Results are wrong after optimization. Check for floating-point differences (use -ffp-contract=off to disable FMA contraction) or race conditions. Run with valgrind --tool=helgrind to detect data races.
  • Program crashes with -march=native. This may happen if you run the binary on a different CPU than the one it was compiled on. Use a more conservative flag like -march=x86-64-v3.
  • Speedup plateaus at a low core count. Check for a serial bottleneck (Amdahl’s law). Use perf to see if one thread is still doing most of the work. Consider overlapping I/O with computation.

Frequently Asked Questions

What’s the difference between profiling and benchmarking?

Profiling identifies where time is spent (function-level analysis), while benchmarking measures overall performance (e.g., runtime or throughput). You profile to find hotspots, then benchmark to verify improvements.

Should I always use the highest optimization level?

No. -O3 can sometimes be slower than -O2 due to increased code size and cache pressure. Test both on your specific workload.

How do I profile a Python script for HPC?

Use cProfile for function-level timing, and line_profiler for line-by-line. For numerical code, consider using Cython or Numba to speed up the hotspots.

What is a good speedup to aim for?

It depends on your code and hardware. A 2x speedup from compiler flags is common. With parallelization, aim for near-linear speedup up to the number of cores, but don’t expect it beyond that due to overhead.

How do I know if my code is memory-bound or CPU-bound?

Use perf stat and look at the ratio of cache misses to instructions. If cache misses are high, you’re memory-bound. If CPU utilization is high and cache misses are low, you’re CPU-bound.

Can I optimize without changing my algorithm?

Yes, you can improve constant factors (e.g., better memory layout, vectorization, loop unrolling). But if your algorithm is inefficient, these gains are limited. Sometimes a better algorithm gives an order-of-magnitude improvement.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →