Of course, you must first measure the overhead before worrying about optimization, since your next-generation genetic algorithm and mutations can put out the time of the estimate.
Once you decide you want to optimize ... the obvious answer is to compile the expression ("as much as possible"). Fortunately, there are many ways to “compile”.
If you implement this in Python, you can ask Python (I'm not an expert) to compile the constructed abstract syntax tree into a function, and it can be much faster, especially if CPython supports this.
It seems that you are implementing this in C ++. In this case, I would not evaluate the expression tree as you defined it, as this means that there are many walks, indirect function calls, etc., which are quite expensive.
One trick is to pop the actual expression as a text string with the corresponding text text of the C ++ function around it into a file and run the C ++ compiler. You can automate the entire spit-compile-relink with enough script magic, so if you do this rarely, it will work and you will get an expression score as fast as the machine can do it.
Assuming you don’t want to do this, I’ll be tempted to go through the expression tree before starting the evaluation process and “compile” this tree as a set of actions stored in a linear array called “code”. Actions will be determined by enumeration:
enum actions { // general actions first pushx, // action to push x on a stack push1, push2, // action to push 2 on a stack ... pushN, add, sub, mul, // action multiply top two stack elements together div, ... // optimized actions add1, sub1, mul1, div1, // action to divide top stack element by 1 ... addN, subN, ... addx, subX, ... }
In this case, I defined actions to implement the evaluator of the push-down stack expression, because it is easy to understand. Fortunately, your vocabulary is quite limited, so your actions can also be quite limited (they would be more complex if you had arbitrary variables or constants).
The expression ((x * 2.0) + x) -1 will be performed by a series of actions
pushx mul2 addx sub1
Most likely, it is much better than that.
It would be possible to determine the actions for the implementation of the register-oriented appraiser using the multi-mode CPU model; which would provide even faster execution (I would assume twice, but only if the expression got very complicated).
What you want is actions that cover the most common calculations that you need to perform (so you can always choose a valid sequence of actions regardless of your original expression) and actions that often occur in expressions you encounter (add1 quite typical for machine code, I don’t know what your statistics look like, and your remark that you are doing genetic programming says that you don’t know what statistics will be, but you can somehow change them rit or do and get an educated guess).
Now your inner loop for evaluation will look (sloppy syntax here):
float stack[max_depth]; stack_depth=0; for (i=1;i<expression_length;i++) { switch (code[i]) // one case for each opcode in the enum { case pushx: stack[stack_depth++]=x; break; case push1: stack[stack_depth++]=1; break; ... case add: stack[stack_depth-1]+=stack[stack_depth]; stack_depth--; break; ... case subx: stack[stack_depth]-=x; break; ... } } // stack[1] contains the answer here
The above code implements a very fast "stream interpreter" for the pushdown expression evaluator.
Now you just need to create the contents of the code array. You can do this using the original expression tree by executing the initial tree of recursive expressions, but instead of evaluating the expression, write down the action that your current expression evaluator will do in the code array and spit out special actions if you find them ( this amounts to "eye optimization"). This is a classic compilation from trees, and you can learn a lot more about how to do this pretty much in any compiler book.
Yes, this is all fair work. But then you decided to run a genetic algorithm, which is quite expensive.