Skip to content
Permalink
Browse files

Add benchmarks

  • Loading branch information...
Vurich committed May 22, 2018
1 parent a998781 commit f839ff3cd76343e7371eec73de61997fa000f1eb
Showing with 285 additions and 0 deletions.
  1. +285 −0 src/lib.rs
@@ -1,3 +1,5 @@
#![cfg_attr(test, feature(test))]

#[macro_use]
extern crate combine;

@@ -125,3 +127,286 @@ parser! {
))
}
}

#[cfg(test)]
mod benches {
extern crate test;

use combine::Parser;

use self::test::{black_box, Bencher};

use super::{eval, expr, Value};

// First we need some helper functions. These are used with the `InbuiltFunc`
// constructor and act as native functions, similar to how you'd add functions
// to the global namespace in Lua.
//
// This one simply sums the arguments.
fn add(variables: Vec<Value>) -> Value {
let mut out = 0u64;

for v in variables {
match v {
Value::Int(i) => out += i,
_ => println!("Tried to add a non-int"),
}
}

Value::Int(out)
}

// This one checks the arguments for equality. I used `Void` to represent true
// and `False` to represent false. This is mostly inspired by scheme, where
// everything is true except for `#f`.
fn eq(mut variables: Vec<Value>) -> Value {
if let Some(last) = variables.pop() {
for v in variables {
if v != last {
return Value::False;
}
}

Value::Void
} else {
Value::Void
}
}

// This version of `if` doesn't lazily evaluate its branches, unlike every
// other programming language in existence. To do lazy evaluation you make
// the `then` and `else` branches return functions and then call the
// functions.
fn if_(variables: Vec<Value>) -> Value {
let mut iter = variables.into_iter();
let (first, second, third) = (
iter.next().expect("No condition for if"),
iter.next().expect("No body for if"),
iter.next().unwrap_or(Value::Void),
);
assert!(iter.next().is_none(), "Too many arguments supplied to `if`");

match first {
Value::False => third,
_ => second,
}
}

// Here are our test program strings. Our language looks a lot like Lisp,
// but it has the important distinction of being totally useless.
//
// This string is used to test the performance when programs include
// deeply-nested structures. Nesting this deep is unlikely but it's a
// good test for the parser's performance on nesting in general.
const DEEP_NESTING: &str = "(((((((((((((((((((((((((((((((((((((((((((((test)))))))))))))))))))))))))))))))))))))))))))))";

// This string is used to test the performance of when programs include
// many variables of many different names, and many repetitions of the
// same name. We'd expect real programs to contain lots of variables and
// so it's important that we get good performance when parsing and
// evaluating them.
const MANY_VARIABLES: &str = r"
((\(a b c d e f g h i j k l m n o p q r s t u v w x y z)
(a b c d e f g h i j k l m n o p q r s t u v w x y z)
(b c d e f g h i j k l m n o p q r s t u v w x y z)
(c d e f g h i j k l m n o p q r s t u v w x y z)
(d e f g h i j k l m n o p q r s t u v w x y z)
(e f g h i j k l m n o p q r s t u v w x y z)
(f g h i j k l m n o p q r s t u v w x y z)
(g h i j k l m n o p q r s t u v w x y z)
(h i j k l m n o p q r s t u v w x y z)
(i j k l m n o p q r s t u v w x y z)
(j k l m n o p q r s t u v w x y z)
(k l m n o p q r s t u v w x y z)
(l m n o p q r s t u v w x y z)
(m n o p q r s t u v w x y z)
(n o p q r s t u v w x y z)
(o p q r s t u v w x y z)
(p q r s t u v w x y z)
(q r s t u v w x y z)
(r s t u v w x y z)
(s t u v w x y z)
(t u v w x y z)
(u v w x y z)
(v w x y z)
(w x y z)
(x y z)
(y z)
(z))
ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore
ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore ignore)
";

// This is used to test that function calls aren't unnecessarily
// expensive. It just passes the same value down and then back up
// the stack.
const NESTED_FUNC: &str = r"
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
((\(val)
val
) val)
) val)
) val)
) val)
) val)
) val)
) val)
) val)
) val)
) val)
) #f)
";

// This is a more realistic program that uses every feature of
// the language. It's not useful for finding hotspots but it's
// definitely useful for seeing improvements.
const REAL_CODE: &str = r"
(= increment (\(a)
(add a 1)))
(= someval (increment 2))
(= double (\ (someval)
(add someval someval)))
(= addfive (\ (first second third fourth fifth) (add first second third fourth fifth)))
(= second (\ (a a) a))
(= rec (\ (a)
((if (eq a 10)
(\() 10)
(\() (rec (add a 1)))))))
(= ne (\ (a b)
(not (eq a b))))
(= not (\ (a)
(if a #f)))
(double 5)
(addfive 1 2 3 4 5)
(second 1 2)
(rec 0)
(ne 1 2)
someval
";

// Now we run the benchmarks. The parsing ones are very simple...
#[bench]
fn parse_deep_nesting(b: &mut Bencher) {
b.iter(|| black_box(expr().easy_parse(DEEP_NESTING)))
}

#[bench]
fn parse_many_variables(b: &mut Bencher) {
b.iter(|| black_box(expr().easy_parse(MANY_VARIABLES)))
}

#[bench]
fn parse_nested_func(b: &mut Bencher) {
b.iter(|| black_box(expr().easy_parse(NESTED_FUNC)))
}

#[bench]
fn parse_real_code(b: &mut Bencher) {
b.iter(|| black_box(expr().easy_parse(REAL_CODE)))
}

// We only test parsing for this one. We could test the speed of
// evaluating these expressions too but I personally prefer to
// keep the benchmarks few and representative.
#[bench]
fn parse_literals(b: &mut Bencher) {
let program_text = r"
((\()
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98 99))
";

b.iter(|| black_box(expr().easy_parse(program_text)))
}

// For the benchmarks that run the code we have to do a little more
// work. We need to put some functions in the global namespace that
// our testing code needs in order to run.
#[bench]
fn run_deep_nesting(b: &mut Bencher) {
use std::collections::HashMap;

// This just returns a function so `((whatever))` (equivalent
// to `(whatever())()`) does something useful. Specifically
// it just returns itself. We try to do as little work as
// possible here so that our benchmark is still testing the
// interpreter and not this function.
fn callable(_: Vec<Value>) -> Value {
Value::InbuiltFunc(callable)
}

let mut env = HashMap::new();
env.insert("test".to_owned(), Value::InbuiltFunc(callable));

let (program, _) = expr().easy_parse(DEEP_NESTING).unwrap();

b.iter(|| black_box(eval(program.clone(), &mut env)));
}

#[bench]
fn run_real_code(b: &mut Bencher) {
use std::collections::HashMap;

let mut env = HashMap::new();

env.insert("eq".to_owned(), Value::InbuiltFunc(eq));
env.insert("add".to_owned(), Value::InbuiltFunc(add));
env.insert("if".to_owned(), Value::InbuiltFunc(if_));

let (program, _) = ::combine::many1::<Vec<_>, _>(expr())
.easy_parse(REAL_CODE)
.unwrap();

b.iter(|| {
let mut env = env.clone();
for line in &program {
black_box(eval(line.clone(), &mut env));
}
});
}

#[bench]
fn run_many_variables(b: &mut Bencher) {
use std::collections::HashMap;

// This just takes anything and returns `Void`. We just
// want a function that can take any number of arguments
// but we don't want that function to do anything useful
// since, again, the benchmark should be of the
// interpreter's code.
fn ignore(_: Vec<Value>) -> Value {
Value::Void
}

let (program, _) = expr().easy_parse(MANY_VARIABLES).unwrap();

let mut env = HashMap::new();

env.insert("ignore".to_owned(), Value::InbuiltFunc(ignore));

b.iter(|| black_box(eval(program.clone(), &mut env)));
}

#[bench]
fn run_nested_func(b: &mut Bencher) {
use std::collections::HashMap;

let (program, _) = expr().easy_parse(NESTED_FUNC).unwrap();
let mut env = HashMap::new();
b.iter(|| black_box(eval(program.clone(), &mut env)));
}
}

0 comments on commit f839ff3

Please sign in to comment.
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.