数千种模式文本匹配的并行化实现困境及可行性咨询
你碰到的情况完全印证了TBB文档里的核心观点:只有当单个并行任务的执行时间足够长,能抵消线程调度、任务拆分的开销时,并行化才有意义。咱们来拆解问题本质和可行的优化方向:
一、当前并行化失效的核心原因
你的场景是单文本行匹配数千个模式,但每个matchWithoutProcessing的耗时极短:
- 像精确匹配、行首匹配、子串匹配这类操作,单个执行可能只需要几纳秒到几十纳秒;
- 即使是正则表达式,简单规则的耗时也不会太长。
而并行化带来的额外开销(线程切换、任务队列调度、结果收集的同步/合并)却要大得多——比如一次线程切换可能要几百纳秒甚至微秒级。这么算下来,多核心省下来的那点时间,完全被并行框架的开销吃掉了,甚至因为频繁调度反而变慢。
另外你的基准测试里,部分TBB实现标注了"semantically incorrect"(没有收集结果),但即使修正这一点,核心问题还是任务粒度太细,单个任务的收益远小于开销。
二、并行化不是死胡同,但要换思路
与其给每个触发器单独开并行任务,不如从批量处理+粗粒度并行入手:
1. 对同类模式做批量优化(最关键)
把同类型的模式合并成批量匹配结构,大幅减少单个匹配的数量:
- 精确/子串匹配:用Aho-Corasick自动机把所有子串/精确模式编译成一个状态机,一次扫描文本就能匹配所有模式,效率比逐个匹配高几个数量级;
- 正则表达式:把功能相近的正则合并成一个带分支的大正则(比如
pattern1|pattern2|...),但要注意正则引擎的分支优化,避免出现性能退化; - 行首匹配:可以整理成前缀树(Trie),批量匹配文本开头是否符合任一前缀。
这样处理后,单个批量匹配任务的耗时足够长,再用多核心并行处理不同的模式组(比如一个线程处理AC自动机,一个处理合并后的正则),就能真正发挥多核心的优势。
2. 调整并行粒度,避免细粒度任务
如果暂时没法做批量模式合并,可以把触发器分成大组(比如每500-1000个为一组),每组作为一个并行任务。你测试里的大grainsize(比如1000)如果修正结果收集逻辑(比如用tbb::parallel_reduce合并每个块的匹配结果),应该能看到一定的性能提升——但这只是权宜之计,远不如批量处理高效。
3. 尝试SIMD指令加速
对于子串、精确匹配这类简单操作,可以用SIMD指令(比如SSE、AVX)批量处理多个模式,这比多线程的开销小很多,适合细粒度的批量操作,很多成熟的字符串库都有现成的SIMD优化实现。
三、基准测试代码的修正建议
你那些"semantically incorrect"的TBB实现,可以改成线程本地收集结果再合并的方式,比如用tbb::parallel_reduce:
benchmark.run("tbb::parallel_reduce (correct result)", [&] { std::vector<TTrigger*> output; output.reserve(mParallelizableTriggers.size()); auto result = tbb::parallel_reduce( tbb::blocked_range<int>(0, mParallelizableTriggers.size()), std::vector<TTrigger*>(), [&](const tbb::blocked_range<int>& r, std::vector<TTrigger*> local) { for(int i=r.begin(); i!=r.end(); ++i) { auto trigger = mParallelizableTriggers.at(i); if (trigger->matchWithoutProcessing(subject, data, line)) { local.push_back(trigger); } } return local; }, [&](std::vector<TTrigger*> a, std::vector<TTrigger*> b) { a.insert(a.end(), b.begin(), b.end()); return a; } ); output.swap(result); });
这种方式避免了线程安全容器的同步开销,每个线程先本地收集结果,最后合并,能减少一部分并行开销。
总结
当前的细粒度并行确实走不通,但通过批量模式处理+合理的粗粒度并行,你完全可以利用多核心提升单文本行的多模式匹配性能。
内容的提问来源于stack exchange,提问作者Vadim Peretokin

