使用ExprTk时限制编译内存占用过大的表达式
解决ExprTk表达式编译内存超限检测的问题
针对多线程环境下无法准确检测ExprTk表达式编译内存占用的问题,提供以下几种可行方案:
方案一:使用独立子进程编译表达式
子进程拥有独立地址空间,编译表达式时的内存变化不受主进程其他线程干扰,能精准测量单个表达式的内存开销:
- 执行步骤:
- 收到表达式字符串后,启动独立子进程
- 子进程内初始化ExprTk符号表、解析器,编译目标表达式
- 子进程通过系统API(如
getrusage)获取编译前后的自身内存占用,计算差值后返回给主进程 - 主进程根据返回值判断是否保留该表达式,子进程退出释放资源
- 优势:完全隔离多线程内存干扰,测量准确;无需修改ExprTk源码
- 示例伪代码(C++):
// 子进程执行逻辑 int compile_expr_in_child(const std::string& expr, size_t& mem_used) { typedef exprtk::symbol_table<double> symbol_table_t; typedef exprtk::expression<double> expression_t; typedef exprtk::parser<double> parser_t; symbol_table_t sym_table; expression_t expr_obj; parser_t parser; expr_obj.register_symbol_table(sym_table); auto before = get_process_memory_usage(); // 子进程内的内存获取函数 if (!parser.compile(expr, expr_obj)) { return -1; // 编译失败 } auto after = get_process_memory_usage(); mem_used = after - before; return 0; } // 主进程调用逻辑 std::string newExprString = getNextExpression(transportConext); size_t mem_used = 0; int status = run_in_child_process(compile_expr_in_child, newExprString, mem_used); if (status != 0) { handleCompilationError(...); } else if (mem_used > maxMemoryLimit) { handleExpressionError(newExprString); } else { // 主进程重新编译或复用子进程编译结果 expressionsList.push_back(...); }
方案二:自定义ExprTk内存分配器统计内存
ExprTk支持自定义内存分配策略,通过重载exprtk::allocator类,精准统计单个表达式编译过程中留存的内存总量:
- 执行步骤:
- 实现自定义分配器,在
allocate和reallocate时累加分配内存(编译后留存的内存无需在deallocate中扣除) - 为每个待编译表达式创建独立的分配器实例
- 编译时让ExprTk使用该分配器,编译完成后读取累计内存值
- 实现自定义分配器,在
- 优势:无需跨进程,性能更高;精准统计单个表达式的内存开销
- 示例代码:
struct tracking_allocator : public exprtk::allocator<double> { std::atomic_size_t total_allocated = 0; void* allocate(const std::size_t size) override { void* ptr = std::malloc(size); if (ptr) { total_allocated += size; } return ptr; } void deallocate(void* ptr) override { // ExprTk编译后留存内存用于求值,此处仅释放临时内存,不扣除统计值 std::free(ptr); } void* reallocate(void* ptr, const std::size_t old_size, const std::size_t new_size) override { void* new_ptr = std::realloc(ptr, new_size); if (new_ptr) { total_allocated += (new_size - old_size); } return new_ptr; } }; // 主进程编译逻辑 tracking_allocator tracker; parser_t parser; parser.set_allocator(&tracker); expression_t newExpression; newExpression.register_symbol_table(globalSymbolTable); if (!parser.compile(newExprString, newExpression)) { handleCompilationError(parser, newExprString); continue; } if (tracker.total_allocated > maxMemoryLimit) { handleExpressionError(newExprString); continue; } expressionsList.push_back(newExpression);
注意:每个表达式必须使用独立的跟踪分配器实例,避免跨表达式统计混乱。
方案三:预解析结合符号表计算理论内存占用
针对含变量下标向量(如var v[max_elems / 2] := 0;),通过预解析表达式定义+符号表求值,提前计算理论内存占用:
- 执行步骤:
- 编写简易解析逻辑提取所有
var定义,区分普通变量和向量变量 - 对向量的大小表达式(如
max_elems / 2),用ExprTk基于全局符号表单独编译求值 - 计算所有变量的总内存(普通变量按
sizeof(double),向量按sizeof(double)*求值结果),提前拦截超限表达式
- 编写简易解析逻辑提取所有
- 优势:无需完整编译,性能开销小;可作为前置过滤环节
- 示例伪代码:
size_t calculate_theoretical_memory(const std::string& expr, const symbol_table_t& global_sym) { typedef exprtk::expression<double> expression_t; typedef exprtk::parser<double> parser_t; size_t total = 0; std::vector<std::string> var_defs = extract_var_definitions(expr); // 提取var定义的逻辑 for (const auto& def : var_defs) { if (def.find('[') != std::string::npos) { // 提取向量大小表达式 std::string size_expr = extract_vector_size(def); // 编译求值大小表达式 expression_t size_expr_obj; size_expr_obj.register_symbol_table(global_sym); parser_t size_parser; if (size_parser.compile(size_expr, size_expr_obj)) { double size_val = size_expr_obj.value(); total += sizeof(double) * static_cast<size_t>(size_val); } else { // 大小表达式无法求值,视为超限 return std::numeric_limits<size_t>::max(); } } else { // 普通变量 total += sizeof(double); } } return total; } // 主进程前置过滤逻辑 size_t theoretical_mem = calculate_theoretical_memory(newExprString, globalSymbolTable); if (theoretical_mem > maxMemoryLimit) { handleExpressionError(newExprString); continue; } // 执行正常编译流程
注意:该方法无法覆盖ExprTk编译产生的额外内存(如语法树节点),建议结合方案一或二使用。
内容的提问来源于stack exchange,提问作者Soda Coader
相关产品推荐
相关产品推荐

