diff --git a/CHANGELOG.md b/CHANGELOG.md index 5913e007..5d3cf23b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,16 +4,22 @@ Rhai Release Notes Version 1.13.0 ============== +This version attempts a number of optimizations that may yield small speed improvements: + +* Simple operators (e.g. integer arithmetic) are inlined to avoid the overhead of a function call. +* The tokenizer uses pre-calculated tables (generated by GNU `gperf`) for keyword recognition. +* A black-arts trick (see `Engine::black_box`) is used to prevent LLVM from optimizing hand-tuned AST node matches back into a lookup table, which messes up branch prediction on modern CPU's. + Bug fixes --------- * Complex indexing/dotting chains now parse correctly, for example: `a[b][c[d]].e` * `map` and `filter` for arrays are marked `pure`. Warnings are added to the documentation of pure array methods that take `this` closures. * Syntax such as `foo.bar::baz` no longer panics, but returns a proper parse error. -* `x += y` where `x` and `y` are `char` now works correctly. * Expressions such as `!inside` now parses correctly instead of as `!in` followed by `side`. * Custom syntax starting with symbols now works correctly and no longer raises a parse error. * Comparing different custom types now works correctly when the appropriate comparison operators are registered. +* Some op-assignments, such as `x += y` where `x` and `y` are `char`, now work correctly instead of failing silently. * Op-assignments to bit flags or bit ranges now work correctly. Potentially breaking changes @@ -30,8 +36,9 @@ Enhancements * The functions `min` and `max` are added for numbers. * Range cases in `switch` statements now also match floating-point and decimal values. In order to support this, however, small numeric ranges cases are no longer unrolled. * Loading a module via `import` now gives the module access to the current scope, including variables and constants defined inside. -* Some very simple operator calls (e.g. integer add) are short-circuited to avoid the overhead of a function call, resulting in a small speed improvement. -* The tokenizer now uses table-driven keyword recognizers generated by GNU gperf. At least _theoretically_ it should be faster... +* Some very simple operator calls (e.g. integer add) are inlined to avoid the overhead of a function call, resulting in a small speed improvement. +* The tokenizer now uses table-driven keyword recognizers generated by GNU `gperf`. At least _theoretically_ it should be faster... +* The field `isAnonymous` is added to JSON functions metadata. Version 1.12.0 diff --git a/src/api/eval.rs b/src/api/eval.rs index 3882fae5..2c93b182 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -242,7 +242,7 @@ impl Engine { ast.resolver().cloned(), ); - auto_restore!(global => move |g| { + auto_restore! { global => move |g| { #[cfg(not(feature = "no_module"))] { g.embedded_module_resolver = orig_embedded_module_resolver; @@ -252,7 +252,7 @@ impl Engine { g.lib.truncate(orig_lib_len); g.source = orig_source; - }); + }} let r = self.eval_global_statements(global, caches, scope, ast.statements())?; diff --git a/src/eval/chaining.rs b/src/eval/chaining.rs index 22e8ae46..a6184061 100644 --- a/src/eval/chaining.rs +++ b/src/eval/chaining.rs @@ -688,7 +688,7 @@ impl Engine { let reset = self.run_debugger_with_reset(global, caches, scope, this_ptr, rhs)?; #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + auto_restore! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } let crate::ast::FnCallExpr { name, hashes, args, .. @@ -857,7 +857,7 @@ impl Engine { let reset = self .run_debugger_with_reset(global, caches, scope, _tp, _node)?; #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + auto_restore! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } let crate::ast::FnCallExpr { name, hashes, args, .. @@ -977,7 +977,7 @@ impl Engine { global, caches, scope, _tp, _node, )?; #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + auto_restore! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } let crate::ast::FnCallExpr { name, hashes, args, .. diff --git a/src/eval/expr.rs b/src/eval/expr.rs index c2aaf79d..f73bd496 100644 --- a/src/eval/expr.rs +++ b/src/eval/expr.rs @@ -243,7 +243,7 @@ impl Engine { let reset = self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), expr)?; #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + auto_restore! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } self.track_operation(global, expr.position())?; diff --git a/src/eval/stmt.rs b/src/eval/stmt.rs index 1f4ae51d..799846b3 100644 --- a/src/eval/stmt.rs +++ b/src/eval/stmt.rs @@ -54,7 +54,7 @@ impl Engine { global.scope_level += 1; } - auto_restore!(global if restore_orig_state => move |g| { + auto_restore! { global if restore_orig_state => move |g| { g.scope_level -= 1; #[cfg(not(feature = "no_module"))] @@ -63,7 +63,7 @@ impl Engine { // The impact of new local variables goes away at the end of a block // because any new variables introduced will go out of scope g.always_search_scope = orig_always_search_scope; - }); + }} // Pop new function resolution caches at end of block auto_restore! { @@ -271,7 +271,7 @@ impl Engine { let reset = self.run_debugger_with_reset(global, caches, scope, this_ptr.as_deref_mut(), stmt)?; #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + auto_restore! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } self.track_operation(global, stmt.position())?; diff --git a/src/func/call.rs b/src/func/call.rs index 079c095b..886cdd75 100644 --- a/src/func/call.rs +++ b/src/func/call.rs @@ -658,7 +658,7 @@ impl Engine { }; let orig_source = mem::replace(&mut global.source, source.clone()); - auto_restore!(global => move |g| g.source = orig_source); + auto_restore! { global => move |g| g.source = orig_source } return if _is_method_call { // Method call of script function - map first argument to `this` @@ -686,7 +686,7 @@ impl Engine { backup.change_first_arg_to_copy(_args); } - auto_restore!(args = (_args) if swap => move |a| backup.restore_first_arg(a)); + auto_restore! { args = (_args) if swap => move |a| backup.restore_first_arg(a) } self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos) } @@ -730,7 +730,7 @@ impl Engine { }) }); #[cfg(feature = "debugging")] - auto_restore!(global if Some(reset) => move |g| g.debugger_mut().reset_status(reset)); + auto_restore! { global if Some(reset) => move |g| g.debugger_mut().reset_status(reset) } self.eval_expr(global, caches, scope, this_ptr, arg_expr) .map(|r| (r, arg_expr.start_position())) @@ -1457,7 +1457,7 @@ impl Engine { let scope = &mut Scope::new(); let orig_source = mem::replace(&mut global.source, module.id_raw().cloned()); - auto_restore!(global => move |g| g.source = orig_source); + auto_restore! { global => move |g| g.source = orig_source } self.call_script_fn(global, caches, scope, None, environ, f, args, true, pos) } diff --git a/src/serde/metadata.rs b/src/serde/metadata.rs index fa41fd6b..e207ee29 100644 --- a/src/serde/metadata.rs +++ b/src/serde/metadata.rs @@ -34,6 +34,8 @@ struct FnMetadata<'a> { pub namespace: crate::FnNamespace, pub access: FnAccess, pub name: &'a str, + #[cfg(not(feature = "no_function"))] + pub is_anonymous: bool, #[serde(rename = "type")] pub typ: FnType, pub num_params: usize, @@ -83,6 +85,8 @@ impl<'a> From<&'a FuncInfo> for FnMetadata<'a> { namespace: info.metadata.namespace, access: info.metadata.access, name: &info.metadata.name, + #[cfg(not(feature = "no_function"))] + is_anonymous: crate::parser::is_anonymous_fn(&info.metadata.name), typ, num_params: info.metadata.num_params, params: info diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 07ad2f9e..27d0cf6b 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -305,7 +305,7 @@ impl fmt::Display for Token { } } -// Table-driven keyword recognizer generated by GNU gperf on the file `tools/keywords.txt`. +// Table-driven keyword recognizer generated by GNU `gperf` on the file `tools/keywords.txt`. // // When adding new keywords, make sure to update `tools/keywords.txt` and re-generate this. @@ -507,7 +507,7 @@ static KEYWORDS_LIST: [(&str, Token); 153] = [ ("#{", Token::MapStart), ]; -// Table-driven reserved symbol recognizer generated by GNU gperf on the file `tools/reserved.txt`. +// Table-driven reserved symbol recognizer generated by GNU `gperf` on the file `tools/reserved.txt`. // // When adding new reserved symbols, make sure to update `tools/reserved.txt` and re-generate this. @@ -872,7 +872,7 @@ impl Token { #[must_use] pub fn lookup_symbol_from_syntax(syntax: &str) -> Option { // This implementation is based upon a pre-calculated table generated - // by GNU gperf on the list of keywords. + // by GNU `gperf` on the list of keywords. let utf8 = syntax.as_bytes(); let len = utf8.len(); let mut hash_val = len; @@ -893,8 +893,8 @@ impl Token { match KEYWORDS_LIST[hash_val] { (_, Token::EOF) => None, - // Fail early to avoid calling memcmp() - // Since we are already working with bytes, mind as well check the first one + // Fail early to avoid calling memcmp(). + // Since we are already working with bytes, mind as well check the first one. (s, ref t) if s.len() == len && s.as_bytes()[0] == utf8[0] && s == syntax => { Some(t.clone()) } @@ -2312,7 +2312,7 @@ pub fn is_id_continue(x: char) -> bool { #[must_use] pub fn is_reserved_keyword_or_symbol(syntax: &str) -> (bool, bool, bool) { // This implementation is based upon a pre-calculated table generated - // by GNU gperf on the list of keywords. + // by GNU `gperf` on the list of keywords. let utf8 = syntax.as_bytes(); let len = utf8.len(); let rounds = len.min(3); @@ -2333,8 +2333,8 @@ pub fn is_reserved_keyword_or_symbol(syntax: &str) -> (bool, bool, bool) { match RESERVED_LIST[hash_val] { ("", ..) => (false, false, false), (s, true, a, b) => ( - // Fail early to avoid calling memcmp() - // Since we are already working with bytes, mind as well check the first one + // Fail early to avoid calling memcmp(). + // Since we are already working with bytes, mind as well check the first one. s.len() == len && s.as_bytes()[0] == utf8[0] && s == syntax, a, b,