LUT caching registry for CORDIC tables
This commit is contained in:
parent
97653580e5
commit
6b01b15b56
|
|
@ -1,4 +1,5 @@
|
||||||
use cord_trig::{TrigGraph, TrigOp};
|
use cord_trig::{TrigGraph, TrigOp};
|
||||||
|
use crate::lut::CordicTable;
|
||||||
use crate::ops::*;
|
use crate::ops::*;
|
||||||
|
|
||||||
/// A compiled CORDIC program ready for binary serialization or execution.
|
/// A compiled CORDIC program ready for binary serialization or execution.
|
||||||
|
|
@ -77,12 +78,14 @@ impl CORDICProgram {
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
|
let table = CordicTable::for_word_bits(config.word_bits);
|
||||||
|
|
||||||
CORDICProgram {
|
CORDICProgram {
|
||||||
word_bits: config.word_bits,
|
word_bits: config.word_bits,
|
||||||
instructions,
|
instructions,
|
||||||
output: graph.output,
|
output: graph.output,
|
||||||
atan_table: atan_table(config.word_bits),
|
atan_table: table.atan.to_vec(),
|
||||||
gain: cordic_gain(config.word_bits, frac_bits),
|
gain: table.gain,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,28 @@
|
||||||
|
use std::sync::Arc;
|
||||||
use cord_trig::{TrigGraph, TrigOp};
|
use cord_trig::{TrigGraph, TrigOp};
|
||||||
|
use crate::lut::CordicTable;
|
||||||
|
|
||||||
/// CORDIC evaluator: evaluates a TrigGraph using only integer
|
/// CORDIC evaluator: evaluates a TrigGraph using only integer
|
||||||
/// shifts, adds, and comparisons. No floating point trig.
|
/// shifts, adds, and comparisons. No floating point trig.
|
||||||
///
|
///
|
||||||
/// Proof that the entire pipeline compiles down to
|
/// Proof that the entire pipeline compiles down to
|
||||||
/// binary arithmetic — shift, add, compare, repeat.
|
/// binary arithmetic -- shift, add, compare, repeat.
|
||||||
pub struct CORDICEvaluator {
|
pub struct CORDICEvaluator {
|
||||||
word_bits: u8,
|
word_bits: u8,
|
||||||
frac_bits: u8,
|
frac_bits: u8,
|
||||||
atan_table: Vec<i64>,
|
atan_table: Arc<[i64]>,
|
||||||
gain: i64,
|
gain: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CORDICEvaluator {
|
impl CORDICEvaluator {
|
||||||
pub fn new(word_bits: u8) -> Self {
|
pub fn new(word_bits: u8) -> Self {
|
||||||
let frac_bits = word_bits - 1;
|
let table = CordicTable::for_word_bits(word_bits);
|
||||||
let iterations = word_bits;
|
CORDICEvaluator {
|
||||||
|
word_bits,
|
||||||
// Precompute atan(2^-i) as fixed-point
|
frac_bits: table.frac_bits,
|
||||||
let atan_table: Vec<i64> = (0..iterations)
|
atan_table: table.atan,
|
||||||
.map(|i| {
|
gain: table.gain,
|
||||||
let angle = (2.0f64).powi(-(i as i32)).atan();
|
|
||||||
(angle * (1i64 << frac_bits) as f64).round() as i64
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// CORDIC gain K = product of 1/sqrt(1 + 2^{-2i})
|
|
||||||
let mut k = 1.0f64;
|
|
||||||
for i in 0..iterations {
|
|
||||||
k *= 1.0 / (1.0 + (2.0f64).powi(-2 * i as i32)).sqrt();
|
|
||||||
}
|
}
|
||||||
let gain = (k * (1i64 << frac_bits) as f64).round() as i64;
|
|
||||||
|
|
||||||
CORDICEvaluator { word_bits, frac_bits, atan_table, gain }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert f64 to fixed-point.
|
/// Convert f64 to fixed-point.
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
pub mod compiler;
|
pub mod compiler;
|
||||||
pub mod ops;
|
pub mod ops;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
|
pub mod lut;
|
||||||
|
|
||||||
pub use compiler::CORDICProgram;
|
pub use compiler::CORDICProgram;
|
||||||
pub use eval::CORDICEvaluator;
|
pub use eval::CORDICEvaluator;
|
||||||
|
pub use lut::CordicTable;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,114 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
|
/// Pre-computed CORDIC table: atan values + gain for a given iteration count.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct CordicTable {
|
||||||
|
pub atan: Arc<[i64]>,
|
||||||
|
pub gain: i64,
|
||||||
|
pub iterations: u8,
|
||||||
|
pub frac_bits: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Global table registry. Keyed by (iterations, frac_bits) so any
|
||||||
|
/// combination of word width and iteration count shares a single
|
||||||
|
/// allocation. Thread-safe via interior Mutex on the map only;
|
||||||
|
/// once an Arc is cloned out, no lock is held during evaluation.
|
||||||
|
struct LutRegistry {
|
||||||
|
tables: Mutex<HashMap<(u8, u8), CordicTable>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static REGISTRY: OnceLock<LutRegistry> = OnceLock::new();
|
||||||
|
|
||||||
|
fn registry() -> &'static LutRegistry {
|
||||||
|
REGISTRY.get_or_init(|| LutRegistry {
|
||||||
|
tables: Mutex::new(HashMap::new()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CordicTable {
|
||||||
|
/// Retrieve or compute the CORDIC table for the given parameters.
|
||||||
|
/// Repeated calls with the same (iterations, frac_bits) return
|
||||||
|
/// a clone of the same Arc'd data -- no recomputation.
|
||||||
|
pub fn get(iterations: u8, frac_bits: u8) -> Self {
|
||||||
|
let reg = registry();
|
||||||
|
let key = (iterations, frac_bits);
|
||||||
|
|
||||||
|
let mut map = reg.tables.lock().unwrap();
|
||||||
|
if let Some(cached) = map.get(&key) {
|
||||||
|
return cached.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let table = Self::compute(iterations, frac_bits);
|
||||||
|
map.insert(key, table.clone());
|
||||||
|
table
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convenience: derive iterations and frac_bits from word_bits
|
||||||
|
/// using the standard CORDIC convention (iterations = word_bits,
|
||||||
|
/// frac_bits = word_bits - 1).
|
||||||
|
pub fn for_word_bits(word_bits: u8) -> Self {
|
||||||
|
Self::get(word_bits, word_bits - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute(iterations: u8, frac_bits: u8) -> Self {
|
||||||
|
let scale = (1i64 << frac_bits) as f64;
|
||||||
|
|
||||||
|
let atan: Arc<[i64]> = (0..iterations)
|
||||||
|
.map(|i| {
|
||||||
|
let angle = (2.0f64).powi(-(i as i32)).atan();
|
||||||
|
(angle * scale).round() as i64
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into();
|
||||||
|
|
||||||
|
let mut k = 1.0f64;
|
||||||
|
for i in 0..iterations {
|
||||||
|
k *= 1.0 / (1.0 + (2.0f64).powi(-2 * i as i32)).sqrt();
|
||||||
|
}
|
||||||
|
let gain = (k * scale).round() as i64;
|
||||||
|
|
||||||
|
CordicTable { atan, gain, iterations, frac_bits }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ops;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cached_matches_raw() {
|
||||||
|
for bits in [8u8, 16, 32, 48, 64] {
|
||||||
|
let table = CordicTable::for_word_bits(bits);
|
||||||
|
let raw_atan = ops::atan_table(bits);
|
||||||
|
let raw_gain = ops::cordic_gain(bits, bits - 1);
|
||||||
|
|
||||||
|
assert_eq!(table.atan.len(), raw_atan.len(), "len mismatch at {bits}");
|
||||||
|
assert_eq!(&*table.atan, &raw_atan[..], "atan mismatch at {bits}");
|
||||||
|
assert_eq!(table.gain, raw_gain, "gain mismatch at {bits}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deduplication() {
|
||||||
|
let a = CordicTable::for_word_bits(32);
|
||||||
|
let b = CordicTable::for_word_bits(32);
|
||||||
|
assert!(Arc::ptr_eq(&a.atan, &b.atan));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn different_widths_distinct() {
|
||||||
|
let a = CordicTable::for_word_bits(16);
|
||||||
|
let b = CordicTable::for_word_bits(32);
|
||||||
|
assert!(!Arc::ptr_eq(&a.atan, &b.atan));
|
||||||
|
assert_ne!(a.atan.len(), b.atan.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn custom_iteration_count() {
|
||||||
|
let t = CordicTable::get(16, 31);
|
||||||
|
assert_eq!(t.atan.len(), 16);
|
||||||
|
assert_eq!(t.frac_bits, 31);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue