Speed up unary operators.

This commit is contained in:
Stephen Chung 2022-09-04 18:12:38 +08:00
parent 62f426d477
commit c7da3c6edb
4 changed files with 120 additions and 118 deletions

View File

@ -4,10 +4,18 @@ Rhai Release Notes
Version 1.10.0 Version 1.10.0
============== ==============
This version, by default, turns on _Fast Operators_ mode, which assumes that built-in operators for This version introduces _Fast Operators_ mode, which is turned on by default but can be disabled via
standard data types are never overloaded – in the vast majority of cases this should be so. a new options API: `Engine::set_fast_operators`.
Avoid checking for overloads may result in substantial speed improvements especially for
operator-heavy scripts. _Fast Operators_ mode assumes that none of Rhai's built-in operators for standard data types are
overloaded by user-registered functions. In the vast majority of cases this should be so (really,
who overloads the `+` operator for integers anyway?).
This assumption allows the `Engine` to avoid checking for overloads for every single operator call.
This usually results in substantial speed improvements, especially for expressions.
Minimum Rust Version
--------------------
The minimum Rust version is now `1.61.0` in order to use some `const` generics. The minimum Rust version is now `1.61.0` in order to use some `const` generics.
@ -28,7 +36,7 @@ New features
### Fast operators ### Fast operators
* A new option `Engine::fast_operators` is introduced (default to `true`) that short-circuits all built-in operators of built-in types for higher speed. User overloads are ignored. For operator-heavy scripts, this may yield substantial speed-up's. * A new option `Engine::fast_operators` is introduced (default to `true`) to enable/disable _Fast Operators_ mode.
### Fallible type iterators ### Fallible type iterators

View File

@ -5,14 +5,14 @@ use crate::ast::{Expr, FnCallExpr, OpAssignment};
use crate::engine::{KEYWORD_THIS, OP_CONCAT}; use crate::engine::{KEYWORD_THIS, OP_CONCAT};
use crate::eval::FnResolutionCacheEntry; use crate::eval::FnResolutionCacheEntry;
use crate::func::{ use crate::func::{
calc_fn_params_hash, combine_hashes, get_builtin_binary_op_fn, CallableFunction, FnAny, calc_fn_params_hash, combine_hashes, gen_fn_call_signature, get_builtin_binary_op_fn,
CallableFunction, FnAny,
}; };
use crate::types::dynamic::AccessMode; use crate::types::dynamic::AccessMode;
use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, ERR}; use crate::{Dynamic, Engine, Module, Position, RhaiResult, RhaiResultOf, Scope, ERR};
use std::collections::btree_map::Entry;
use std::num::NonZeroUsize;
#[cfg(feature = "no_std")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
use std::{collections::btree_map::Entry, num::NonZeroUsize};
impl Engine { impl Engine {
/// Search for a module within an imports stack. /// Search for a module within an imports stack.
@ -223,84 +223,78 @@ impl Engine {
level: usize, level: usize,
) -> RhaiResult { ) -> RhaiResult {
let FnCallExpr { let FnCallExpr {
name, name, hashes, args, ..
#[cfg(not(feature = "no_module"))]
namespace,
capture_parent_scope: capture,
is_native_operator: native_ops,
hashes,
args,
..
} = expr; } = expr;
// Short-circuit native binary operator call if under Fast Operators mode // Short-circuit native binary operator call if under Fast Operators mode
if *native_ops && self.fast_operators() && args.len() == 2 { if expr.is_native_operator && self.fast_operators() && (args.len() == 1 || args.len() == 2)
{
let mut lhs = self let mut lhs = self
.get_arg_value(scope, global, caches, lib, this_ptr, &args[0], level)? .get_arg_value(scope, global, caches, lib, this_ptr, &args[0], level)?
.0 .0
.flatten(); .flatten();
let mut rhs = self let mut rhs = if args.len() == 2 {
.get_arg_value(scope, global, caches, lib, this_ptr, &args[1], level)? self.get_arg_value(scope, global, caches, lib, this_ptr, &args[1], level)?
.0 .0
.flatten(); .flatten()
let args = &mut [&mut lhs, &mut rhs];
let hash = combine_hashes(
hashes.native,
calc_fn_params_hash(args.iter().map(|a| a.type_id())),
);
let c = caches.fn_resolution_cache_mut();
let entry = if let Entry::Vacant(e) = c.entry(hash) {
match get_builtin_binary_op_fn(&name, args[0], args[1]) {
Some(f) => {
let entry = FnResolutionCacheEntry {
func: CallableFunction::from_method(Box::new(f) as Box<FnAny>),
source: None,
};
e.insert(Some(entry));
c.get(&hash).unwrap().as_ref().unwrap()
}
None => {
return self
.exec_fn_call(
None, global, caches, lib, name, *hashes, args, false, false, pos,
level,
)
.map(|(v, ..)| v)
}
}
} else { } else {
match c.get(&hash).unwrap() { Dynamic::UNIT
Some(entry) => entry, };
None => {
return Err(ERR::ErrorFunctionNotFound( let mut operands = [&mut lhs, &mut rhs];
self.gen_fn_call_signature( let operands = if args.len() == 2 {
#[cfg(not(feature = "no_module"))] &mut operands[..]
&crate::ast::Namespace::NONE, } else {
name, &mut operands[0..1]
args, };
),
pos, let hash = calc_fn_params_hash(operands.iter().map(|a| a.type_id()));
) let hash = combine_hashes(hashes.native, hash);
.into())
} let cache = caches.fn_resolution_cache_mut();
}
let func = if let Entry::Vacant(entry) = cache.entry(hash) {
let func = if args.len() == 2 {
get_builtin_binary_op_fn(&name, operands[0], operands[1])
} else {
None
};
if let Some(f) = func {
entry.insert(Some(FnResolutionCacheEntry {
func: CallableFunction::from_method(Box::new(f) as Box<FnAny>),
source: None,
}));
&cache.get(&hash).unwrap().as_ref().unwrap().func
} else {
let result = self.exec_fn_call(
None, global, caches, lib, name, *hashes, operands, false, false, pos,
level,
);
return result.map(|(v, ..)| v);
}
} else if let Some(entry) = cache.get(&hash).unwrap() {
&entry.func
} else {
let sig = gen_fn_call_signature(self, name, operands);
return Err(ERR::ErrorFunctionNotFound(sig, pos).into());
}; };
let func = entry.func.get_native_fn().unwrap();
let context = (self, name, None, &*global, lib, pos, level).into(); let context = (self, name, None, &*global, lib, pos, level).into();
let result = (func)(context, args); let result = if func.is_plugin_fn() {
func.get_plugin_fn().unwrap().call(context, operands)
} else {
func.get_native_fn().unwrap()(context, operands)
};
return self.check_return_value(result, pos); return self.check_return_value(result, pos);
} }
#[cfg(not(feature = "no_module"))] #[cfg(not(feature = "no_module"))]
if !namespace.is_empty() { if !expr.namespace.is_empty() {
// Qualified function call // Qualified function call
let hash = hashes.native; let hash = hashes.native;
let namespace = &expr.namespace;
return self.make_qualified_function_call( return self.make_qualified_function_call(
scope, global, caches, lib, this_ptr, namespace, name, args, hash, pos, level, scope, global, caches, lib, this_ptr, namespace, name, args, hash, pos, level,
@ -323,8 +317,8 @@ impl Engine {
first_arg, first_arg,
args, args,
*hashes, *hashes,
*capture, expr.capture_parent_scope,
*native_ops, expr.is_native_operator,
pos, pos,
level, level,
) )

View File

@ -127,41 +127,48 @@ pub fn ensure_no_data_race(
Ok(()) Ok(())
} }
impl Engine { /// Generate the signature for a function call.
/// Generate the signature for a function call. #[inline]
#[inline] #[must_use]
#[must_use] pub fn gen_fn_call_signature(engine: &Engine, fn_name: &str, args: &[&mut Dynamic]) -> String {
pub(crate) fn gen_fn_call_signature( format!(
&self, "{fn_name} ({})",
#[cfg(not(feature = "no_module"))] namespace: &crate::ast::Namespace, args.iter()
fn_name: &str, .map(|a| if a.is::<ImmutableString>() {
args: &[&mut Dynamic], "&str | ImmutableString | String"
) -> String {
#[cfg(not(feature = "no_module"))]
let (ns, sep) = (
namespace.to_string(),
if namespace.is_empty() {
""
} else { } else {
crate::tokenizer::Token::DoubleColon.literal_syntax() engine.map_type_name(a.type_name())
}, })
); .collect::<FnArgsVec<_>>()
#[cfg(feature = "no_module")] .join(", ")
let (ns, sep) = ("", ""); )
}
format!( /// Generate the signature for a namespace-qualified function call.
"{ns}{sep}{fn_name} ({})", ///
args.iter() /// Not available under `no_module`.
.map(|a| if a.is::<ImmutableString>() { #[cfg(not(feature = "no_module"))]
"&str | ImmutableString | String" #[inline]
} else { #[must_use]
self.map_type_name(a.type_name()) pub fn gen_qualified_fn_call_signature(
}) engine: &Engine,
.collect::<FnArgsVec<_>>() namespace: &crate::ast::Namespace,
.join(", ") fn_name: &str,
) args: &[&mut Dynamic],
} ) -> String {
let (ns, sep) = (
namespace.to_string(),
if namespace.is_empty() {
""
} else {
crate::tokenizer::Token::DoubleColon.literal_syntax()
},
);
format!("{ns}{sep}{}", gen_fn_call_signature(engine, fn_name, args))
}
impl Engine {
/// Resolve a normal (non-qualified) function call. /// Resolve a normal (non-qualified) function call.
/// ///
/// Search order: /// Search order:
@ -405,11 +412,9 @@ impl Engine {
let context = (self, name, source, &*global, lib, pos, level).into(); let context = (self, name, source, &*global, lib, pos, level).into();
let result = if func.is_plugin_fn() { let result = if func.is_plugin_fn() {
func.get_plugin_fn() func.get_plugin_fn().unwrap().call(context, args)
.expect("plugin function")
.call(context, args)
} else { } else {
func.get_native_fn().expect("native function")(context, args) func.get_native_fn().unwrap()(context, args)
}; };
// Restore the original reference // Restore the original reference
@ -541,16 +546,9 @@ impl Engine {
} }
// Raise error // Raise error
_ => Err(ERR::ErrorFunctionNotFound( _ => {
self.gen_fn_call_signature( Err(ERR::ErrorFunctionNotFound(gen_fn_call_signature(self, name, args), pos).into())
#[cfg(not(feature = "no_module"))] }
&crate::ast::Namespace::NONE,
name,
args,
),
pos,
)
.into()),
} }
} }
@ -1432,7 +1430,7 @@ impl Engine {
Some(f) => unreachable!("unknown function type: {:?}", f), Some(f) => unreachable!("unknown function type: {:?}", f),
None => Err(ERR::ErrorFunctionNotFound( None => Err(ERR::ErrorFunctionNotFound(
self.gen_fn_call_signature(namespace, fn_name, &args), gen_qualified_fn_call_signature(self, namespace, fn_name, &args),
pos, pos,
) )
.into()), .into()),

View File

@ -13,7 +13,9 @@ pub mod script;
pub use args::FuncArgs; pub use args::FuncArgs;
pub use builtin::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn}; pub use builtin::{get_builtin_binary_op_fn, get_builtin_op_assignment_fn};
pub use call::FnCallArgs; #[cfg(not(feature = "no_module"))]
pub use call::gen_qualified_fn_call_signature;
pub use call::{gen_fn_call_signature, FnCallArgs};
pub use callable_function::CallableFunction; pub use callable_function::CallableFunction;
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
pub use func::Func; pub use func::Func;