From c85daae608833b6096c27129d96d22f59364a03e Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 13 Sep 2026 09:53:04 +0800 Subject: [PATCH] perf(data): inline numeric factor keys and preserve borrowed static names --- crates/fidc-core/src/data.rs | 2 +- crates/fidc-core/src/numeric_factors.rs | 77 ++++++++++++++++++++----- 2 files changed, 64 insertions(+), 15 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 8f6f838..6200e38 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -4411,7 +4411,7 @@ fn normalize_factor_snapshots( { Some((field, value)) } else { - Some((Cow::Owned(trimmed.to_ascii_lowercase()), value)) + Some((CompactString::from(trimmed.to_ascii_lowercase()), value)) } }) .collect(); diff --git a/crates/fidc-core/src/numeric_factors.rs b/crates/fidc-core/src/numeric_factors.rs index 75c5c86..81ac5a6 100644 --- a/crates/fidc-core/src/numeric_factors.rs +++ b/crates/fidc-core/src/numeric_factors.rs @@ -3,6 +3,7 @@ use std::collections::BTreeMap; use std::fmt; use std::ops::Index; +use compact_str::CompactString; use serde::de::{MapAccess, Visitor}; use serde::ser::SerializeMap; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -10,7 +11,14 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer}; /// Sorted numeric fields stored contiguously, without a tree node per snapshot. #[derive(Clone, Default, PartialEq)] pub struct NumericFactorMap { - entries: Vec<(Cow<'static, str>, f64)>, + entries: Vec<(CompactString, f64)>, +} + +fn compact_key(key: Cow<'static, str>) -> CompactString { + match key { + Cow::Borrowed(value) => CompactString::const_new(value), + Cow::Owned(value) => CompactString::from(value), + } } impl NumericFactorMap { @@ -49,6 +57,10 @@ impl NumericFactorMap { } pub fn insert(&mut self, key: Cow<'static, str>, value: f64) -> Option { + self.insert_compact(compact_key(key), value) + } + + pub fn insert_compact(&mut self, key: CompactString, value: f64) -> Option { if self .entries .last() @@ -76,14 +88,14 @@ impl NumericFactorMap { .map(|index| self.entries.remove(index).1) } - pub fn retain(&mut self, mut keep: impl FnMut(&Cow<'static, str>, &mut f64) -> bool) { + pub fn retain(&mut self, mut keep: impl FnMut(&CompactString, &mut f64) -> bool) { self.entries.retain_mut(|(key, value)| keep(key, value)); } pub fn iter(&self) -> Iter<'_> { Iter(self.entries.iter()) } - pub fn keys(&self) -> impl DoubleEndedIterator> + ExactSizeIterator { + pub fn keys(&self) -> impl DoubleEndedIterator + ExactSizeIterator { self.entries.iter().map(|(key, _)| key) } pub fn values(&self) -> impl DoubleEndedIterator + ExactSizeIterator { @@ -104,9 +116,9 @@ impl Index<&str> for NumericFactorMap { } } -pub struct Iter<'a>(std::slice::Iter<'a, (Cow<'static, str>, f64)>); +pub struct Iter<'a>(std::slice::Iter<'a, (CompactString, f64)>); impl<'a> Iterator for Iter<'a> { - type Item = (&'a Cow<'static, str>, &'a f64); + type Item = (&'a CompactString, &'a f64); fn next(&mut self) -> Option { self.0.next().map(|(k, v)| (k, v)) } @@ -121,14 +133,14 @@ impl DoubleEndedIterator for Iter<'_> { } impl ExactSizeIterator for Iter<'_> {} impl<'a> IntoIterator for &'a NumericFactorMap { - type Item = (&'a Cow<'static, str>, &'a f64); + type Item = (&'a CompactString, &'a f64); type IntoIter = Iter<'a>; fn into_iter(self) -> Self::IntoIter { self.iter() } } impl IntoIterator for NumericFactorMap { - type Item = (Cow<'static, str>, f64); + type Item = (CompactString, f64); type IntoIter = std::vec::IntoIter; fn into_iter(self) -> Self::IntoIter { self.entries.into_iter() @@ -137,6 +149,11 @@ impl IntoIterator for NumericFactorMap { impl FromIterator<(Cow<'static, str>, f64)> for NumericFactorMap { fn from_iter, f64)>>(iter: T) -> Self { + iter.into_iter().map(|(key, value)| (compact_key(key), value)).collect() + } +} +impl FromIterator<(CompactString, f64)> for NumericFactorMap { + fn from_iter>(iter: T) -> Self { let mut entries: Vec<_> = iter.into_iter().collect(); // Stable sorting preserves last-value-wins for repeated input keys. if !entries.windows(2).all(|pair| pair[0].0 <= pair[1].0) { @@ -155,6 +172,11 @@ impl FromIterator<(Cow<'static, str>, f64)> for NumericFactorMap { } impl Extend<(Cow<'static, str>, f64)> for NumericFactorMap { fn extend, f64)>>(&mut self, iter: T) { + self.extend(iter.into_iter().map(|(key, value)| (compact_key(key), value))); + } +} +impl Extend<(CompactString, f64)> for NumericFactorMap { + fn extend>(&mut self, iter: T) { let mut incoming: Self = iter.into_iter().collect(); if incoming.is_empty() { return; @@ -194,9 +216,7 @@ impl From<[(Cow<'static, str>, f64); N]> for NumericFactorMap { } impl From, f64>> for NumericFactorMap { fn from(entries: BTreeMap, f64>) -> Self { - Self { - entries: entries.into_iter().collect(), - } + entries.into_iter().collect() } } @@ -219,8 +239,8 @@ impl<'de> Deserialize<'de> for NumericFactorMap { } fn visit_map>(self, mut map: A) -> Result { let mut entries = Vec::new(); - while let Some((key, value)) = map.next_entry::()? { - entries.push((Cow::Owned(key), value)); + while let Some((key, value)) = map.next_entry::()? { + entries.push((key, value)); } Ok(entries.into_iter().collect()) } @@ -233,6 +253,34 @@ impl<'de> Deserialize<'de> for NumericFactorMap { mod tests { use super::*; + #[test] + fn compact_keys_inline_dynamic_names_and_keep_long_static_storage() { + const LONG: &str = "a_long_static_factor_identifier_that_must_remain_borrowed"; + let map = NumericFactorMap::from([ + (Cow::Owned("dynamic_factor_20".to_owned()), -0.0), + (Cow::Borrowed(LONG), 1.0), + ]); + let cloned = map.clone(); + let short = cloned.keys().find(|key| key.as_str() == "dynamic_factor_20").unwrap(); + assert!(!short.is_heap_allocated()); + let long = cloned.keys().find(|key| key.as_str() == LONG).unwrap(); + assert_eq!(long.as_static_str(), Some(LONG)); + assert_eq!(cloned["dynamic_factor_20"].to_bits(), (-0.0_f64).to_bits()); + assert_eq!(std::mem::size_of::<(CompactString, f64)>(), std::mem::size_of::<(Cow<'static, str>, f64)>()); + } + + #[test] + fn long_dynamic_unicode_and_short_keys_keep_the_same_json_map() { + let entries = ["", "a", "a_field_longer_than_the_inline_string_capacity", "价格因子", "ths_up_days_stock"] + .into_iter().enumerate().map(|(index, key)| (Cow::Owned(key.to_string()), index as f64 + 0.25)) + .collect::>(); + let map = NumericFactorMap::from(entries.clone()); + assert_eq!(serde_json::to_string(&map).unwrap(), serde_json::to_string(&entries).unwrap()); + let decoded: NumericFactorMap = serde_json::from_str(&serde_json::to_string(&map).unwrap()).unwrap(); + assert_eq!(decoded, map); + assert!(!decoded.keys().find(|key| key.as_str() == "ths_up_days_stock").unwrap().is_heap_allocated()); + } + #[test] fn updates_order_removal_and_values_match_tree_map() { let mut flat = NumericFactorMap::new(); @@ -330,10 +378,11 @@ mod tests { flat.iter().collect::>(), tree.iter().collect::>() ); - assert!(matches!(flat.keys().last(), Some(Cow::Borrowed("shared")))); + assert_eq!(flat.keys().last().map(CompactString::as_str), Some("shared")); + assert!(!flat.keys().last().unwrap().is_heap_allocated()); flat.extend([(Cow::Borrowed("zz"), f64::NAN)]); assert!(flat["zz"].is_nan()); - flat.extend(std::iter::empty()); + flat.extend(std::iter::empty::<(CompactString, f64)>()); assert_eq!(flat.len(), tree.len() + 1); } }