Understanding the interaction between software and hardware has become increasingly essential for building high-performance applications.
The architecture of modern hardware systems has grown significantly in complexity, including deep memory hierarchies and advanced CPUs with features like out-of-order execution and sophisticated branch prediction mechanisms.
, and ).
The benchmark employs two distinct arrays: one holding the data and another containing indices that establish the random access pattern.
After initializing these arrays, we execute the micro-benchmark by sequentially scanning through the indices array and accessing data from the data array. This method generally leads to approximately one cache miss per access within the contiguous data array.
Perf Stat
To observe the underlying hardware dynamics, we utilize the .
Pramod Kumbhar provides a practical guide to implementing this technique on .
Given the complexity of this interface, various libraries have been developed to simplify interaction by embedding the perf_event_open system call into their framework.
Notable examples include , and and demonstrate practical examples of how to activate and deactivate hardware performance counters for targeted code segments.
The perf::EventCounter class in perf-cpp allows users to define which events to measure and provides start() and stop() methods to manage the counters.
Below is a code snippet that sets up the EventCounter and focuses the measurement on the desired code segment:
#include <perfcpp/event_counter.h>
/// Initialize the hardware event counter
auto counters = perf::CounterDefinition{};
auto event_counter = perf::EventCounter{ counters };
/// Specify hardware events to count
event_counter.add({"instructions", "cycles", "cache-references", "cache-misses"});
/// Setup benchmark here (this will not be measured)
struct alignas(64U) cache_line { std::uint64_t value; };
auto data = std::vector<cache_line>{};
auto indices = std::vector<std::uint64_t>{};
/// Fill both vectors here...
auto sum = 0ULL;
/// Run the workload and count hardware events
event_counter.start();
for (const auto index : indices) {
sum += data[index]; // <-- critical memory access
}
asm volatile("" : : "r,m"(value) : "memory"); // Ensure the compiler will not optimize sum away
event_counter.stop();
Once the EventCounter is initiated and the events of interest are added, we set up the benchmark by initializing the data and pattern arrays.
Enclosing the workload we wish to measure with start() and stop() calls enables precise monitoring of that particular code segment.
Upon stopping the counter, the EventCounter can be queried to obtain the measured events:
const auto result = event_counter.result();
/// Print the performance counters.
for (const auto [name, value] : result)
{
std::cout << value << " " << name << " (" << value / 16777216 << " per access)" << std::endl;
}
The output reflects only the activity during the benchmark, effectively excluding the initial setup phase where data is allocated, and patterns are established:
102,284,667 instructions (6.09664 per access)
992,091,716 cycles (59.1333 per access)
34,227,532 L1-dcache-loads (2.04012 per access)
18,944,008 L1-dcache-load-misses (1.12915 per access)
The results obtained are markedly more explicable than those we got from the perf stat command.
We observe two L1d cache references per access: one for the randomly accessed cache line and another for the index of the pattern array.
Additionally, there are approximately 1.3 cache misses—one for each data cache line and 0.125 for the access index, as eight indices fit into a single cache line of the pattern array.
Hardware-specific Events
While basic performance metrics such as instructions, cycles, and cache misses shed light on the interplay of hardware and software, modern CPUs offer a far broader spectrum of events to monitor.
However, it's important to note that many of these events are specific to the underlying hardware substrate.
The perf subsystem standardizes only a select group of events universally supported across different processors (.
In order to use hardware-specific counters within applications, the readable event names need to be translated into event codes.
To that end, , offer a solution by allowing direct control over hardware performance counters from within the application itself.
By leveraging the perf subsystem (more precisely the perf_event_open system call), these tools enable precise measurements of only the code segments that are truly relevant.
SOCIAL SHARE CARD GENERATOR