From 8ed22897eac506297c9ab4023140fbb6cfa0cc26 Mon Sep 17 00:00:00 2001 From: boris Date: Fri, 4 Sep 2026 22:46:14 +0800 Subject: [PATCH] perf: index instruments by symbol id --- crates/fidc-core/src/data.rs | 99 +++++++++++++++++++ .../fidc-core/src/platform_expr_strategy.rs | 6 +- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index b3c7a06..33d3ec4 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -1301,6 +1301,7 @@ impl BenchmarkPriceSeries { #[derive(Debug, Clone)] pub struct DataSet { instruments: Arc>, + instruments_by_symbol_id: Arc>>, calendar: Arc, market_by_date: Arc>>, market_symbol_ids_by_date: Arc>>, @@ -1766,6 +1767,12 @@ impl DataSet { &factor_by_date, &candidate_by_date, ); + let mut instruments_by_symbol_id = vec![None; symbol_id_by_code.len()]; + for (symbol, instrument) in &instruments { + if let Some(symbol_id) = symbol_id_by_code.get(symbol).copied() { + instruments_by_symbol_id[symbol_id as usize] = Some(instrument.clone()); + } + } let market_symbol_ids_by_date = build_group_symbol_ids(&market_by_date, &symbol_id_by_code, |item| { item.symbol.as_str() @@ -1818,6 +1825,7 @@ impl DataSet { Ok(Self { instruments: Arc::new(instruments), + instruments_by_symbol_id: Arc::new(instruments_by_symbol_id), calendar: Arc::new(calendar), market_by_date: Arc::new(market_by_date), market_symbol_ids_by_date: Arc::new(market_symbol_ids_by_date), @@ -1887,6 +1895,12 @@ impl DataSet { self.instruments.get(symbol) } + pub(crate) fn instrument_by_symbol_id(&self, symbol_id: u32) -> Option<&Instrument> { + self.instruments_by_symbol_id + .get(symbol_id as usize)? + .as_ref() + } + pub fn symbol_id(&self, symbol: &str) -> Option { self.symbol_id_by_code.get(symbol).copied() } @@ -4888,6 +4902,11 @@ mod tests { for symbol in ["000001.SZ", "600000.SH"] { let symbol_id = data.symbol_id(symbol).unwrap(); let day = data.daily_snapshot_view(date); + assert_eq!( + data.instrument_by_symbol_id(symbol_id) + .map(|row| row.symbol.as_str()), + Some(symbol) + ); assert_eq!( data.market_by_symbol_id(date, symbol_id) .map(|row| row.symbol.as_str()), @@ -4923,6 +4942,11 @@ mod tests { assert!(data.factor_by_symbol_id(date, signal_id).is_none()); assert!(data.candidate_by_symbol_id(date, signal_id).is_none()); assert!(day.candidate(signal_id).is_none()); + assert_eq!( + data.instrument_by_symbol_id(signal_id) + .map(|row| row.symbol.as_str()), + Some("000300.SH") + ); // `get_factor` must use the same symbol-id index as direct snapshot // lookups. Sparse factor rows must not accidentally select another @@ -4945,6 +4969,81 @@ mod tests { ); } + #[test] + #[ignore = "manual release-mode instrument lookup benchmark"] + fn benchmark_instrument_symbol_id_lookup() { + let rows = (0..6_000u32) + .map(|symbol_id| { + let symbol = format!("{:06}.SZ", symbol_id); + let instrument = Instrument { + symbol: symbol.clone(), + name: symbol.clone(), + board: "SZ".to_string(), + round_lot: 100, + listed_at: None, + delisted_at: None, + status: "active".to_string(), + }; + (symbol, symbol_id, instrument) + }) + .collect::>(); + let map = rows + .iter() + .map(|(symbol, _, instrument)| (symbol.clone(), instrument.clone())) + .collect::>(); + let mut dense = vec![None; rows.len()]; + for (_, symbol_id, instrument) in &rows { + dense[*symbol_id as usize] = Some(instrument.clone()); + } + let iterations = 1_000usize; + let mut map_nanos = 0u128; + let mut dense_nanos = 0u128; + let mut map_checksum = 0u64; + let mut dense_checksum = 0u64; + + for iteration in 0..iterations { + if iteration % 2 == 0 { + let started = std::time::Instant::now(); + for (symbol, _, _) in &rows { + map_checksum += map.get(symbol).unwrap().round_lot as u64; + } + map_nanos += started.elapsed().as_nanos(); + let started = std::time::Instant::now(); + for (_, symbol_id, _) in &rows { + dense_checksum += dense[*symbol_id as usize].as_ref().unwrap().round_lot as u64; + } + dense_nanos += started.elapsed().as_nanos(); + } else { + let started = std::time::Instant::now(); + for (_, symbol_id, _) in &rows { + dense_checksum += dense[*symbol_id as usize].as_ref().unwrap().round_lot as u64; + } + dense_nanos += started.elapsed().as_nanos(); + let started = std::time::Instant::now(); + for (symbol, _, _) in &rows { + map_checksum += map.get(symbol).unwrap().round_lot as u64; + } + map_nanos += started.elapsed().as_nanos(); + } + } + + assert_eq!(map_checksum, dense_checksum); + let map_seconds = map_nanos as f64 / 1_000_000_000.0; + let dense_seconds = dense_nanos as f64 / 1_000_000_000.0; + eprintln!( + "{}", + serde_json::json!({ + "schemaVersion": "fidc-instrument-symbol-id-lookup-benchmark/v1", + "rows": rows.len(), + "iterations": iterations, + "mapSeconds": map_seconds, + "denseSeconds": dense_seconds, + "speedup": map_seconds / dense_seconds, + "checksum": map_checksum, + }) + ); + } + #[test] #[ignore = "manual component benchmark"] fn benchmark_daily_snapshot_view_lookup() { diff --git a/crates/fidc-core/src/platform_expr_strategy.rs b/crates/fidc-core/src/platform_expr_strategy.rs index 2d6159d..c7c86d0 100644 --- a/crates/fidc-core/src/platform_expr_strategy.rs +++ b/crates/fidc-core/src/platform_expr_strategy.rs @@ -4064,7 +4064,7 @@ impl PlatformExprStrategy { } else { None }; - let instrument = ctx.data.instrument(symbol); + let instrument = ctx.data.instrument_by_symbol_id(symbol_id); let required_rolling = |field: &'static str, lookback: usize| { self.stock_rolling_requirements .requires(field, lookback) @@ -9249,7 +9249,7 @@ impl PlatformExprStrategy { date, candidate, market, - ctx.data.instrument(&factor.symbol), + ctx.data.instrument_by_symbol_id(symbol_id), &self.config.risk_config, ); let rejected = decision.as_ref().is_some_and(|decision| { @@ -9261,7 +9261,7 @@ impl PlatformExprStrategy { date, candidate, market, - ctx.data.instrument(&factor.symbol), + ctx.data.instrument_by_symbol_id(symbol_id), &self.config.risk_config, ); (