mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-10-31 00:40:18 +00:00
Support hardware counters (instructions, cache misses, ...) (#983)
* Add hardware counters to JSON exports * Clarify Linux hardware counter setup
This commit is contained in:
1 parent
ab86cb99cc
commit
bc3a3f0524
13 files changed
+432
-34
No files matched your search
Generated
+10
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
]),
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user