Profiling with gcov
A timer tells you that a program is slow; a profiler tells you where. The simplest profiler in the compiler's own toolbox is gcov, which counts how many times each line of a source file ran.
The cost is the --coverage build, which is slower and larger than the normal one, and the data it produces is about counts, not time. That is enough to find the function that runs a million times, which is where the time usually is. Lesson 118 measured the whole loop; this lesson opens it.
gcov counts lines, and a line that ran is not a line whose result was checked. A coverage report answers which code the program reached; the assertions of lesson 085 answer whether the result was right, and a project needs both numbers.
Build with coverage
--coverage is the short form of -fprofile-arcs -ftest-coverage and it also links the gcov runtime. The compiler writes a .gcno file with the graph of the code, and the program writes a .gcda file with the counts when it exits.
The program calls a hot function 100 times and a cold one once, and prints 19801. The two commands show the normal output and the files the run left behind.
The instrumented binary is for profiling only: it is slower, and the data files belong to one build. A release build is made without the flag, and a change to the source makes the old .gcda unusable, which is why the files are deleted with the objects.
#include <iostream>
int hot(int value)
{
return value * 2;
}
int cold(int value)
{
return value + 1;
}
int main()
{
int total = 0;
for (int i = 0; i < 100; ++i) {
total += hot(i);
}
total += cold(total);
std::cout << total << '\n';
}
g++ -std=c++23 -Wall -Wextra --coverage profile.cpp -o profile
./profile
ls profile.gcno profile.gcda
19801
profile.gcda
profile.gcno
The gcov summary
gcov profile.cpp reads the .gcno and .gcda files and writes profile.cpp.gcov, a copy of the source with a count in front of every line. The summary says how many lines were executed at least once.
The output names the file, reports 100 percent of 11 lines and says which file it created. The percentage is a coverage number: a line that never ran is a branch or an error path the test did not reach.
The build in this section repeats the compile and the run, so the coverage files exist in the section's own directory. That is also why the program prints its result again before the gcov lines: the run is redirected to /dev/null here to keep the transcript short.
#include <iostream>
int hot(int value)
{
return value * 2;
}
int cold(int value)
{
return value + 1;
}
int main()
{
int total = 0;
for (int i = 0; i < 100; ++i) {
total += hot(i);
}
total += cold(total);
std::cout << total << '\n';
}
g++ -std=c++23 -Wall -Wextra --coverage profile.cpp -o profile
./profile > /dev/null
gcov profile.cpp
File 'profile.cpp'
Lines executed:100.00% of 11
Creating 'profile.cpp.gcov'
Lines executed:100.00% of 11
The count of every line
Each line of the .gcov file starts with the number of times it ran, then a colon, then the line number and the source. A line with no count is one that cannot run, such as a declaration or a closing brace.
The grep prints the two function headers and the two calls: hot and its call ran 100 times, cold and its call once. That is the hot function, found without a timer and without a guess.
A high count is not the same as a high cost: a line can run a million times and take no time, while one call to a slow function dominates. The count narrows the search, and the timer of lesson 118 or a sampling profiler confirms the cost.
#include <iostream>
int hot(int value)
{
return value * 2;
}
int cold(int value)
{
return value + 1;
}
int main()
{
int total = 0;
for (int i = 0; i < 100; ++i) {
total += hot(i);
}
total += cold(total);
std::cout << total << '\n';
}
g++ -std=c++23 -Wall -Wextra --coverage profile.cpp -o profile
./profile > /dev/null
gcov profile.cpp > /dev/null
grep -E 'hot|cold' profile.cpp.gcov
100: 3:int hot(int value)
1: 8:int cold(int value)
100: 18: total += hot(i);
1: 21: total += cold(total);
No data without the flag
gcov reads files the instrumented build produced. Running it on a program that was compiled without --coverage gives two errors about the notes and the data file, and a line that says there is nothing to count.
The program below is compiled without the flag and gcov is asked for a report. The output names profile.gcno and profile.gcda as missing and reports no executable lines.
The fix is the flag on the compile and the link, one build directory for the instrumented binary and a run before gcov. The data only exists after the program has run, which is the step that is easy to forget.
int main()
{
return 0;
}
g++ -std=c++23 -Wall -Wextra profile.cpp -o profile
gcov profile.cpp
profile.gcno:cannot open notes file
profile.gcda:cannot open data file, assuming not executed
No executable lines
Exercises
- Add a branch that runs only when the total is negative and check the coverage percentage it produces.
- Profile the same program with -O2 --coverage and compare the line counts with the -O0 build.
- Write a program with three functions called 1, 10 and 100 times and find the hot one in the .gcov file.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
--coverageadd to a build?Answer
Instrumentation for gcov and the runtime that writes the counts
The flag enables the arc and line counters and links the code that writes the data file at exit.
-
Which file does the program write when it runs?
Answer
The
.gcdafile with the countsThe compiler writes the notes, the run writes the data, and gcov combines them into the report.
-
What does the first column of a
.gcovfile mean?Answer
How many times the line ran
The count comes first, then the line number, then the source text.
-
What does this program print?
#include <iostream> int hot(int value) { return value * 2; } int cold(int value) { return value + 1; } int main() { int total = 0; for (int i = 0; i < 100; ++i) total += hot(i); total += cold(total); std::cout << total; }Answer
19801
The loop adds twice the numbers 0 to 99, and the cold call adds one more.
-
What does the gcov summary report for the lesson's program?
Answer
Lines executed:100.00% of 11
Every executable line of this small program ran, and there are 11 of them.
-
Which function does the count reveal as the hot one?
Answer
hot, called 100 times
The loop calls hot 100 times and cold once, and the .gcov file shows the two counts.
-
What does
gcovreport when the program was built without --coverage?Answer
That the notes and data files cannot be opened, and no executable lines
Without the instrumented build there are no .gcno and .gcda files to read.
-
Why is a high count not the same as a high cost?
Answer
Because a cheap line can run often while an expensive call runs once
Counts measure how often, not how long, so they narrow the search and a timer confirms it.
-
Why is the instrumented binary not the one to ship?
Answer
It is slower and larger because of the counters
The counters cost time and space, and the release build is made without them.
-
When is the
.gcdafile written?Answer
When the instrumented program exits
The counts accumulate while the program runs and are flushed at exit, so a program that never ran produces none.
The code on this page was built with g++ 15.2.0 and gcov 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The section that shows the gcov error built the program without the coverage flag and ran gcov on a directory with no data files, which is what the message reports.
Article author: Arthur Isaev