Eagerly evaluate built-in operators for OptimizationLevel::Simple.
This commit is contained in:
@@ -3,8 +3,8 @@ Eager Function Evaluation When Using Full Optimization Level
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_ and will _eagerly_
|
||||
evaluated all function calls with constant arguments, using the result to replace the call.
|
||||
When the optimization level is [`OptimizationLevel::Full`], the [`Engine`] assumes all functions to be _pure_
|
||||
and will _eagerly_ evaluated all function calls with constant arguments, using the result to replace the call.
|
||||
|
||||
This also applies to all operators (which are implemented as functions).
|
||||
|
||||
@@ -14,8 +14,8 @@ For instance, the same example above:
|
||||
// When compiling the following with OptimizationLevel::Full...
|
||||
|
||||
const DECISION = 1;
|
||||
// this condition is now eliminated because 'DECISION == 1'
|
||||
if DECISION == 1 { // is a function call to the '==' function, and it returns 'true'
|
||||
// this condition is now eliminated because 'sign(DECISION) > 0'
|
||||
if DECISION.sign() > 0 { // is a call to the 'sign' and '>' functions, and they return 'true'
|
||||
print("hello!"); // this block is promoted to the parent level
|
||||
} else {
|
||||
print("boo!"); // this block is eliminated because it is never reached
|
||||
@@ -24,13 +24,3 @@ if DECISION == 1 { // is a function call to the '==' function, and it r
|
||||
print("hello!"); // <- the above is equivalent to this
|
||||
// ('print' and 'debug' are handled specially)
|
||||
```
|
||||
|
||||
Because of the eager evaluation of functions, many constant expressions will be evaluated and replaced by the result.
|
||||
This does not happen with [`OptimizationLevel::Simple`] which doesn't assume all functions to be _pure_.
|
||||
|
||||
```rust
|
||||
// When compiling the following with OptimizationLevel::Full...
|
||||
|
||||
let x = (1+2)*3-4/5%6; // <- will be replaced by 'let x = 9'
|
||||
let y = (1>2) || (3<=4); // <- will be replaced by 'let y = true'
|
||||
```
|
||||
|
@@ -61,17 +61,63 @@ For fixed script texts, the constant values can be provided in a user-defined [`
|
||||
to the [`Engine`] for use in compilation and evaluation.
|
||||
|
||||
|
||||
Watch Out for Function Calls
|
||||
---------------------------
|
||||
Eager Operator Evaluations
|
||||
-------------------------
|
||||
|
||||
Beware, however, that most operators are actually function calls, and those functions can be overridden,
|
||||
so they are not optimized away:
|
||||
so whether they are optimized away depends on the situation:
|
||||
|
||||
* If the operands are not _constant_ values, it is not optimized.
|
||||
|
||||
* If the operator is [overloaded][operator overloading], it is not optimized because the overloading function may not be _pure_
|
||||
(i.e. may cause side-effects when called).
|
||||
|
||||
* If the operator is not _binary_, it is not optimized. Only binary operators are built-in to Rhai.
|
||||
|
||||
* If the operands are not of the same type, it is not optimized.
|
||||
|
||||
* If the operator is not _built-in_ (see list of [built-in operators]), it is not optimized.
|
||||
|
||||
* If the operator is a binary built-in operator for a [standard type][standard types], it is called and replaced by a constant result.
|
||||
|
||||
Rhai guarantees that no external function will be run (in order not to trigger side-effects) during the
|
||||
optimization process (unless the optimization level is set to [`OptimizationLevel::Full`]).
|
||||
|
||||
```rust
|
||||
const DECISION = 1;
|
||||
const DECISION = 1; // this is an integer, one of the standard types
|
||||
|
||||
if DECISION == 1 { // NOT optimized away because you can define
|
||||
: // your own '==' function to override the built-in default!
|
||||
if DECISION == 1 { // this is optimized into 'true'
|
||||
:
|
||||
} else if DECISION == 2 { // this is optimized into 'false'
|
||||
:
|
||||
} else if DECISION == 3 { // this is optimized into 'false'
|
||||
:
|
||||
} else {
|
||||
:
|
||||
}
|
||||
```
|
||||
|
||||
Because of the eager evaluation of operators for [standard types], many constant expressions will be evaluated
|
||||
and replaced by the result.
|
||||
|
||||
```rust
|
||||
let x = (1+2)*3-4/5%6; // will be replaced by 'let x = 9'
|
||||
|
||||
let y = (1>2) || (3<=4); // will be replaced by 'let y = true'
|
||||
```
|
||||
|
||||
For operators that are not optimized away due to one of the above reasons, the function calls
|
||||
are simply left behind:
|
||||
|
||||
```rust
|
||||
// Assume 'new_state' returns some custom type that is NOT one of the standard types.
|
||||
// Also assume that the '==; operator is defined for that custom type.
|
||||
const DECISION_1 = new_state(1);
|
||||
const DECISION_2 = new_state(2);
|
||||
const DECISION_3 = new_state(3);
|
||||
|
||||
if DECISION == 1 { // NOT optimized away because the operator is not built-in
|
||||
: // and may cause side-effects if called!
|
||||
:
|
||||
} else if DECISION == 2 { // same here, NOT optimized away
|
||||
:
|
||||
@@ -82,28 +128,4 @@ if DECISION == 1 { // NOT optimized away because you can define
|
||||
}
|
||||
```
|
||||
|
||||
because no operator functions will be run (in order not to trigger side-effects) during the optimization process
|
||||
(unless the optimization level is set to [`OptimizationLevel::Full`]).
|
||||
|
||||
So, instead, do this:
|
||||
|
||||
```rust
|
||||
const DECISION_1 = true;
|
||||
const DECISION_2 = false;
|
||||
const DECISION_3 = false;
|
||||
|
||||
if DECISION_1 {
|
||||
: // this branch is kept and promoted to the parent level
|
||||
} else if DECISION_2 {
|
||||
: // this branch is eliminated
|
||||
} else if DECISION_3 {
|
||||
: // this branch is eliminated
|
||||
} else {
|
||||
: // this branch is eliminated
|
||||
}
|
||||
```
|
||||
|
||||
In general, boolean constants are most effective for the optimizer to automatically prune
|
||||
large `if`-`else` branches because they do not depend on operators.
|
||||
|
||||
Alternatively, turn the optimizer to [`OptimizationLevel::Full`].
|
||||
|
@@ -8,9 +8,10 @@ There are three levels of optimization: `None`, `Simple` and `Full`.
|
||||
* `None` is obvious - no optimization on the AST is performed.
|
||||
|
||||
* `Simple` (default) performs only relatively _safe_ optimizations without causing side-effects
|
||||
(i.e. it only relies on static analysis and will not actually perform any function calls).
|
||||
(i.e. it only relies on static analysis and [built-in operators] for constant [standard types],
|
||||
and will not perform any external function calls).
|
||||
|
||||
* `Full` is _much_ more aggressive, _including_ running functions on constant arguments to determine their result.
|
||||
* `Full` is _much_ more aggressive, _including_ calling external functions on constant arguments to determine their result.
|
||||
One benefit to this is that many more optimization opportunities arise, especially with regards to comparison operators.
|
||||
|
||||
|
||||
|
@@ -16,11 +16,11 @@ The final, optimized [`AST`] is then used for evaluations.
|
||||
// Compile master script to AST
|
||||
let master_ast = engine.compile(
|
||||
r"
|
||||
if SCENARIO_1 {
|
||||
if SCENARIO == 1 {
|
||||
do_work();
|
||||
} else if SCENARIO_2 {
|
||||
} else if SCENARIO == 2 {
|
||||
do_something();
|
||||
} else if SCENARIO_3 {
|
||||
} else if SCENARIO == 3 {
|
||||
do_something_else();
|
||||
} else {
|
||||
do_nothing();
|
||||
@@ -29,9 +29,7 @@ r"
|
||||
|
||||
// Create a new 'Scope' - put constants in it to aid optimization
|
||||
let mut scope = Scope::new();
|
||||
scope.push_constant("SCENARIO_1", true);
|
||||
scope.push_constant("SCENARIO_2", false);
|
||||
scope.push_constant("SCENARIO_3", false);
|
||||
scope.push_constant("SCENARIO", 1_i64);
|
||||
|
||||
// Re-optimize the AST
|
||||
let new_ast = engine.optimize_ast(&scope, master_ast.clone(), OptimizationLevel::Simple);
|
||||
|
@@ -3,17 +3,20 @@ Side-Effect Considerations for Full Optimization Level
|
||||
|
||||
{{#include ../../links.md}}
|
||||
|
||||
All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_ (i.e. they do not mutate state
|
||||
nor cause any side-effects, with the exception of `print` and `debug` which are handled specially) so using
|
||||
[`OptimizationLevel::Full`] is usually quite safe _unless_ custom types and functions are registered.
|
||||
All of Rhai's built-in functions (and operators which are implemented as functions) are _pure_
|
||||
(i.e. they do not mutate state nor cause any side-effects, with the exception of `print` and `debug`
|
||||
which are handled specially) so using [`OptimizationLevel::Full`] is usually quite safe _unless_
|
||||
custom types and functions are registered.
|
||||
|
||||
If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie within a pruned code block).
|
||||
If custom functions are registered, they _may_ be called (or maybe not, if the calls happen to lie
|
||||
within a pruned code block).
|
||||
|
||||
If custom functions are registered to overload built-in operators, they will also be called when the operators are used
|
||||
(in an `if` statement, for example) causing side-effects.
|
||||
If custom functions are registered to overload built-in operators, they will also be called when
|
||||
the operators are used (in an `if` statement, for example) causing side-effects.
|
||||
|
||||
Therefore, the rule-of-thumb is:
|
||||
|
||||
* _Always_ register custom types and functions _after_ compiling scripts if [`OptimizationLevel::Full`] is used.
|
||||
|
||||
* _DO NOT_ depend on knowledge that the functions have no side-effects, because those functions can change later on and, when that happens, existing scripts may break in subtle ways.
|
||||
* _DO NOT_ depend on knowledge that the functions have no side-effects, because those functions can change later on and,
|
||||
when that happens, existing scripts may break in subtle ways.
|
||||
|
Reference in New Issue
Block a user