perf(rust): intern CellValues keys as Arc<str> to kill per-cell String clones
The decoder's `parse_lola_binary` inserts the same "VAR:LEVEL"
string into ~92k cell maps for every one of ~60 GRIB2 messages in
a chain step — previously a String::clone per cell, yielding
~5.5M heap allocations and frees per hourly run. Switching
`CellValues` from `HashMap<String, f32>` to
`HashMap<Arc<str>, f32>` makes each insertion a single atomic
refcount increment: the string is allocated once per message, and
`Arc::clone` is cheap from there.
Callers were mostly unchanged — `cell.get("...")` still works via
`Arc<str>: Borrow<str>`. Two touch-ups needed: native_duct.rs had
a couple of `cell.get(&format!(...))` lookups that were passing
`&String` (triggering the wrong Borrow impl) — switched to
.as_str(), and the test fixtures' `.insert(format!(...), _)` now
need a `.into()` to coerce into `Arc<str>`. Zero behavior change.
This commit is contained in:
parent
4d5b5f1aee
commit
83aa3115df
2 changed files with 31 additions and 11 deletions
|
|
@ -40,7 +40,17 @@ pub struct Message {
|
|||
|
||||
/// Per-cell: `"VAR:LEVEL" -> f32`. Key format matches Elixir exactly so
|
||||
/// scorer input can be wired unchanged.
|
||||
pub type CellValues = HashMap<String, f32>;
|
||||
///
|
||||
/// Keys are `Arc<str>` rather than `String`: the hot decode loop
|
||||
/// inserts the same `"VAR:LEVEL"` string into ~92k cell maps per
|
||||
/// GRIB2 message, which was previously a String::clone per cell
|
||||
/// (~5.5M heap allocations + frees per chain step across 60
|
||||
/// messages). `Arc::clone` is a single atomic increment — keys
|
||||
/// allocate once per message and are ref-counted thereafter.
|
||||
///
|
||||
/// Lookup is unchanged: `cell.get("TMP:2 m above ground")` works
|
||||
/// because `Arc<str>: Borrow<str>`.
|
||||
pub type CellValues = HashMap<std::sync::Arc<str>, f32>;
|
||||
|
||||
/// Lat/lon rounded to 3 decimals. f64 keys can't be hashed directly, so
|
||||
/// we key by `(lat_mdeg, lon_mdeg)` where `*_mdeg = round(x * 1000) as i32`.
|
||||
|
|
@ -224,7 +234,10 @@ pub fn parse_lola_binary(bin: &[u8], messages: &[Message], grid_spec: &GridSpec)
|
|||
continue;
|
||||
}
|
||||
let chunk = &bin[data_offset..data_offset + bytes_per_msg];
|
||||
let key = format!("{}:{}", msg.var, msg.level);
|
||||
// Allocate the "VAR:LEVEL" string once per message. Every
|
||||
// subsequent cell insertion is Arc::clone (one atomic inc),
|
||||
// not a String::clone (heap alloc + copy + free).
|
||||
let key: std::sync::Arc<str> = format!("{}:{}", msg.var, msg.level).into();
|
||||
|
||||
for j in 0..ny {
|
||||
let lat = round3(grid_spec.lat_start + j as f64 * grid_spec.lat_step);
|
||||
|
|
@ -240,7 +253,7 @@ pub fn parse_lola_binary(bin: &[u8], messages: &[Message], grid_spec: &GridSpec)
|
|||
let lon_key = (lon * 1000.0).round() as i32;
|
||||
out.entry((lat_key, lon_key))
|
||||
.or_default()
|
||||
.insert(key.clone(), value);
|
||||
.insert(std::sync::Arc::clone(&key), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,18 +127,25 @@ pub fn build_native_profile(cell: &CellValues) -> Option<NativeProfile> {
|
|||
// or TMP is absent — those levels are junk.
|
||||
let mut levels: Vec<(f64, f64, f64, f64)> = Vec::with_capacity(NATIVE_LEVEL_COUNT as usize);
|
||||
for level in 1..=NATIVE_LEVEL_COUNT {
|
||||
// `cell.get` dispatches via Arc<str>'s Borrow<str>, so pass
|
||||
// &str refs built by `format!(…).as_str()` (or inline &str
|
||||
// concat) — passing `&String` triggers the wrong Borrow impl.
|
||||
let lvl_str = format!("{level} hybrid level");
|
||||
let Some(&hgt) = cell.get(&format!("HGT:{lvl_str}")) else {
|
||||
let hgt_key = format!("HGT:{lvl_str}");
|
||||
let tmp_key = format!("TMP:{lvl_str}");
|
||||
let spfh_key = format!("SPFH:{lvl_str}");
|
||||
let pres_key = format!("PRES:{lvl_str}");
|
||||
let Some(&hgt) = cell.get(hgt_key.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(&tmp) = cell.get(&format!("TMP:{lvl_str}")) else {
|
||||
let Some(&tmp) = cell.get(tmp_key.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
// SPFH / PRES default to 0.0 if absent — the duct math handles
|
||||
// degenerate rows (returns 0.0 gradient / no duct) without
|
||||
// raising, matching Elixir's nil-tolerance.
|
||||
let spfh = cell.get(&format!("SPFH:{lvl_str}")).copied().unwrap_or(0.0);
|
||||
let pres = cell.get(&format!("PRES:{lvl_str}")).copied().unwrap_or(0.0);
|
||||
let spfh = cell.get(spfh_key.as_str()).copied().unwrap_or(0.0);
|
||||
let pres = cell.get(pres_key.as_str()).copied().unwrap_or(0.0);
|
||||
levels.push((hgt as f64, tmp as f64, spfh as f64, pres as f64));
|
||||
}
|
||||
|
||||
|
|
@ -221,14 +228,14 @@ mod tests {
|
|||
fn cell_with_levels(count: usize) -> CellValues {
|
||||
let mut c = CellValues::new();
|
||||
for i in 1..=count {
|
||||
c.insert(format!("HGT:{i} hybrid level"), (i as f32) * 50.0);
|
||||
c.insert(format!("TMP:{i} hybrid level"), 290.0 - (i as f32) * 0.5);
|
||||
c.insert(format!("HGT:{i} hybrid level").into(), (i as f32) * 50.0);
|
||||
c.insert(format!("TMP:{i} hybrid level").into(), 290.0 - (i as f32) * 0.5);
|
||||
c.insert(
|
||||
format!("SPFH:{i} hybrid level"),
|
||||
format!("SPFH:{i} hybrid level").into(),
|
||||
0.008 - (i as f32) * 0.0001,
|
||||
);
|
||||
c.insert(
|
||||
format!("PRES:{i} hybrid level"),
|
||||
format!("PRES:{i} hybrid level").into(),
|
||||
101_000.0 - (i as f32) * 600.0,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue