Support hardware counters (instructions, cache misses, ...) (#983)

* Add hardware counters to JSON exports

* Clarify Linux hardware counter setup
This commit is contained in:
David Peter authored and GitHub committed 2026-10-06 08:59:40 +02:00
1 parent ab86cb99cc
commit bc3a3f0524
13 files changed
+432 -34

No files matched your search

Generated
+10
View File
@@ -393,6 +393,7 @@ dependencies = [
"libc",
"nix",
"once_cell",
"perf-event-open-sys",
"platform-info",
"predicates",
"rand 0.10.3",
@@ -539,6 +540,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "perf-event-open-sys"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5f8d1487a4ffa23c80a1c355dd27235f9b66fb71ba0f261eb417e4fe8451347"
dependencies = [
"libc",
]
[[package]]
name = "platform-info"
version = "2.2.0"
+3
View File
@@ -50,6 +50,9 @@ once_cell = "1.21"
[target.'cfg(target_os="linux")'.dependencies]
nix = { version = "0.31", features = ["zerocopy"] }
[target.'cfg(all(target_os = "linux", target_pointer_width = "64", any(target_arch = "aarch64", target_arch = "x86_64", target_arch = "riscv64", target_arch = "powerpc64", target_arch = "loongarch64")))'.dependencies]
perf-event-open-sys = "6"
[dependencies.clap]
version = "4"
default-features = false
+28
View File
@@ -187,6 +187,34 @@ The JSON export includes the following metrics for each measured run (excluding
*not the simultaneous total memory of the full process tree*.
- Windows: Currently not supported.
- **`cpu_cycles`**: CPU cycles consumed.
- Linux: Includes threads and child processes, but excludes kernel and hypervisor execution.
- macOS: Includes the process's threads and kernel execution, but excludes child processes.
- Windows: Currently not supported.
- **`instructions`**: Completed CPU instructions.
- Linux/macOS: Same scope as `cpu_cycles`.
- Windows: Currently not supported.
- **`cache_references`**: Cache accesses counted by the CPU's generic cache event.
- Linux: Same scope as `cpu_cycles`. Cache-event definitions depend on the CPU.
- macOS/Windows: Currently not supported.
- **`cache_misses`**: Cache misses.
- Linux: Same scope as `cpu_cycles`. Cache-event definitions depend on the CPU.
- macOS/Windows: Currently not supported.
- **`branch_misses`**: Mispredicted branches.
- Linux: Same scope as `cpu_cycles`.
- macOS/Windows: Currently not supported.
Hardware counters are not available if a `--shell` is used.
The JSON output is useful if you want to analyze the benchmark results in more detail. The
[`scripts/`](https://github.com/sharkdp/hyperfine/tree/master/scripts) folder includes a lot
of helpful Python programs to further analyze benchmark results and create helpful
+6 -1
View File
@@ -62,6 +62,7 @@ fn run_command_and_measure_common(
command_input_policy: &CommandInputPolicy,
command_output_policy: &CommandOutputPolicy,
command_name: &str,
collect_hardware_counters: bool,
) -> Result<Measurement> {
let stdin = command_input_policy.get_stdin()?;
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
@@ -76,7 +77,7 @@ fn run_command_and_measure_common(
command.env("HYPERFINE_ITERATION", value);
}
let measurement = execute_and_measure(command)
let measurement = execute_and_measure(command, collect_hardware_counters)
.with_context(|| format!("Failed to run command '{command_name}'"))?;
if !measurement.exit_status.success() {
@@ -144,6 +145,7 @@ impl Executor for RawExecutor<'_> {
&self.options.command_input_policy,
output_policy,
&command.get_command_line(),
matches!(iteration, BenchmarkIteration::Benchmark(_)),
)
}
@@ -199,6 +201,7 @@ impl Executor for ShellExecutor<'_> {
&self.options.command_input_policy,
output_policy,
&command.get_command_line(),
false,
)?;
// Subtract shell spawning time
@@ -278,6 +281,7 @@ impl Executor for ShellExecutor<'_> {
time_user: measurements.time_user_mean(),
time_system: measurements.time_system_mean(),
memory_peak_resident: None,
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
});
@@ -336,6 +340,7 @@ impl Executor for MockExecutor {
time_user: Time::zero(),
time_system: Time::zero(),
memory_peak_resident: Some(Information::zero()),
hardware_counters: Default::default(),
exit_status,
})
}
+44
View File
@@ -29,6 +29,47 @@ where
}
}
fn serialize_optional_count<S>(value: &Option<u64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
#[derive(Serialize)]
struct Count {
value: u64,
}
value.map(|value| Count { value }).serialize(serializer)
}
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
pub struct HardwareCounters {
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_count"
)]
pub cpu_cycles: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_count"
)]
pub instructions: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_count"
)]
pub cache_references: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_count"
)]
pub cache_misses: Option<u64>,
#[serde(
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_optional_count"
)]
pub branch_misses: Option<u64>,
}
/// Performance metric measurements and exit code for a single run
#[derive(Debug, Clone, Default, Serialize, PartialEq)]
pub struct Measurement {
@@ -51,6 +92,9 @@ pub struct Measurement {
)]
pub memory_peak_resident: Option<Information>,
#[serde(flatten)]
pub hardware_counters: HardwareCounters,
// The exit status of the process
#[serde(rename = "exit_code", serialize_with = "serialize_exit_status")]
pub exit_status: ExitStatus,
+6
View File
@@ -89,6 +89,7 @@ fn test_csv() {
time_user: Time::new::<second>(7.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -96,6 +97,7 @@ fn test_csv() {
time_user: Time::new::<second>(8.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -103,6 +105,7 @@ fn test_csv() {
time_user: Time::new::<second>(12.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -133,6 +136,7 @@ fn test_csv() {
time_user: Time::new::<second>(17.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -140,6 +144,7 @@ fn test_csv() {
time_user: Time::new::<second>(18.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -147,6 +152,7 @@ fn test_csv() {
time_user: Time::new::<second>(19.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
+94 -32
View File
@@ -8,7 +8,7 @@ use crate::benchmark::benchmark_result::BenchmarkResult;
use crate::benchmark::measurement::Measurements;
use crate::options::SortOrder;
use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue};
use crate::quantity::{byte, second, QuantityInUnit, Ratio, TimeUnit, Zero};
use crate::quantity::{byte, ratio, second, QuantityInUnit, Ratio, TimeUnit, Zero};
use anyhow::Result;
@@ -40,36 +40,92 @@ struct BenchmarkSummary {
time_system: StatisticalSummary,
#[serde(skip_serializing_if = "Option::is_none")]
memory_peak_resident: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
cpu_cycles: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
instructions: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_references: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
cache_misses: Option<StatisticalSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
branch_misses: Option<StatisticalSummary>,
}
impl BenchmarkSummary {
fn from_measurements(measurements: &Measurements) -> Self {
let values = &measurements.measurements;
let mut memory_values = values
.iter()
.filter_map(|m| m.memory_peak_resident)
.peekable();
Self {
time_wall_clock: StatisticalSummary::from_values(
values.iter().map(|m| m.time_wall_clock),
second,
),
time_user: StatisticalSummary::from_values(values.iter().map(|m| m.time_user), second),
values.iter().map(|m| Some(m.time_wall_clock)),
Some(second),
)
.expect("Benchmarks have at least one measurement"),
time_user: StatisticalSummary::from_values(
values.iter().map(|m| Some(m.time_user)),
Some(second),
)
.expect("Benchmarks have at least one measurement"),
time_system: StatisticalSummary::from_values(
values.iter().map(|m| m.time_system),
second,
values.iter().map(|m| Some(m.time_system)),
Some(second),
)
.expect("Benchmarks have at least one measurement"),
memory_peak_resident: StatisticalSummary::from_values(
values
.iter()
.filter_map(|m| m.memory_peak_resident)
.map(Some),
Some(byte),
),
cpu_cycles: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.cpu_cycles
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
instructions: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.instructions
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
cache_references: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.cache_references
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
cache_misses: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.cache_misses
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
branch_misses: StatisticalSummary::from_values(
values.iter().map(|m| {
m.hardware_counters
.branch_misses
.map(|v| Ratio::new::<ratio>(v as f64))
}),
None::<ratio>,
),
memory_peak_resident: memory_values
.peek()
.is_some()
.then(|| StatisticalSummary::from_values(memory_values, byte)),
}
}
}
#[derive(Serialize)]
struct StatisticalSummary {
unit: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
unit: Option<&'static str>,
count: usize,
mean: f64,
stddev: Option<f64>,
@@ -79,7 +135,10 @@ struct StatisticalSummary {
}
impl StatisticalSummary {
fn from_values<Q, P, U>(values: impl IntoIterator<Item = Q>, _unit: U) -> Self
fn from_values<Q, P, U>(
values: impl IntoIterator<Item = Option<Q>>,
unit: Option<U>,
) -> Option<Self>
where
Q: Copy
+ PartialOrd
@@ -92,13 +151,13 @@ impl StatisticalSummary {
P: Into<Q>,
U: uom::si::Unit,
{
let values: Vec<_> = values.into_iter().collect();
assert!(
!values.is_empty(),
"Statistical summaries require at least one measurement"
);
Self {
unit: U::singular(),
// Only summarize a metric when it is available for every supplied measurement.
let values: Vec<_> = values.into_iter().collect::<Option<_>>()?;
if values.is_empty() {
return None;
}
Some(Self {
unit: unit.map(|_| U::singular()),
count: values.len(),
mean: mean(values.iter().copied()).value_in_unit(),
stddev: (values.len() > 1)
@@ -106,7 +165,7 @@ impl StatisticalSummary {
median: median(values.iter().copied()).value_in_unit(),
min: min(values.iter().copied()).value_in_unit(),
max: max(values.iter().copied()).value_in_unit(),
}
})
}
}
@@ -149,22 +208,25 @@ fn test_statistical_summary_unit_conversion() {
use crate::quantity::{kibibyte, millisecond, Information, Time};
let time = StatisticalSummary::from_values(
[Time::new::<second>(1.0), Time::new::<second>(3.0)],
millisecond,
);
[Time::new::<second>(1.0), Time::new::<second>(3.0)].map(Some),
Some(millisecond),
)
.unwrap();
let information = StatisticalSummary::from_values(
[
Information::new::<byte>(1024.0),
Information::new::<byte>(3072.0),
],
kibibyte,
);
]
.map(Some),
Some(kibibyte),
)
.unwrap();
for (summary, unit, scale) in [
(time, "millisecond", 1000.0),
(information, "kibibyte", 1.0),
] {
assert_eq!(summary.unit, unit);
assert_eq!(summary.unit, Some(unit));
assert_eq!(summary.count, 2);
approx::assert_relative_eq!(summary.mean, 2.0 * scale);
approx::assert_relative_eq!(summary.median, 2.0 * scale);
+24
View File
@@ -36,6 +36,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.09),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -43,6 +44,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.10),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -50,6 +52,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.14),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -65,6 +68,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(2.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -72,6 +76,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(3.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -79,6 +84,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(4.0),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -139,6 +145,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -146,6 +153,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -153,6 +161,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -168,6 +177,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -175,6 +185,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -182,6 +193,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -242,6 +254,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -249,6 +262,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -256,6 +270,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -271,6 +286,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.1),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -278,6 +294,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.2),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -285,6 +302,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.3),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -344,6 +362,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.01),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -351,6 +370,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.02),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -358,6 +378,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.03),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
@@ -373,6 +394,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.11),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -380,6 +402,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.12),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
Measurement {
@@ -387,6 +410,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.13),
time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(),
},
]),
+6
View File
@@ -34,6 +34,12 @@ impl<U: uom::si::information::Unit + uom::Conversion<f64, T = f64>> QuantityInUn
}
}
impl<U: uom::si::ratio::Unit + uom::Conversion<f64, T = f64>> QuantityInUnit<U> for Ratio {
fn value_in_unit(self) -> f64 {
self.get::<U>()
}
}
pub trait FormatQuantity {
type Unit;
+10
View File
@@ -91,6 +91,16 @@ impl UnsafeRawValue for Information {
}
}
impl UnsafeRawValue for Ratio {
fn unsafe_raw_value(&self) -> f64 {
self.get::<ratio>()
}
fn unsafe_from_raw_value(value: f64) -> Self {
Ratio::new::<ratio>(value)
}
}
fn standard_deviation_f64(values: &[f64]) -> f64 {
let mean_value = mean(values.iter().copied());
+113
View File
@@ -0,0 +1,113 @@
use std::fs::File;
use std::io::Read;
use crate::benchmark::measurement::HardwareCounters;
/// Disabled events on the spawning thread, inherited and enabled by the child's exec.
/// The parent never enables its own events, so its work is not counted.
pub struct CounterGroup {
events: [Option<File>; 5],
}
impl CounterGroup {
#[cfg(all(
target_pointer_width = "64",
any(
target_arch = "aarch64",
target_arch = "x86_64",
target_arch = "riscv64",
target_arch = "powerpc64",
target_arch = "loongarch64"
)
))]
pub fn new() -> Self {
use perf_event_open_sys::{bindings, perf_event_open};
use std::os::fd::{AsRawFd, FromRawFd};
let mut group_leader = -1;
let events = [
bindings::PERF_COUNT_HW_CPU_CYCLES,
bindings::PERF_COUNT_HW_INSTRUCTIONS,
bindings::PERF_COUNT_HW_CACHE_REFERENCES,
bindings::PERF_COUNT_HW_CACHE_MISSES,
bindings::PERF_COUNT_HW_BRANCH_MISSES,
]
.map(|config| {
let mut attr = bindings::perf_event_attr {
type_: bindings::PERF_TYPE_HARDWARE,
size: std::mem::size_of::<bindings::perf_event_attr>() as u32,
config: u64::from(config),
..Default::default()
};
// Don't start counting immediately.
attr.set_disabled(1);
// Start counting when the child executes the benchmark program.
attr.set_enable_on_exec(1);
// New child processes and threads inherit the counters.
attr.set_inherit(1);
// Exclude kernel execution.
attr.set_exclude_kernel(1);
// Exclude hypervisor execution.
attr.set_exclude_hv(1);
// Attach counters to the calling thread.
const CURRENT_THREAD: libc::pid_t = 0;
// Count on whichever CPU the task runs on.
const ANY_CPU: libc::c_int = -1;
// SAFETY: attr is initialized and sized for the kernel API.
let fd = unsafe {
perf_event_open(
&mut attr,
CURRENT_THREAD,
ANY_CPU,
group_leader,
bindings::PERF_FLAG_FD_CLOEXEC.into(),
)
};
if fd < 0 {
return None;
}
// SAFETY: perf_event_open returned a new, valid descriptor.
let event = unsafe { File::from_raw_fd(fd) };
if group_leader == -1 {
group_leader = event.as_raw_fd();
}
Some(event)
});
Self { events }
}
#[cfg(not(all(
target_pointer_width = "64",
any(
target_arch = "aarch64",
target_arch = "x86_64",
target_arch = "riscv64",
target_arch = "powerpc64",
target_arch = "loongarch64"
)
)))]
pub fn new() -> Self {
Self {
events: Default::default(),
}
}
/// Read raw counts after the child has exited. Like poop, do not scale for multiplexing.
pub fn read(mut self) -> HardwareCounters {
let [cpu_cycles, instructions, cache_references, cache_misses, branch_misses] =
self.events.each_mut().map(|event| {
let mut bytes = [0; 8];
event.as_mut()?.read_exact(&mut bytes).ok()?;
Some(u64::from_ne_bytes(bytes))
});
HardwareCounters {
cpu_cycles,
instructions,
cache_references,
cache_misses,
branch_misses,
}
}
}
+55
View File
@@ -0,0 +1,55 @@
use std::io;
use std::mem::MaybeUninit;
use std::process::Child;
use crate::benchmark::measurement::HardwareCounters;
/// Wait for exit without reaping: proc_pid_rusage can still inspect the zombie,
/// and the existing wait4 call can subsequently collect CPU times and peak RSS.
pub fn wait(child: &mut Child) -> io::Result<()> {
drop(child.stdin.take());
let mut info = MaybeUninit::uninit();
loop {
// SAFETY: info points to writable siginfo_t storage. WNOWAIT preserves the child.
let result = unsafe {
libc::waitid(
libc::P_PID,
child.id(),
info.as_mut_ptr(),
libc::WEXITED | libc::WNOWAIT,
)
};
if result == 0 {
return Ok(());
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
}
}
pub fn read(pid: u32) -> HardwareCounters {
let mut usage = MaybeUninit::<libc::rusage_info_v4>::zeroed();
// SAFETY: the buffer has the layout and size requested by RUSAGE_INFO_V4.
// Despite the pointer-to-pointer signature, this API writes directly into the buffer.
let result = unsafe {
libc::proc_pid_rusage(
pid as libc::pid_t,
libc::RUSAGE_INFO_V4,
usage.as_mut_ptr().cast(),
)
};
if result != 0 {
return HardwareCounters::default();
}
// SAFETY: the call succeeded and initialized the buffer.
let usage = unsafe { usage.assume_init() };
HardwareCounters {
// Like macOS time(1), omit zero counters: the API can succeed on systems
// where instruction/cycle accounting is unavailable.
cpu_cycles: (usage.ri_cycles != 0).then_some(usage.ri_cycles),
instructions: (usage.ri_instructions != 0).then_some(usage.ri_instructions),
..Default::default()
}
}
+33 -1
View File
@@ -1,5 +1,10 @@
mod wall_clock_timer;
#[cfg(target_os = "linux")]
mod linux_counters;
#[cfg(target_os = "macos")]
mod macos_counters;
#[cfg(windows)]
mod windows_timer;
@@ -56,7 +61,12 @@ fn discard(output: ChildStdout) {
}
/// Execute the given command and return a timing summary
pub fn execute_and_measure(mut command: Command) -> Result<Measurement> {
pub fn execute_and_measure(
mut command: Command,
_collect_hardware_counters: bool,
) -> Result<Measurement> {
#[cfg(target_os = "linux")]
let counters = _collect_hardware_counters.then(linux_counters::CounterGroup::new);
#[cfg(not(windows))]
let cpu_timer = self::unix_timer::CPUTimer::start();
@@ -82,14 +92,36 @@ pub fn execute_and_measure(mut command: Command) -> Result<Measurement> {
discard(output);
}
#[cfg(target_os = "macos")]
let (time_wall_clock, hardware_counters) =
if _collect_hardware_counters && macos_counters::wait(&mut child).is_ok() {
// Stop timing before querying counters, but leave the child available for wait4.
(
Some(wallclock_timer.stop()),
macos_counters::read(child.id()),
)
} else {
(None, Default::default())
};
let (time_user, time_system, memory_peak_resident, exit_status) = cpu_timer.stop(child)?;
#[cfg(target_os = "macos")]
let time_wall_clock = time_wall_clock.unwrap_or_else(|| wallclock_timer.stop());
#[cfg(not(target_os = "macos"))]
let time_wall_clock = wallclock_timer.stop();
#[cfg(target_os = "linux")]
let hardware_counters = counters
.map(linux_counters::CounterGroup::read)
.unwrap_or_default();
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let hardware_counters = Default::default();
Ok(Measurement {
time_wall_clock,
time_user,
time_system,
memory_peak_resident,
hardware_counters,
exit_status,
})
}