Skip to content

Commit

Permalink
opt: Strategies & Numeric Literal Optimization
Browse files Browse the repository at this point in the history
  • Loading branch information
rpitasky committed Sep 5, 2024
1 parent a172eb9 commit 173e9ac
Show file tree
Hide file tree
Showing 9 changed files with 648 additions and 93 deletions.
1 change: 1 addition & 0 deletions ti-basic-optimizer/src/optimize/mod.rs
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 {
Expand Down
74 changes: 74 additions & 0 deletions ti-basic-optimizer/src/optimize/strategies/mod.rs
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!")
}
}
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)]
}
}
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));
}
}
}
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
}
}
Loading

0 comments on commit 173e9ac

Please sign in to comment.