用于具有一个变量的数学表达式的 C 解析器并将其保存到函数中
Posted
技术标签:
【中文标题】用于具有一个变量的数学表达式的 C 解析器并将其保存到函数中【英文标题】:C parser for mathematical expressions with one variable and save it into a function 【发布时间】:2017-02-27 16:08:03 【问题描述】:我知道有诸如 boost.spirit 之类的库或来自 http://jamesgregson.blogspot.de/2012/06/mathematical-expression-parser-in-c.html 的表达式解析器和许多其他库。
但是,我的表达式要在一个循环中被计算很多次,每次运行解析器似乎效率很低。
假设我的表达式字符串是"exp(-0.5*(t-t_0)^2/(b^2))"
(简单高斯),
其中t_0
和b
是常量,只有t
(=time) 在循环内变化。
我希望能够以某种方式将表达式保存到一个接受一个参数(对于变量 t)的函数中,而不是调用解析器数百万次,然后再计算表达式。
您是否知道如何做到这一点或是否可以做到?
【问题讨论】:
除非我遗漏了什么,听起来你只需要一个带有一个参数的函数,并且它在调用时进行计算并返回结果,就像你描述的那样。听起来你有 2 个常数,公式保持不变。 如果您在编写程序时就知道您的表达式字符串,请将其转换为 C 函数,然后让编译器完成剩下的工作。 用户进入函数。它可以是任何东西,但总是有一个变量“t” 是的,您几乎需要一个解析器。请参阅我关于如何编写递归下降解析器的答案。这些很简单,非常适合表达式,并且可以轻松修改(答案显示如何)以构建 AST 并对其进行评估。见***.com/questions/2245962/… 如果你的平台是 x86,你也可以使用 Tiny C Compiler 作为库 【参考方案1】:你想要达到的目标是完全可行的。例如,您可以从表达式构建一个 AST (https://en.wikipedia.org/wiki/Abstract_syntax_tree),其中树的一些节点代表变量。
然后,表达式的某些变量值的评估对应于该树的评估。
实际上,大多数解析器都会在内部生成这样的树,您可能会找到一些库来生成满足您需要的 AST,或者您可能想自己编写代码(另请参阅https://en.wikipedia.org/wiki/Shunting-yard_algorithm)。 Here 是一个生成 AST 的表达式解析器的简单示例(尽管在 Java 中)。
【讨论】:
【参考方案2】:我完全同意上面提到的关于创建 AST 并在每次迭代中对其进行评估的所有答案。这是迄今为止做你想做的最好(也是最正确)的方式。
但我以编写编译器为生,我忍不住提出了另一个有趣的解决方案。当你说你想创建一个接受 1 个参数并返回结果的“函数”时,我挑了挑眉。
让我们试着做到这一点。
我们将从为我们的“函数”分配一些内存开始。
现在让我们假设 4k 字节就足够了。 所以我们开始做
void* my_func = malloc(4k);
现在这是我们需要使区域可执行的真正函数。这取决于您的操作系统,您必须调用正确的系统调用。 您所要做的就是授予此页面的执行权限。
现在我们解析表示表达式的字符串。 我在这里假设 WIN 64 fastcall 调用约定。你可以使用你自己的。 所以参数 t 会在 %rcx 中,thr 结果会在 %rax 中返回。
现在让我们举个例子 - t*2 + 5
所以我们将有组装 -
imulq $2, %rcx, %rcx
addq $5, %rcx
movq %rcx, %rax
retq
现在我们将它组装成 my_func 中的等效字节
所以你会有一些等价的 -
strcpy((char*)my_func, "\x48\x6B\xC9\x02\x48\x83\xC1\x05\x48\x89\C8\C3\0");
您将在解析之上构建缓冲区,而不是一个字符串。 但你明白了。
如果需要更多内存,可以分配两倍大小并复制内容。
最后你要做的就是在循环中调用你的“函数” -
typedef int (*func_type)(int);
for(t=0; t<N; t++)
s=(func_type)(my_func)(t);
虽然这是最不切实际且难以实现的方法,但我向您保证,这将为您提供最佳性能(假设您生成了高效的程序集)。
这是一个有趣的练习,不要认真对待。很高兴看到一个库为简单的表达式这样做。
别忘了释放你的内存并移除执行标志。
编辑:一种半最优但易于生成的策略是将堆栈用于所有操作。基本上,一旦你在解析后构建了 AST,你就会从堆栈中弹出每个节点的参数,使用寄存器计算结果并推回堆栈。最终值可以弹出到 %rax 中。这种策略对于 AST 的任何运行时评估仍然是有效的。 让您摆脱寄存器分配和指令调度的所有负担。
【讨论】:
非常好,非常感谢...这种方法取决于用户的系统,对吧? 是的,您必须生成特定于目标的代码。我宁愿建议调用编译器并复制字节,因为这是您必须在内部执行的操作。【参考方案3】:将(字符串)表达式转换为抽象语法树,并在每次迭代中解释它,而不是(也)每次都解析它。如果这还不够,那么找到合适的字节码表示并将您的抽象语法树编译为字节码列表并将其提供给(字节码)解释器。如果这仍然不够性能,则将您的抽象语法树或字节码编译为机器码,使用(依赖于平台的)方式让操作系统知道它是可执行代码并调用它。
正如您可能已经猜到的那样,难度和复杂性每一步都会大大增加。但这是一个很好的学习项目。对于生产代码,您可以考虑将 llvm 用于字节码到机器码编译任务。
【讨论】:
以上是关于用于具有一个变量的数学表达式的 C 解析器并将其保存到函数中的主要内容,如果未能解决你的问题,请参考以下文章
Swift 功能强大的数学表达式解析器 DDMathParser
具有多个嵌套解析器并将字段映射到参数的 GraphQL 查询