当然,在您在这里进行优化之前,您需要先衡量成本开销,因为您的遗传算法下一代生产和突变可能会占用评估时间。
一旦您确定要优化...
显而易见的答案是编译表达式(“尽可能”)。幸运的是,有很多方法可以“编译”。
如果你在 Python 中实现这个,你可以让 Python(我不是专家)将一个构造的抽象语法树编译成一个函数,这可能会快很多,特别是如果 CPython 支持这个.
不过,您似乎是在 C++ 中实现这一点的。在这种情况下,我不会像您定义的那样评估表达式树,因为这意味着大量的树遍历、间接函数调用等非常昂贵。
一个俗气的技巧是将实际表达式作为文本字符串吐出,并在其周围带有适当的 C++ 函数正文文本,然后在该文件上启动 C++ 编译器。
您可以使用足够的脚本魔法自动化所有的 spit-compile-relink,这样如果
你很少这样做,这会起作用,你会尽快得到表达式评估
机器可以做到。
假设您不想这样做,我很想在您开始评估过程之前遍历您的表达式树,并将该树“编译”成一组动作存储在称为“代码”的线性数组中。操作将由枚举定义:
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,
...
}
在这种情况下,我已经定义了实现下推堆栈表达式求值器的操作,因为这很容易理解。幸运的是,您的表达词汇非常有限,因此您的操作也可能非常有限(如果您有任意变量或常量,它们会更复杂)。
表达式 ((x*2.0)+x)-1 将由一系列动作执行
pushx
mul2
addx
sub1
可能很难比这更好。
可以改为定义操作,以实现遵循多寄存器 CPU 模型的面向寄存器的表达式求值器;这将使执行速度更快(我猜是两倍,但前提是表达式变得非常复杂)。
您想要的是涵盖您需要执行的最一般计算的操作(因此无论您的原始表达式如何,您始终可以选择一个有效的操作序列)和您遇到的表达式中经常发生的操作(add1 在机器代码,不知道你的统计数据是什么样的,你说你正在做基因编程表明你不知道统计数据是什么,但你可以以某种方式测量它们或做出有根据的猜测)。
你的内部评估循环看起来像(这里的语法草率):
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
上面的代码为下推堆栈表达式求值器实现了一个非常快速的“线程解释器”。
现在“所有”你需要做的就是生成代码数组的内容。你可以这样做
通过使用您的原始表达式树,执行您的原始递归表达式树遍历,而不是执行表达式评估,将您当前的表达式评估器将执行的操作写入代码数组,并在找到它们时吐出特殊情况操作(这相当于“窥孔优化”)。这是经典的树编译,您可以在几乎所有编译器书籍中找到更多关于如何执行此操作的信息。
是的,这都是一项相当大的工作。但是后来,你决定运行一个遗传算法,这在计算上是相当昂贵的。