-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
opt: Strategies & Numeric Literal Optimization
- Loading branch information
Showing
9 changed files
with
648 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
mod expressions; | ||
mod strategies; | ||
|
||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] | ||
pub enum Priority { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
//! # Strategies | ||
//! Sometimes a decision needs to be made between several competing ways of doing the same thing. | ||
//! | ||
//! `Strategy` provides a systematic way to compare these alternatives so that adding a new strategy | ||
//! is easy. See [`numeric_literal`] for an example of how `Strategy` can be used to implement a | ||
//! peephole optimization for numeric literals. | ||
mod numeric_literal; | ||
|
||
use crate::optimize::Priority; | ||
use crate::parse::Reconstruct; | ||
use crate::Config; | ||
use std::cmp::Ordering; | ||
use titokens::Token; | ||
|
||
pub trait Strategy<T>: Reconstruct { | ||
fn exists(&self) -> bool; | ||
|
||
/// The exact number of bytes that this `Strategy` would use. | ||
fn size_cost(&self) -> Option<usize>; | ||
/// Estimation of the average clock cycles that this `Strategy` would use. | ||
fn speed_cost(&self) -> Option<u32>; | ||
} | ||
|
||
impl<T> Strategy<T> for Box<dyn Strategy<T>> { | ||
fn exists(&self) -> bool { | ||
(**self).exists() | ||
} | ||
|
||
fn size_cost(&self) -> Option<usize> { | ||
(**self).size_cost() | ||
} | ||
|
||
fn speed_cost(&self) -> Option<u32> { | ||
(**self).speed_cost() | ||
} | ||
} | ||
|
||
impl<T> Reconstruct for Box<dyn Strategy<T>> { | ||
fn reconstruct(&self, config: &Config) -> Vec<Token> { | ||
(**self).reconstruct(config) | ||
} | ||
} | ||
|
||
/// Compare two `Strategies`. This function makes the resource-allocation decision for balancing | ||
/// speed and size under [neutral](Priority::Neutral) optimization | ||
fn partial_cmp<T>( | ||
a: &dyn Strategy<T>, | ||
b: &dyn Strategy<T>, | ||
priority: Priority, | ||
) -> Option<Ordering> { | ||
match priority { | ||
Priority::Neutral => { | ||
let my_cost = (a.size_cost()? as u64).saturating_mul(a.speed_cost()? as u64); | ||
let other_cost = (b.size_cost()? as u64).saturating_mul(b.speed_cost()? as u64); | ||
|
||
Some(my_cost.cmp(&other_cost)) | ||
} | ||
Priority::Speed => a.speed_cost().partial_cmp(&b.speed_cost()), | ||
Priority::Size => a.size_cost().partial_cmp(&b.size_cost()), | ||
} | ||
} | ||
|
||
impl<T> Reconstruct for Vec<Box<dyn Strategy<T>>> { | ||
fn reconstruct(&self, config: &Config) -> Vec<Token> { | ||
self.iter() | ||
.filter(|&x| x.exists()) | ||
.min_by(|&a, &b| { | ||
partial_cmp(a, b, config.priority) | ||
.expect("Strategy which `exists` returned `None` for a `_cost`.") | ||
}) | ||
.map(|x| x.reconstruct(config)) | ||
.expect("No strategies were available!") | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
ti-basic-optimizer/src/optimize/strategies/numeric_literal/color_constant.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
//! # Color Constant | ||
//! When available, colors are substantially faster than writing out numerals, because looking up | ||
//! the float value from a static memory location is less expensive than parsing two digits. | ||
use crate::optimize::strategies::Strategy; | ||
use crate::parse::Reconstruct; | ||
use crate::Config; | ||
use tifloats::{tifloat, Float}; | ||
use titokens::{Token, Version}; | ||
|
||
pub(super) struct ColorConstant { | ||
item: Float, | ||
version: Version, | ||
} | ||
|
||
impl ColorConstant { | ||
pub(crate) fn new(item: Float, version: &Version) -> Self { | ||
Self { | ||
item, | ||
version: version.clone(), | ||
} | ||
} | ||
} | ||
|
||
impl Strategy<Float> for ColorConstant { | ||
fn exists(&self) -> bool { | ||
(self.version >= *titokens::version::EARLIEST_COLOR) | ||
&& (tifloat!(0x0010000000000000 * 10 ^ 1)..=tifloat!(0x0024000000000000 * 10 ^ 1)) | ||
.contains(&self.item) | ||
} | ||
|
||
fn size_cost(&self) -> Option<usize> { | ||
self.exists().then_some(2) | ||
} | ||
|
||
fn speed_cost(&self) -> Option<u32> { | ||
self.exists().then_some(5898) | ||
} | ||
} | ||
|
||
impl Reconstruct for ColorConstant { | ||
fn reconstruct(&self, config: &Config) -> Vec<Token> { | ||
assert!(self.exists()); | ||
|
||
let sig_figs = self.item.significant_figures(); | ||
let lower_byte = | ||
(0x41 - 10) + sig_figs[0] * 10 + if sig_figs.len() == 2 { sig_figs[1] } else { 0 }; | ||
assert!((0x41..=0x4F).contains(&lower_byte)); | ||
|
||
vec![Token::TwoByte(0xEF, lower_byte)] | ||
} | ||
} |
133 changes: 133 additions & 0 deletions
133
ti-basic-optimizer/src/optimize/strategies/numeric_literal/fpart_with_exponent.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
//! # Decimal-with-Exponent Representation | ||
//! Attempt to put the float into the form `.<mantissa>|E<exponent>`, where all the significant | ||
//! figures are placed behind the decimal point. Even though the initial parsing of the decimal point | ||
//! is slow, digits are parsed much faster after a decimal point. This is only rarely chosen, usually | ||
//! when [Priority::Speed](crate::Priority::Speed) is selected. | ||
//! | ||
//! Example: `1234` becomes `.1234|E4` | ||
use super::write_digits::WriteDigits; | ||
use crate::optimize::strategies::numeric_literal::integer_with_exponent::IntegerWithExponent; | ||
use crate::optimize::strategies::Strategy; | ||
use crate::parse::Reconstruct; | ||
use crate::Config; | ||
use tifloats::Float; | ||
use titokens::Token; | ||
|
||
pub(super) struct FPartWithExponent { | ||
original: Float, | ||
adjusted: Float, | ||
} | ||
|
||
impl FPartWithExponent { | ||
fn adjust(item: &Float) -> Float { | ||
item.shift(-item.exponent() - 1) | ||
} | ||
|
||
pub fn new(item: Float) -> Self { | ||
Self { | ||
original: item, | ||
adjusted: Self::adjust(&item), | ||
} | ||
} | ||
} | ||
|
||
impl Strategy<Float> for FPartWithExponent { | ||
fn exists(&self) -> bool { | ||
(-99..=99).contains(&(self.original.exponent() - self.adjusted.exponent())) | ||
} | ||
|
||
fn size_cost(&self) -> Option<usize> { | ||
self.exists().then(|| { | ||
let negation_cost = if self.original.is_negative() { 1 } else { 0 }; | ||
|
||
let sig_figs = self.original.significant_figures().len(); | ||
let shift = self.original.exponent() - self.adjusted.exponent(); | ||
|
||
let exponent_cost = match shift { | ||
0..=9 => 1, | ||
-9..=-1 | 10..=99 => 2, | ||
-99..=-10 => 3, | ||
_ => unreachable!(), | ||
}; | ||
|
||
negation_cost + 1 + sig_figs + 1 + exponent_cost | ||
}) | ||
} | ||
|
||
fn speed_cost(&self) -> Option<u32> { | ||
self.exists().then(|| { | ||
// WriteDigits always exists | ||
let mantissa_cost = WriteDigits::new(self.adjusted).speed_cost().unwrap(); | ||
|
||
let required_shift = self.original.exponent() - self.adjusted.exponent(); | ||
// IntegerWithExponent::exponent_speed_cost is always Some if FPartWithExponent exists | ||
let exponent_cost = IntegerWithExponent::exponent_speed_cost(required_shift).unwrap(); | ||
|
||
mantissa_cost + exponent_cost | ||
}) | ||
} | ||
} | ||
|
||
impl Reconstruct for FPartWithExponent { | ||
fn reconstruct(&self, config: &Config) -> Vec<Token> { | ||
assert!(self.exists()); | ||
|
||
let mut result = WriteDigits::new(self.adjusted).reconstruct(config); | ||
|
||
result.push(Token::OneByte(0x3B)); | ||
|
||
let mut exponent = self.original.exponent() - self.adjusted.exponent(); | ||
if exponent < 0 { | ||
result.push(Token::OneByte(0xB0)); | ||
exponent = exponent.abs(); | ||
} | ||
|
||
if exponent > 10 { | ||
result.push(Token::OneByte(0x30 + (exponent as u8 / 10))); | ||
} | ||
|
||
result.push(Token::OneByte(0x30 + (exponent as u8 % 10))); | ||
|
||
result | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use tifloats::tifloat; | ||
|
||
#[test] | ||
fn adjust() { | ||
let cases = [ | ||
tifloat!(0x0010000000000000 * 10 ^ 1), | ||
tifloat!(0x0010000000000000 * 10 ^ 2), | ||
tifloat!(0x0011000000000000 * 10 ^ -1), | ||
tifloat!(0x0011100000000000 * 10 ^ -10), | ||
tifloat!(0x0010000000000000 * 10 ^ -11), | ||
tifloat!(0x0011000000000000 * 10 ^ -11), | ||
]; | ||
for case in &cases { | ||
assert_eq!(FPartWithExponent::adjust(case).exponent(), -1); | ||
} | ||
} | ||
|
||
#[test] | ||
fn speed_cost() { | ||
let version = &*titokens::version::LATEST; | ||
|
||
let cases = vec![ | ||
(tifloat!(0x0010000000000000 * 10 ^ 1), 11635), | ||
(tifloat!(0x0010000000000000 * 10 ^ 2), 11635), | ||
(tifloat!(0x0011000000000000 * 10 ^ -1), 13502), | ||
(tifloat!(0x0011100000000000 * 10 ^ -10), 16526), | ||
(tifloat!(0x0010000000000000 * 10 ^ -11), 13924), | ||
(tifloat!(0x0011000000000000 * 10 ^ -11), 15791), | ||
]; | ||
|
||
for (item, expected) in cases { | ||
assert_eq!(FPartWithExponent::new(item).speed_cost(), Some(expected)); | ||
} | ||
} | ||
} |
113 changes: 113 additions & 0 deletions
113
ti-basic-optimizer/src/optimize/strategies/numeric_literal/integer_with_exponent.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
//! # Decimal-with-Exponent Representation | ||
//! Attempt to put the float into the form `<mantissa>|E<exponent>`, where all of the significant | ||
//! figures are placed before the `|E`. This is usually substantially faster than writing every zero. | ||
use super::write_digits::{WriteDigits, BASE_COST, DIGIT_COST, SHIFTING_COST}; | ||
use crate::optimize::strategies::Strategy; | ||
use crate::parse::Reconstruct; | ||
use crate::Config; | ||
use tifloats::Float; | ||
use titokens::Token; | ||
|
||
// time to parse <x> | ||
#[rustfmt::skip] | ||
macro_rules! ttp { | ||
(1|E1) => {10621}; | ||
(1|E~1) => {11699}; | ||
(1|E11) => {11832}; | ||
(1|E21) => {11893}; | ||
} | ||
|
||
pub(super) const EXPONENT_DECADE_COST: u32 = ttp!(1 | E21) - ttp!(1 | E11); | ||
pub(super) const EXPONENT_NEGATION_COST: u32 = ttp!(1|E~1) - ttp!(1 | E1); | ||
pub(super) const EXPONENT_TENS_COST: u32 = ttp!(1 | E11) - ttp!(1 | E1) - EXPONENT_DECADE_COST; | ||
pub(super) const EXPONENT_BASE_COST: u32 = ttp!(1 | E1) - BASE_COST - DIGIT_COST - SHIFTING_COST; | ||
|
||
// todo: drop the significant figure when it is just 1 | ||
pub(super) struct IntegerWithExponent { | ||
original: Float, | ||
adjusted: Float, | ||
} | ||
|
||
impl IntegerWithExponent { | ||
fn adjust(item: &Float) -> Float { | ||
item.shift(-(item.exponent() - item.significant_figures().len() as i8 + 1)) | ||
} | ||
|
||
/// Computes the speed cost due to just the |E part of a numeric literal. | ||
pub fn exponent_speed_cost(exponent: i8) -> Option<u32> { | ||
(-99..=99).contains(&exponent).then(|| { | ||
let base_cost = EXPONENT_BASE_COST; | ||
let neg_cost = if exponent < 0 { | ||
EXPONENT_NEGATION_COST | ||
} else { | ||
0 | ||
}; | ||
|
||
let decades = (exponent.unsigned_abs() / 10) as u32; | ||
let decade_cost = if decades != 0 { | ||
EXPONENT_TENS_COST + EXPONENT_DECADE_COST * decades | ||
} else { | ||
0 | ||
}; | ||
|
||
base_cost + neg_cost + decade_cost | ||
}) | ||
} | ||
|
||
pub fn new(item: Float) -> Self { | ||
Self { | ||
original: item, | ||
adjusted: Self::adjust(&item), | ||
} | ||
} | ||
} | ||
|
||
impl Strategy<Float> for IntegerWithExponent { | ||
fn exists(&self) -> bool { | ||
(-99..=99).contains(&(self.original.exponent() - self.adjusted.exponent())) | ||
} | ||
|
||
fn size_cost(&self) -> Option<usize> { | ||
self.exists().then(|| { | ||
self.original.significant_figures().len() | ||
+ 1 | ||
+ match self.original.exponent() - self.adjusted.exponent() { | ||
0..=9 => 1, | ||
-9..=-1 | 10..=99 => 2, | ||
-99..=-10 => 3, | ||
_ => unreachable!(), | ||
} | ||
}) | ||
} | ||
|
||
fn speed_cost(&self) -> Option<u32> { | ||
self.exists().then(|| { | ||
// WriteDigits always exists & self.exists iff the required adjustment is in range. | ||
WriteDigits::new(self.adjusted).speed_cost().unwrap() | ||
+ Self::exponent_speed_cost(self.original.exponent() - self.adjusted.exponent()) | ||
.unwrap() | ||
}) | ||
} | ||
} | ||
|
||
impl Reconstruct for IntegerWithExponent { | ||
fn reconstruct(&self, config: &Config) -> Vec<Token> { | ||
let mut result = WriteDigits::new(self.adjusted).reconstruct(config); | ||
result.push(Token::OneByte(0x3B)); | ||
|
||
let mut exponent = self.original.exponent() - self.adjusted.exponent(); | ||
if exponent < 0 { | ||
result.push(Token::OneByte(0xB0)); | ||
exponent = exponent.abs(); | ||
} | ||
|
||
if exponent >= 10 { | ||
result.push(Token::OneByte(0x30 + exponent as u8 / 10)); | ||
} | ||
|
||
result.push(Token::OneByte(0x30 + exponent as u8 % 10)); | ||
|
||
result | ||
} | ||
} |
Oops, something went wrong.