From c85daae608833b6096c27129d96d22f59364a03e Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 13 Sep 2026 09:53:04 +0800 Subject: [PATCH 1/3] 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); } } From 0ff90c432920735660cad1c6ff3fd5774cae222c Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 13 Sep 2026 09:55:15 +0800 Subject: [PATCH 2/3] refactor(data): use explicit string views for compact numeric names --- crates/fidc-core/src/data.rs | 8 ++++---- crates/fidc-core/src/numeric_factors.rs | 10 +++++----- crates/fidc-core/src/platform_expr_strategy.rs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 6200e38..82dd92d 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -4390,9 +4390,9 @@ fn normalize_factor_snapshots( }); } let already_normalized = snapshot.extra_factors.iter().all(|(field, value)| { - let trimmed = field.as_ref().trim().trim_matches('"').trim_matches('\''); + let trimmed = field.as_str().trim().trim_matches('"').trim_matches('\''); !trimmed.is_empty() - && trimmed == field.as_ref() + && trimmed == field.as_str() && trimmed.bytes().all(|byte| !byte.is_ascii_uppercase()) && value.is_finite() }); @@ -4403,10 +4403,10 @@ fn normalize_factor_snapshots( .extra_factors .into_iter() .filter_map(|(field, value)| { - let trimmed = field.as_ref().trim().trim_matches('"').trim_matches('\''); + let trimmed = field.as_str().trim().trim_matches('"').trim_matches('\''); if trimmed.is_empty() || !value.is_finite() { None - } else if trimmed == field.as_ref() + } else if trimmed == field.as_str() && trimmed.bytes().all(|byte| !byte.is_ascii_uppercase()) { Some((field, value)) diff --git a/crates/fidc-core/src/numeric_factors.rs b/crates/fidc-core/src/numeric_factors.rs index 81ac5a6..f89bba9 100644 --- a/crates/fidc-core/src/numeric_factors.rs +++ b/crates/fidc-core/src/numeric_factors.rs @@ -40,14 +40,14 @@ impl NumericFactorMap { pub fn get(&self, key: &str) -> Option<&f64> { self.entries - .binary_search_by(|(name, _)| name.as_ref().cmp(key)) + .binary_search_by(|(name, _)| name.as_str().cmp(key)) .ok() .map(|index| &self.entries[index].1) } pub fn get_mut(&mut self, key: &str) -> Option<&mut f64> { self.entries - .binary_search_by(|(name, _)| name.as_ref().cmp(key)) + .binary_search_by(|(name, _)| name.as_str().cmp(key)) .ok() .map(|index| &mut self.entries[index].1) } @@ -64,14 +64,14 @@ impl NumericFactorMap { if self .entries .last() - .is_none_or(|(last, _)| last.as_ref() < key.as_ref()) + .is_none_or(|(last, _)| last.as_str() < key.as_str()) { self.entries.push((key, value)); return None; } match self .entries - .binary_search_by(|(name, _)| name.as_ref().cmp(key.as_ref())) + .binary_search_by(|(name, _)| name.as_str().cmp(key.as_str())) { Ok(index) => Some(std::mem::replace(&mut self.entries[index].1, value)), Err(index) => { @@ -83,7 +83,7 @@ impl NumericFactorMap { pub fn remove(&mut self, key: &str) -> Option { self.entries - .binary_search_by(|(name, _)| name.as_ref().cmp(key)) + .binary_search_by(|(name, _)| name.as_str().cmp(key)) .ok() .map(|index| self.entries.remove(index).1) } diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 35fe906..1f9d471 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -4905,7 +4905,7 @@ impl PlatformExprStrategy { .iter() .filter(|(field, _)| { self.stock_extra_factor_map_required - || self.stock_extra_factor_identifiers.contains(field.as_ref()) + || self.stock_extra_factor_identifiers.contains(field.as_str()) }) .map(|(field, value)| (field.clone(), *value)) .collect() @@ -5653,7 +5653,7 @@ impl PlatformExprStrategy { Dynamic::from(stock.stock_volume_ma100), ); for (key, value) in &stock.extra_factors { - factors.insert(key.as_ref().into(), Dynamic::from(*value)); + factors.insert(key.as_str().into(), Dynamic::from(*value)); } for (key, value) in &stock.extra_text_factors { factors.insert(key.clone().into(), Dynamic::from(value.clone())); @@ -17561,7 +17561,7 @@ mod tests { assert_eq!(copied_state.extra_factors["negative_zero"].to_bits(), (-0.0_f64).to_bits()); assert!(copied_state.extra_factors["undefined_value"].is_nan()); let exposed = copied_state.extra_factors.iter() - .map(|(key, value)| (key.as_ref().into(), Dynamic::from(*value))) + .map(|(key, value)| (key.as_str().into(), Dynamic::from(*value))) .collect::(); assert_eq!(exposed["negative_zero"].as_float().unwrap().to_bits(), (-0.0_f64).to_bits()); assert!(exposed["undefined_value"].as_float().unwrap().is_nan()); From fe05384f80d281ca74a49b3fde8d2706d44db02d Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 13 Sep 2026 09:57:46 +0800 Subject: [PATCH 3/3] test(data): compare compact names by value and allocation class --- crates/fidc-core/src/data.rs | 6 ++---- crates/fidc-core/src/numeric_factors.rs | 16 ++++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index 82dd92d..c519304 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -6470,10 +6470,8 @@ mod tests { extra_factors: From::from([(Cow::Borrowed("amount"), 10.0)]), }]) .expect("normalize clean factor snapshot"); - assert!(matches!( - clean[0].extra_factors.keys().next(), - Some(Cow::Borrowed("amount")) - )); + assert_eq!(clean[0].extra_factors.keys().next().map(CompactString::as_str), Some("amount")); + assert!(!clean[0].extra_factors.keys().next().unwrap().is_heap_allocated()); let dirty = normalize_factor_snapshots(vec![DailyFactorSnapshot { date, diff --git a/crates/fidc-core/src/numeric_factors.rs b/crates/fidc-core/src/numeric_factors.rs index f89bba9..be44e83 100644 --- a/crates/fidc-core/src/numeric_factors.rs +++ b/crates/fidc-core/src/numeric_factors.rs @@ -297,14 +297,14 @@ mod tests { } } assert_eq!( - flat.iter().collect::>(), - tree.iter().collect::>() + flat.iter().map(|(key, value)| (key.as_str(), value)).collect::>(), + tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::>() ); flat.retain(|_, value| *value > 100.0); tree.retain(|_, value| *value > 100.0); assert_eq!( - flat.iter().collect::>(), - tree.iter().collect::>() + flat.iter().map(|(key, value)| (key.as_str(), value)).collect::>(), + tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::>() ); assert_eq!( std::mem::size_of::(), @@ -323,8 +323,8 @@ mod tests { let flat: NumericFactorMap = input.clone().into_iter().collect(); let tree: BTreeMap<_, _> = input.into_iter().collect(); assert_eq!( - flat.iter().collect::>(), - tree.iter().collect::>() + flat.iter().map(|(key, value)| (key.as_str(), value)).collect::>(), + tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::>() ); assert_eq!(flat["z"], 4.0); } @@ -375,8 +375,8 @@ mod tests { flat.extend(incoming.clone()); tree.extend(incoming); assert_eq!( - flat.iter().collect::>(), - tree.iter().collect::>() + flat.iter().map(|(key, value)| (key.as_str(), value)).collect::>(), + tree.iter().map(|(key, value)| (key.as_ref(), value)).collect::>() ); assert_eq!(flat.keys().last().map(CompactString::as_str), Some("shared")); assert!(!flat.keys().last().unwrap().is_heap_allocated());