Black Friday 2025 – NPI EA (cat = Baeldung on Linux)
announcement - icon

Yes, we're now running our Black Friday Sale. All Access and Pro are 33% off until 2nd December, 2025:

>> EXPLORE ACCESS NOW

Baeldung Pro – Linux – NPI EA (cat = Baeldung on Linux)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

Partner – Orkes – NPI EA (tag=Kubernetes)
announcement - icon

Modern software architecture is often broken. Slow delivery leads to missed opportunities, innovation is stalled due to architectural complexities, and engineering resources are exceedingly expensive.

Orkes is the leading workflow orchestration platform built to enable teams to transform the way they develop, connect, and deploy applications, microservices, AI agents, and more.

With Orkes Conductor managed through Orkes Cloud, developers can focus on building mission critical applications without worrying about infrastructure maintenance to meet goals and, simply put, taking new products live faster and reducing total cost of ownership.

Try a 14-Day Free Trial of Orkes Conductor today.

1. Introduction

Developers often worry about performance when working on large-scale C++ software. So, to avoid pitfalls, it’s a common best practice to profile the code. Profiling helps developers analyze program performance, identify bottlenecks, and optimize resource usage. Of course, as an outstanding development platform, Linux supports various code profiling tools like gprof, perf, and Valgrind, which provide detailed insights into CPU and memory usage for an efficient and optimized C++ program.

In this tutorial, we’ll discuss the need for profiling and explore different tools for profiling C++ code running on Linux.

2. Why We Profile C++ Code Running on Linux

As mentioned earlier, profiling enables developers to identify how efficient programs are and where optimizations are required.

Let’s highlight some key benefits of profiling C++ code running on Linux:

  • identify memory allocations that are never freed
  • analyze CPU utilization and system performance
  • detect slow functions and inefficient code
  • improve execution speed and resource management
  • reduce I/O or computation bottlenecks
  • spot issues before they become critical
  • balance workloads and improve parallel execution
  • enhance cache efficiency to minimize latency
  • identify unnecessary system calls to reduce overhead

Overall, profiling is a dynamic program analysis technique that examines code while it’s running to ensure efficiency and optimization. Moreover, it helps developers maintain the program’s stability and scalability.

3. Preparing C++ Code for Profiling

Before diving into the practical demonstration of code profiling in Linux, let’s create and save a C++ program named baeldung_profiling.cpp:

$ cat baeldung_profiling.cpp
#include <iostream>
#include <chrono>
#include <thread>

using namespace std;

// CPU-intensive task (factorial calculation)
unsigned long long factorial(int n) {
    return (n <= 1) ? 1 : n * factorial(n - 1);
}

// Intentionally leaking memory for profiling
void memoryLeak() {
    new int[100]; // Not freeing memory on purpose
}

// Simulating an I/O-bound task (we're adding a delay)
void ioBoundTask() {
    this_thread::sleep_for(chrono::seconds(1));
}

int main() {
    cout << "Baeldung Profiling Test" << endl;
    auto start = chrono::high_resolution_clock::now();

    // Running CPU-intensive task
    cout << "Factorial(25) = " << factorial(25) << endl;

    // Simulating memory leak
    memoryLeak();

    // Simulating an I/O-bound task
    ioBoundTask();

    auto end = chrono::high_resolution_clock::now();
    cout << "Total Execution Time: "
         << chrono::duration<double>(end - start).count() << " seconds" << endl;

    return 0;
}

We’ve created the above C++ factorial program with an intentional memory leak, a CPU-intensive task, and a delay to test the performance of Linux profilers.

4. C++ Code Profiling Tools

While there are many options for checking and analyzing code on Linux, several tools stand out when it comes to profiling:

  • gprof
  • perf
  • Valgrind

If we require function-level performance insights, we can use gprof. Additionally, if we want to check real-time system performance and CPU analysis, we can use perf. On the other hand, if we need to detect memory leaks and invalid access attempts, Valgrind with Memcheck is the best option. Lastly, we can use Valgrind with Massif to analyze heap memory usage and optimize allocations.

So, let’s explore the gprof, perf, and Valgrind profilers in Linux to analyze C++ code.

5. Basic Profiling Using gprof

The gprof (also known as the GNU Profiler) is a lightweight and popular profiler that helps developers track the execution time of functions within a C or C++ program.

5.1. Installation

First, let’s install gprof on the Linux system:

$ sudo apt install binutils

The binutils package contains the gprof tool.

5.2. Compile With Profiling

Afterward, we can compile the baeldung_profiling.cpp program with profiling enabled using the -pg flag:

$ g++ -g -pg -o baeldung_profiling baeldung_profiling.cpp

The above command compiles the program with debugging and profiling enabled and generates the baeldung_profiling executable file.

Specifically, -p targets the prof profiler as it produces suitable output. On the other hand, -g enables general debugging information in the system-native format.

5.3. Run With Profiling

Next, we run the compiled program to execute the factorial calculation and generate the profiling data file named gmon.out automatically:

$ ./baeldung_profiling
Baeldung Profiling Test
Factorial(25) = 7034535277573963776
Total Execution Time: 1.0021 seconds

Thus, we see the output of the program.

5.4. View and Analyze Profiling Data

Now, we use gprof to analyze the profiling data stored in gmon.out and save the output to the gprof_report.txt file:

$ gprof baeldung_profiling gmon.out > gprof_report.txt

Finally, we can display the content of the gprof_report.txt file:

$ cat gprof_report.txt
Flat profile:

Each sample counts as 0.01 seconds.
 no time accumulated

  %   cumulative   self              self     total           
 time   seconds   seconds    calls  Ts/call  Ts/call  name    
  0.00      0.00     0.00        7     0.00     0.00  std::chrono::duration<long, std::ratio<1l, 1l> >::count() const
  0.00      0.00     0.00        4     0.00     0.00  std::chrono::duration<long, std::ratio<1l, 1000000000l> >::count() const
  0.00      0.00     0.00        3     0.00     0.00  std::chrono::duration<long, std::ratio<1l, 1l> 
 …

Notably, we can view function call statistics and execution times, which can help developers improve program performance.

6. Advanced Profiling Using perf

perf is a lightweight profiler that provides detailed real-time CPU and performance analysis, including CPU cycles, cache misses, and branch mispredictions.

6.1. Installation

We can install perf on Debian-based Linux systems by ensuring several packages are deployed in the environment:

$ sudo apt install linux-tools-common linux-tools-generic linux-tools-$(uname -r)

Here, we see that all linux-tools-* packages are installed.

6.2. Compile With Profiling

After installing perf, we compile the C++ program:

$ g++ -g -o baeldung_profiling baeldung_profiling.cpp

Thus, we ensure that the code includes profiling helpers.

6.3. Run With Profiling

Next, we profile the baeldung_profiling executable using perf, which records various CPU and performance metrics:

$ sudo perf record ./baeldung_profiling
Baeldung Profiling Test
Factorial(25) = 7034535277573963776
Total Execution Time: 1.00351 seconds
[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 0.015 MB perf.data (111 samples) ]

Notably, we can observe perf collected and stored 111 samples of performance data.

6.4. View and Analyze Profiling Data

Lastly, we can view these metrics:

$ perf report

The command should open a file containing the perf report output:

Samples: 111  of event 'task-clock:ppp', Event count (approx.): 27750000
Overhead  Command          Shared Object         Symbol
  33.33%  baeldung_profil  [kernel.kallsyms]     [k] 0xffffffff8250a114
  28.83%  baeldung_profil  ld-linux-x86-64.so.2  [.] intel_check_word.constprop.
…

Finally, we can close this report by pressing Ctrl + X.

7. Memory Profiling With Valgrind

Valgrind is a powerful instrumentation framework for memory debugging, memory leak detection, and profiling.

7.1. Installation

Before moving ahead, we install Valgrind on the Linux system.

After this, we can check the installed version:

$ valgrind --version
valgrind-3.24.0

At this point, we have a working Valgrind installation.

7.2. Compile With Profiling

Of course, before profiling the code, we compile the C++ program:

$ g++ -g -o baeldung_profiling baeldung_profiling.cpp

Similar to all other cases, the -g flag ensures profilers have information to work with.

7.3. Perform Profiling and Analysis

At this point, we can use any of the seven tools offered by Valgrind to analyze C++ code. For demonstration, we use Memcheck and Massif.

In particular, we leverage Valgrind Memcheck to detect memory leaks and errors in the baeldung_profiling program:

$ valgrind --leak-check=full --show-leak-kinds=all ./baeldung_profiling

Eventually, a file should open displaying the memory leak results:

==4510==    by 0x40065F8: _dl_init (dl-init.c:84)
==4510==    by 0x40202C9: ??? (in /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2)
==4510== 
==4510== 72,704 bytes in 1 blocks are still reachable in loss record 4 of 4
...
==4510== LEAK SUMMARY:
==4510==    definitely lost: 400 bytes in 1 blocks
==4510==    indirectly lost: 0 bytes in 0 blocks
...
==4510== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Profiling timer expired

Additionally, we can use the Valgrind Massif tool for heap profiling and analyzing memory usage:

$ valgrind --tool=massif ./baeldung_profiling
==4621== Massif, a heap profiler
…
==4621== Command: ./baeldung_profiling
==4621== 
Baeldung Profiling Test
Factorial(25) = 7034535277573963776
Total Execution Time: 1.01294 seconds
==4621==

Thus, upon successful execution, the above command generates an output file named massif.out.<pid>, where <pid> is the process ID of the executed program.

Finally, we can view and analyze a human-readable summary of heap memory usage from the output files of Massif or another specific file using its PID. To do so, we check the readable results of all files by executing ms_print:

$ ms_print massif.out.*

Here, we can observe the results in a file:

|   ->01.18% (1,024B) 0x4B1FF5F: _IO_file_overflow@@GLIBC_2.2.5 (fileops.c:744)
|     ->01.18% (1,024B) 0x4B1E6D4: _IO_new_file_xsputn (fileops.c:1243)
...
--------------------------------------------------------------------------------
  n        time(i)         total(B)   useful-heap(B) extra-heap(B)    stacks(B)
--------------------------------------------------------------------------------
  6      2,430,694           74,152           74,128            24            0
  7      2,434,292            1,440            1,424            16            0
  8      2,435,399              408              400             8            0

This detailed information can help developers optimize code and ensure better performance, avoiding crashes.

8. Conclusion

In this article, we covered three C++ code profiling tools on Linux. Profiling helps developers identify performance bottlenecks, memory leaks, and other inefficiencies in their code.

If we require function-level performance insights, we can use gprof. Additionally, perf is useful for real-time CPU and system performance analysis. Of course, we can rely on Valgrind with Memcheck and Massif to detect memory leaks and analyze heap usage.