Rival is a library for evaluating real expressions to high precision. Port of https://github.com/herbie-fp/rival. You can play around with the CLI:
$ cargo run --bin rival-cli --release -- "(- (sqrt (+ x 1)) 1)" "(x)" "(1e-30)"
Will give you:
Executed 4 instructions for 2 iterations:
┌────────┬────────┬────────┬────────┬────────┐
│ Name │ 0 Bits │ 0 Time │ 1 Bits │ 1 Time │
├────────┼────────┼────────┼────────┼────────┤
│ adjust │ │ │ │ 0.5 µs │
│ Add │ 62 │ 0.3 µs │ 633 │ 0.2 µs │
│ Sqrt │ 60 │ 0.8 µs │ 632 │ 1.6 µs │
│ Sub │ 58 │ 0.2 µs │ 58 │ 0.2 µs │
│ Total │ │ 1.2 µs │ │ 2.6 µs │
└────────┴────────┴────────┴────────┴────────┘
Final value: [0.0000000000000000000000000000005, 0.0000000000000000000000000000005]
Total: 5.2 µs
- File breakdown:
eval/adjust/: Backwards pass logicexecute.rs: Evaluate an instruction at a precision (should consider moving tomacros.rsor moving out a lot of the macro generated functions into similar files)instructions.rs: The machine instructionsmachine.rs: Rival machine builder and state; lowers expressions and initializes the machinemacros.rs: Generates the entire AST, instruction specification, amplification bounds, optimizations, path reductions, and more; provides thedef_ops!macroops.rs: The operation registry usingdef_ops!(more on this below)run.rs: Public Rival APItricks.rs: More or less helper functions that are used when specifying amplification bounds inops.rsprofile.rs: Profiling
interval/value.rs: DefinesIval,Endpoint, andErrorFlags- Everything else is the same as
ops/*.rktin Racket Rival
mpfr.rs: Unsafe wrappers around MPFR
- Appears to be much faster than Racket Rival:
- 3-5x faster for complex, MPFR dominant computations (I suspect Racket has some heavy MPFR FFI costs)
- >30x faster for smaller, Racket runtime dominant computations
- A number of TODO comments are spread throughout the code (mostly about efficiency)
- I ran both the Racket Rival and Rust Rival on a large portion of
points.json(from Racket Rival'sinfra/) and compared machine state per iteration. Observations:- Compared per iteration precisions, repeats, etc.
- Both Racket Rival and Rust Rival always output a correct final value
- Final precisions are almost always the same, and in the cases where they differ, I believe it's a bug in the printing logic rather than a difference in the code
- Rust Rival was noticeably faster
- What's missing:
- Proper handling of rationals
- Currently both the numerator and denominator are expressed as
u64 - Overflows when run on certain expressions in
jmatjs.fpcorefrom the Herbie benchmark - Workaround for now is to just convert rationals to intervals
- Currently both the numerator and denominator are expressed as
ops/gamma.rktis not ported- Proper interval arithmetic tests
- Nightly benchmarks + Sollya
- Proper handling of rationals
- All operations are mutating
- Every operation looks like
out.mul_assign(lhs, rhs)whereout,lhs, andrhsare allIvals
- Every operation looks like
- Registering operations across
eval/can be done simply using a macro! This macro (ineval/ops.rs) will generate the entire AST, instruction specification, amplification bounds, optimizations, path reductions, and more:Thus, adding a new operation simply requires a user to create andef_ops! { constant { ... } unary { Exp: { method: exp_assign, bounds: |ctx, out, inp| { let upper = ctx.maxlog(inp, false) + ctx.logspan(out); let lower = if ctx.lower_bound_early_stopping { ctx.minlog(inp, true) } else { 0 }; AmplBounds::new(upper, lower) }, optimize: |arg| { // Exp(Log(x)) => x if let Log(x) = arg { *x } else { Exp(Box::new(arg)) } }, }, Not: { method: not_assign, bounds: |_, _, _| AmplBounds::zero(), path_reduce: path_reduction::not_op_path_reduce, }, } ... unary_param { ... } // sinu, cosu, tanu binary { ... } ternary { ... } }
Ivalmethod (ininterval/) to perform the interval arithmetic, and then register it inops.rs.