Skip to content

Commit

Permalink
Implement log2_1p.
Browse files Browse the repository at this point in the history
This switches between the linear approximation log_B(x) ~ x/ln(B) for
small x and the "obvious" log_B(1 + x), with the size threshold chosen
by `optimizer`. There was not much value in optimizing the
multiplicative constant (especially true for `ln_1p` if that is every
implemented: ln(x) ~ x avoids a multiplication), or using a
higher-degree polynomial, because the equation above is as accurate as
the `log2` approximation.

Fixes #11.
  • Loading branch information
huonw committed Jan 26, 2019
1 parent b16ad3c commit 4453c8e
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 5 deletions.
17 changes: 15 additions & 2 deletions benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ extern crate fast_math;
extern crate criterion;
use criterion::{Criterion, Fun, black_box};

use std::f32::consts::LN_2;
use std::f32::consts::{LN_2, LOG2_E};

fn bench<Fast, Raw, Std>(c: &mut Criterion, name: &str, values: &'static [f32],
fast: &'static Fast, raw: &'static Raw, std: &'static Std)
Expand Down Expand Up @@ -100,6 +100,18 @@ fn bench_log2(c: &mut Criterion) {
];
bench(c, "log2", values, &fast_math::log2, &fast_math::log2_raw, &f32::log2)
}
fn bench_log2_1p(c: &mut Criterion) {
let values = &[
-0.85708036, 2.43390621, 2.80163358, 2.55126348, 3.18046186,
2.88689427, 0.32215155, -0.07701401, 1.22922506, -0.4580259 ,
0.01257442, 4.23107197, 0.89538113, 1.65219582, 0.14632742,
1.68663984, 1.88125115, 2.16773942, 1.27461936, -0.03091265
];
fn log2_1p(x: f32) -> f32 {
x.ln_1p() * LOG2_E
}
bench(c, "log2_1p", values, &fast_math::log2_1p, &fast_math::log2_1p_raw, &log2_1p)
}

fn bench_atan(c: &mut Criterion) {
let values = &[
Expand Down Expand Up @@ -183,6 +195,7 @@ fn bench_atan2(c: &mut Criterion) {
c.bench_functions("scalar/atan2", vec![baseline, full, std], values);
}

criterion_group!(benches, bench_log2, bench_exp, bench_exp2, bench_exp_m1, bench_exp2_m1,
criterion_group!(benches, bench_log2, bench_log2_1p,
bench_exp, bench_exp2, bench_exp_m1, bench_exp2_m1,
bench_atan, bench_atan2);
criterion_main!(benches);
21 changes: 21 additions & 0 deletions examples/exhaustive-log2_1p.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
extern crate fast_math;
extern crate ieee754;
use ieee754::Ieee754;
use std::f64;

fn exact_log2_1p(x: f32) -> f32 {
((x as f64).ln_1p() * f64::consts::LOG2_E) as f32
}

fn main() {
let (abs, rel) = (-1.0).upto(std::f32::MAX)
.map(|x| {
let e = fast_math::log2_1p(x);
let t = exact_log2_1p(x);
let diff = (e - t).abs();
(diff, e.rel_error(t).abs())
})
.fold((0_f32, 0_f32), |(a, a_), (b, b_)| (a.max(b), a_.max(b_)));

println!("absolute: {:.8}, relative: {:.8}", abs, rel);
}
3 changes: 3 additions & 0 deletions optimiser/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ fn run<A: Approximation>(_a: A, num_test_values: usize) {
let exact = test_values.iter().map(|x| A::exact(*x as f64));

let (rel, _abs) = max_errors(approx, exact);
println!("{} {}", view, rel);
rel
};
let result = minimizer.minimize(&func, guesses.column(2));
Expand All @@ -108,6 +109,8 @@ fn main() {
"exp2" => run(problems::Exp2, n),
"exp_m1" => run(problems::ExpM1, n),
"log2" => run(problems::Log2, n),
"log2_1p" => run(problems::Log2_1p, n),
"log_1p" => run(problems::Log_1p, n),
s => panic!("unknown argument '{}'", s),
}
}
Expand Down
33 changes: 33 additions & 0 deletions optimiser/src/problems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use ndarray::prelude::*;
use crate::Approximation;
use ieee754::Ieee754;
use std::f32::{self, consts};
use std::f64;

pub struct Exp;
impl Approximation for Exp {
Expand Down Expand Up @@ -156,6 +157,38 @@ impl Approximation for Log2 {
}
}

pub struct Log2_1p;
impl Approximation for Log2_1p {
fn name() -> &'static str { "log2_1p" }

const NUM_PARAMS: usize = 1;
fn ranges() -> Vec<(f32, f32, Option<f32>)> {
vec![(0.0, 1.0, Some(1.0))]
}

const MIN: f32 = -0.99999994;
const MAX: f32 = 1.0;
fn exact_test_values() -> Vec<f32> {
vec![-0.015, -1e-30, 0.0, 1e-30, 0.015, 1.0]
}

fn exact(x: f64) -> f64 {
x.ln_1p() * f64::consts::LOG2_E
}

fn approx(x: f32, params: ArrayView1<f64>) -> f32 {
assert_eq!(params.len(), Self::NUM_PARAMS);
let limit = params[0] as f32;

let value = if x.abs() <= limit {
x * f32::consts::LOG2_E
} else {
fast_math::log2(1.0 + x)
};
value
}
}

pub struct Atan;
impl Approximation for Atan {
fn name() -> &'static str { "atan" }
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
extern crate ieee754;

pub use log::{log2, log2_raw};
pub use log::{log2_1p, log2_1p_raw};
pub use atan::{atan_raw, atan, atan2};
pub use exp::{exp_raw, exp2_raw, exp, exp2};
pub use exp::{exp_m1_raw, exp_m1, exp2_m1_raw, exp2_m1};
Expand Down
92 changes: 89 additions & 3 deletions src/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,48 @@ pub fn log2_raw(x: f32) -> f32 {
add_exp as f32 + normalised * (B + A * normalised)
}

const LOG2_1P_LIMIT: f32 = 0.04277497;
/// Compute a fast approximation of the base-2 logarithm of `1 + x`
/// for -1 < `x` < ∞.
///
/// This will return unspecified nonsense if `x` is doesn't not
/// satisfy those constraints. Use `log2_1p` if correct handling is
/// required (at the expense of some speed).
///
/// The maximum relative error across all valid input is less than
/// 0.022. The maximum absolute error is less than 0.009.
#[inline]
pub fn log2_1p(x: f32) -> f32 {
if x.abs() < LOG2_1P_LIMIT {
x * f::consts::LOG2_E
} else {
log2(1.0 + x)
}
}

/// Compute a fast approximation of the base-2 logarithm of `1 + x`
/// for -1 < `x` < ∞.
///
/// This will return unspecified nonsense if `x` is doesn't not
/// satisfy those constraints. Use `log2_1p` if correct handling is
/// required (at the expense of some speed).
///
/// The maximum relative error across all valid input is less than
/// 0.022. The maximum absolute error is less than 0.009.
#[inline]
pub fn log2_1p_raw(x: f32) -> f32 {
if x.abs() < LOG2_1P_LIMIT {
x * f::consts::LOG2_E
} else {
log2_raw(1.0 + x)
}
}

#[cfg(test)]
mod tests {
use super::*;
use quickcheck as qc;
use std::f32 as f;
use std::{f32 as f, f64};
use ieee754::Ieee754;

#[test]
Expand Down Expand Up @@ -113,7 +150,7 @@ mod tests {
}

#[test]
fn edge_cases() {
fn log2_edge_cases() {
assert!(log2(f::NAN).is_nan());
assert!(log2(-1.0).is_nan());
assert!(log2(f::NEG_INFINITY).is_nan());
Expand All @@ -123,7 +160,7 @@ mod tests {
}

#[test]
fn denormals() {
fn log2_denormals() {
fn prop(x: u8, y: u16) -> bool {
let signif = ((x as u32) << 16) | (y as u32);
let mut x = f32::recompose_raw(false, 1, signif);
Expand All @@ -143,4 +180,53 @@ mod tests {
}
qc::quickcheck(prop as fn(u8, u16) -> bool)
}

fn exact_log2_1p(x: f32) -> f32 {
((x as f64).ln_1p() * f64::consts::LOG2_E) as f32
}
#[test]
fn log2_1p_rel_err_qc() {
fn prop(x: f32) -> qc::TestResult {
if !(x > 1.0) { return qc::TestResult::discard() }

let e = log2_1p(x);
let t = exact_log2_1p(x);

qc::TestResult::from_bool(e.rel_error(t).abs() < 0.025)
}
qc::quickcheck(prop as fn(f32) -> qc::TestResult)
}

#[test]
fn log2_1p_rel_err_exhaustive() {
let mut max = 0.0;
for i in 0..PREC + 1 {
for &sign in &[-1.0, 1.0] {
let upper = if sign > 0.0 { 6 } else { 0 };
for j in -5..upper {
let x = sign * (1.0 + i as f32 / PREC as f32) * 2f32.powi(j * 20);
let e = log2_1p(x);
let t = exact_log2_1p(x);
let rel = e.rel_error(t).abs();
if rel > max { max = rel }
assert!(rel < 0.025 && (e - t).abs() < 0.009,
"{:.8}: {:.8}, {:.8}. {:.4}", x, e, t, rel);
}
}
}
println!("maximum {}", max);
}

#[test]
fn log2_1p_edge_cases() {
assert!(log2_1p(f::NAN).is_nan());
assert!(log2_1p(f::NEG_INFINITY).is_nan());
assert!(log2_1p(-2.0).is_nan());
assert_eq!(log2_1p(-1.0), f::NEG_INFINITY);
assert_eq!(log2_1p(0.0), 0.0);
let denormal = f32::recompose_raw(false, 0, 1);
assert_eq!(log2_1p(denormal), denormal);
assert_eq!(log2_1p(1.0), 1.0);
assert_eq!(log2_1p(f::INFINITY), f::INFINITY);
}
}

0 comments on commit 4453c8e

Please sign in to comment.