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", "libc",
"nix", "nix",
"once_cell", "once_cell",
"perf-event-open-sys",
"platform-info", "platform-info",
"predicates", "predicates",
"rand 0.10.3", "rand 0.10.3",
@@ -539,6 +540,15 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 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]] [[package]]
name = "platform-info" name = "platform-info"
version = "2.2.0" version = "2.2.0"
+3
View File
@@ -50,6 +50,9 @@ once_cell = "1.21"
[target.'cfg(target_os="linux")'.dependencies] [target.'cfg(target_os="linux")'.dependencies]
nix = { version = "0.31", features = ["zerocopy"] } 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] [dependencies.clap]
version = "4" version = "4"
default-features = false 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*. *not the simultaneous total memory of the full process tree*.
- Windows: Currently not supported. - 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 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 [`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 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_input_policy: &CommandInputPolicy,
command_output_policy: &CommandOutputPolicy, command_output_policy: &CommandOutputPolicy,
command_name: &str, command_name: &str,
collect_hardware_counters: bool,
) -> Result<Measurement> { ) -> Result<Measurement> {
let stdin = command_input_policy.get_stdin()?; let stdin = command_input_policy.get_stdin()?;
let (stdout, stderr) = command_output_policy.get_stdout_stderr()?; let (stdout, stderr) = command_output_policy.get_stdout_stderr()?;
@@ -76,7 +77,7 @@ fn run_command_and_measure_common(
command.env("HYPERFINE_ITERATION", value); 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}'"))?; .with_context(|| format!("Failed to run command '{command_name}'"))?;
if !measurement.exit_status.success() { if !measurement.exit_status.success() {
@@ -144,6 +145,7 @@ impl Executor for RawExecutor<'_> {
&self.options.command_input_policy, &self.options.command_input_policy,
output_policy, output_policy,
&command.get_command_line(), &command.get_command_line(),
matches!(iteration, BenchmarkIteration::Benchmark(_)),
) )
} }
@@ -199,6 +201,7 @@ impl Executor for ShellExecutor<'_> {
&self.options.command_input_policy, &self.options.command_input_policy,
output_policy, output_policy,
&command.get_command_line(), &command.get_command_line(),
false,
)?; )?;
// Subtract shell spawning time // Subtract shell spawning time
@@ -278,6 +281,7 @@ impl Executor for ShellExecutor<'_> {
time_user: measurements.time_user_mean(), time_user: measurements.time_user_mean(),
time_system: measurements.time_system_mean(), time_system: measurements.time_system_mean(),
memory_peak_resident: None, memory_peak_resident: None,
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}); });
@@ -336,6 +340,7 @@ impl Executor for MockExecutor {
time_user: Time::zero(), time_user: Time::zero(),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::zero()), memory_peak_resident: Some(Information::zero()),
hardware_counters: Default::default(),
exit_status, 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 /// Performance metric measurements and exit code for a single run
#[derive(Debug, Clone, Default, Serialize, PartialEq)] #[derive(Debug, Clone, Default, Serialize, PartialEq)]
pub struct Measurement { pub struct Measurement {
@@ -51,6 +92,9 @@ pub struct Measurement {
)] )]
pub memory_peak_resident: Option<Information>, pub memory_peak_resident: Option<Information>,
#[serde(flatten)]
pub hardware_counters: HardwareCounters,
// The exit status of the process // The exit status of the process
#[serde(rename = "exit_code", serialize_with = "serialize_exit_status")] #[serde(rename = "exit_code", serialize_with = "serialize_exit_status")]
pub exit_status: ExitStatus, pub exit_status: ExitStatus,
+6
View File
@@ -89,6 +89,7 @@ fn test_csv() {
time_user: Time::new::<second>(7.0), time_user: Time::new::<second>(7.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -96,6 +97,7 @@ fn test_csv() {
time_user: Time::new::<second>(8.0), time_user: Time::new::<second>(8.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -103,6 +105,7 @@ fn test_csv() {
time_user: Time::new::<second>(12.0), time_user: Time::new::<second>(12.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -133,6 +136,7 @@ fn test_csv() {
time_user: Time::new::<second>(17.0), time_user: Time::new::<second>(17.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -140,6 +144,7 @@ fn test_csv() {
time_user: Time::new::<second>(18.0), time_user: Time::new::<second>(18.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -147,6 +152,7 @@ fn test_csv() {
time_user: Time::new::<second>(19.0), time_user: Time::new::<second>(19.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::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::benchmark::measurement::Measurements;
use crate::options::SortOrder; use crate::options::SortOrder;
use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue}; 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; use anyhow::Result;
@@ -40,36 +40,92 @@ struct BenchmarkSummary {
time_system: StatisticalSummary, time_system: StatisticalSummary,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
memory_peak_resident: Option<StatisticalSummary>, 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 { impl BenchmarkSummary {
fn from_measurements(measurements: &Measurements) -> Self { fn from_measurements(measurements: &Measurements) -> Self {
let values = &measurements.measurements; let values = &measurements.measurements;
let mut memory_values = values
.iter()
.filter_map(|m| m.memory_peak_resident)
.peekable();
Self { Self {
time_wall_clock: StatisticalSummary::from_values( time_wall_clock: StatisticalSummary::from_values(
values.iter().map(|m| m.time_wall_clock), values.iter().map(|m| Some(m.time_wall_clock)),
second, Some(second),
), )
time_user: StatisticalSummary::from_values(values.iter().map(|m| m.time_user), 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( time_system: StatisticalSummary::from_values(
values.iter().map(|m| m.time_system), values.iter().map(|m| Some(m.time_system)),
second, 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)] #[derive(Serialize)]
struct StatisticalSummary { struct StatisticalSummary {
unit: &'static str, #[serde(skip_serializing_if = "Option::is_none")]
unit: Option<&'static str>,
count: usize, count: usize,
mean: f64, mean: f64,
stddev: Option<f64>, stddev: Option<f64>,
@@ -79,7 +135,10 @@ struct StatisticalSummary {
} }
impl 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 where
Q: Copy Q: Copy
+ PartialOrd + PartialOrd
@@ -92,13 +151,13 @@ impl StatisticalSummary {
P: Into<Q>, P: Into<Q>,
U: uom::si::Unit, U: uom::si::Unit,
{ {
let values: Vec<_> = values.into_iter().collect(); // Only summarize a metric when it is available for every supplied measurement.
assert!( let values: Vec<_> = values.into_iter().collect::<Option<_>>()?;
!values.is_empty(), if values.is_empty() {
"Statistical summaries require at least one measurement" return None;
); }
Self { Some(Self {
unit: U::singular(), unit: unit.map(|_| U::singular()),
count: values.len(), count: values.len(),
mean: mean(values.iter().copied()).value_in_unit(), mean: mean(values.iter().copied()).value_in_unit(),
stddev: (values.len() > 1) stddev: (values.len() > 1)
@@ -106,7 +165,7 @@ impl StatisticalSummary {
median: median(values.iter().copied()).value_in_unit(), median: median(values.iter().copied()).value_in_unit(),
min: min(values.iter().copied()).value_in_unit(), min: min(values.iter().copied()).value_in_unit(),
max: max(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}; use crate::quantity::{kibibyte, millisecond, Information, Time};
let time = StatisticalSummary::from_values( let time = StatisticalSummary::from_values(
[Time::new::<second>(1.0), Time::new::<second>(3.0)], [Time::new::<second>(1.0), Time::new::<second>(3.0)].map(Some),
millisecond, Some(millisecond),
); )
.unwrap();
let information = StatisticalSummary::from_values( let information = StatisticalSummary::from_values(
[ [
Information::new::<byte>(1024.0), Information::new::<byte>(1024.0),
Information::new::<byte>(3072.0), Information::new::<byte>(3072.0),
], ]
kibibyte, .map(Some),
); Some(kibibyte),
)
.unwrap();
for (summary, unit, scale) in [ for (summary, unit, scale) in [
(time, "millisecond", 1000.0), (time, "millisecond", 1000.0),
(information, "kibibyte", 1.0), (information, "kibibyte", 1.0),
] { ] {
assert_eq!(summary.unit, unit); assert_eq!(summary.unit, Some(unit));
assert_eq!(summary.count, 2); assert_eq!(summary.count, 2);
approx::assert_relative_eq!(summary.mean, 2.0 * scale); approx::assert_relative_eq!(summary.mean, 2.0 * scale);
approx::assert_relative_eq!(summary.median, 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_user: Time::new::<second>(0.09),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -43,6 +44,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.10), time_user: Time::new::<second>(0.10),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -50,6 +52,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(0.14), time_user: Time::new::<second>(0.14),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -65,6 +68,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(2.0), time_user: Time::new::<second>(2.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -72,6 +76,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(3.0), time_user: Time::new::<second>(3.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -79,6 +84,7 @@ fn test_markup_export_auto_ms() {
time_user: Time::new::<second>(4.0), time_user: Time::new::<second>(4.0),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -139,6 +145,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.1), time_user: Time::new::<second>(2.1),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -146,6 +153,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.2), time_user: Time::new::<second>(2.2),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -153,6 +161,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(2.3), time_user: Time::new::<second>(2.3),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -168,6 +177,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.1), time_user: Time::new::<second>(0.1),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -175,6 +185,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.2), time_user: Time::new::<second>(0.2),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -182,6 +193,7 @@ fn test_markup_export_auto_s() {
time_user: Time::new::<second>(0.3), time_user: Time::new::<second>(0.3),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -242,6 +254,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.1), time_user: Time::new::<second>(2.1),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -249,6 +262,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.2), time_user: Time::new::<second>(2.2),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -256,6 +270,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(2.3), time_user: Time::new::<second>(2.3),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -271,6 +286,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.1), time_user: Time::new::<second>(0.1),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -278,6 +294,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.2), time_user: Time::new::<second>(0.2),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -285,6 +302,7 @@ fn test_markup_export_manual_ms() {
time_user: Time::new::<second>(0.3), time_user: Time::new::<second>(0.3),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -344,6 +362,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.01), time_user: Time::new::<second>(2.01),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -351,6 +370,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.02), time_user: Time::new::<second>(2.02),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -358,6 +378,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(2.03), time_user: Time::new::<second>(2.03),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
]), ]),
@@ -373,6 +394,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.11), time_user: Time::new::<second>(0.11),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -380,6 +402,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.12), time_user: Time::new::<second>(0.12),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::default(), exit_status: ExitStatus::default(),
}, },
Measurement { Measurement {
@@ -387,6 +410,7 @@ fn test_markup_export_manual_s() {
time_user: Time::new::<second>(0.13), time_user: Time::new::<second>(0.13),
time_system: Time::zero(), time_system: Time::zero(),
memory_peak_resident: Some(Information::new::<byte>(1024.)), memory_peak_resident: Some(Information::new::<byte>(1024.)),
hardware_counters: Default::default(),
exit_status: ExitStatus::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 { pub trait FormatQuantity {
type Unit; 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 { fn standard_deviation_f64(values: &[f64]) -> f64 {
let mean_value = mean(values.iter().copied()); 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; mod wall_clock_timer;
#[cfg(target_os = "linux")]
mod linux_counters;
#[cfg(target_os = "macos")]
mod macos_counters;
#[cfg(windows)] #[cfg(windows)]
mod windows_timer; mod windows_timer;
@@ -56,7 +61,12 @@ fn discard(output: ChildStdout) {
} }
/// Execute the given command and return a timing summary /// 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))] #[cfg(not(windows))]
let cpu_timer = self::unix_timer::CPUTimer::start(); let cpu_timer = self::unix_timer::CPUTimer::start();
@@ -82,14 +92,36 @@ pub fn execute_and_measure(mut command: Command) -> Result<Measurement> {
discard(output); 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)?; 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(); 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 { Ok(Measurement {
time_wall_clock, time_wall_clock,
time_user, time_user,
time_system, time_system,
memory_peak_resident, memory_peak_resident,
hardware_counters,
exit_status, exit_status,
}) })
} }