News Hacker|极客洞察

129 74 天前 iev.ee
🚀用 F# 与衍生式派生构建的 RE#:理论双指数难题与实践上的极速折衷
宣称线性保证,是要用百万字样本证明吗?

🎯 讨论背景

原文与论文介绍了 RE#(实现于 F#,宣称支持 intersection 与 complement 且在大量基准上速度最快)的设计:基于正则衍生式(derivatives)按需构造 lazy DFA,并用双向扫描实现 leftmost-longest 匹配。讨论集中在学术上对扩展正则 DFA 最坏情况双指数爆炸的已知结果与作者提出的“实用上看似线性”保证的兼容性,评论里把焦点放在首次运行的生成开销、语法树简化(Brzozowski 的提醒)、hash-consing/memoization、以及在简单字面量场景下 Hyperscan/Rust regex 的 SIMD 优化等工程折衷上。社区还对功能覆盖(如缺少 submatch/captures)、Rust 原生实现何时开源、与 OMRN/ Hyperscan 的对比、以及 F# 作为实现语言在生态与可用性上的影响进行了讨论。

📌 讨论焦点

理论复杂度与衍生式(derivatives)方法的折衷

学术界指出对扩展正则(包含 intersection 与 complement)的完整 DFA 在最坏情况下会出现双指数级状态爆炸,这一结果被评论引用作为不支持交集/补集的理由。作者与评论者解释说这与文章中“线性时间保证”并不冲突:RE# 采用基于 Brzozowski 的 regular expression derivatives 并懒惰(lazy)地构造 DFA,通常最多为每个输入字符创建一个新状态,因此需要非常长的输入才会触发完整爆炸。不过每个新状态的生成并非常数时间,派生产生的语法树需要简化,工程上用 hash-consing 与 memoization 来把派生数量与大小控制在可接受范围内;首次遇到新输入代价高,重复相似输入后缓存使后续运行近似线性。评论同时承认在极端模式(例如多个 (.*a.*)&(.*b.*)&... 的交集)下仍会出现指数或双指数的状态增长,作者在论文里也承认这一点并以工程化手段缓解。

[来源1] [来源2] [来源3] [来源4]

基准与实际速度的场景化比较

有人对“最快”这一结论持怀疑态度,但作者在评论里澄清了适用场景:RE# 在“大模式”与大量 lookaround 的基准上表现优异,leftmost-longest 策略让引擎避免在 DFA/NFA 之间频繁切换,从而节省内存与时间。对比中指出对于可抽取出少量字面量的模式,Hyperscan 与 Rust regex(依赖左到右的 SIMD 优化与字面量抽取)通常更快;作者也说明已用 SearchValues 做某些回退优化。另有评论把 RE# 与 OMRN(将正则编译为本地汇编以极致优化的项目)对比,指出 OMRN 的 eager compilation 能迅速给出第一个匹配但牺牲部分正则特性,二者各有取舍。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8]

匹配语义与算法细节(leftmost-longest 与 AllEnds)

RE# 为了保证 leftmost-longest,采用双向扫描:先做右到左的 DFA 扫描以标记所有可能的 match starts,再用正向 DFA 找出 ends,然后将最左的 start 与最长的 end 配对得到 leftmost-longest 的结果。评论指出这种做法的直接后果是:在返回第一个匹配之前可能需要扫描整个输入,并且必须记住一组起始位置与结束位置(AllEnds 算法),因此匹配所有结果会产生与匹配总数成比例的分配与内存需求。作者与社区给出的工程化优化是把可合并的连续匹配压缩为范围(例如把 a* 的多次单字符匹配合并成区间)以减小 lookaround 的内部状态与存储开销。具体示例(如对 'aabbcc' 与模式 b+ 的说明)在评论中被用来说明为何需要这些折衷与实现细节。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7]

功能权衡:子匹配、延迟编译与抗对抗性

RE# 的卖点是支持交集/补集与无额外搜索时开销的无界 lookaround,但当前实现存在功能与性能权衡:评论指出它缺少 submatch(捕获组)提取功能,这对很多实用场景是重要缺陷。lazy compilation 的设计让第一次对新或恶意构造的输入开销很大——虽可以通过状态缓存缓解,但在单次长文本中寻找第一个匹配时并非最佳选择。社区建议将专门的字面量/多模式 SIMD 路径(如 SearchValues 或 Teddy/Hyperscan 的做法)作为快速路径回退,以兼顾简单模式下的速度与复杂模式下的表达能力。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7]

实现、开源与跨语言可用性

社区关心代码能否跨语言使用与何时开源原生实现:作者表示已有基于相同核心的字符串求解器公开仓库,Rust 原生引擎计划在短期内开源,但目前主要发布渠道是 .NET 的 NuGet 包。评论里有人指出学术相关实现(如 Mamouras 等人的 Haskell 实现 lregex)与作者的交互式 web demo 可以参考,并建议通过编译为本地库并提供 C 风格 wrapper 来方便在非 .NET 环境中调用。总体上多数人希望尽快拿到 Rust 版以便在更多语言/环境中做真实负载测试,同时在工程上将热路径替换为已知高性能字面量搜索以提升实用表现。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6]

F# 语言与生态讨论

评论中大量人在赞扬 F# 的简洁性与 ML 系语言的表达力,认为 F# 在 tokens 效率与类型表达上有明显优势,适合做这类偏研究/工具化的实现。与此同时也有人认为 F# 的流行度受限于它作为 .NET 的二线语言,微软生态更偏好 C# 导致社区感知不足。讨论还触及语言简洁性对工程生产力与可维护性的影响,部分人认为更结构化、强类型的语言(如 F#/OCaml/Haskell)在长期工程上优于更冗长的语言。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9] [来源10]

展示、命名与社区反响(次要问题)

尽管技术本身获得称赞,但评论中指出若干展示与命名问题:博文有读者抱怨句首小写与浅色主题下代码不可见,作者随后修复了这些可见性问题。另有评论提醒项目名 RE#(resharp)可能和 JetBrains 的 ReSharper(R#) 在 .NET 社区产生混淆,影响 SEO 或存在商标风险。总体情绪既有对算法与实现的欣赏,也有务实的可用性与品牌风险提醒,以及对“人类写作与实现”这类技术帖回归的怀念。

[来源1] [来源2] [来源3] [来源4] [来源5] [来源6] [来源7] [来源8] [来源9]

📚 术语解释

regular expression derivatives(衍生式/Brzozowski derivative): 一种通过计算在消费某个输入符号后正则表达式剩余匹配的技术(Brzozowski 的方法),把“看下一个字符后该做什么”表示为新的正则式,常用于按需构造 DFA 与实现无回溯匹配。

lazy DFA: 一种按需构造确定性有限自动机(DFA)的策略:不预先决定化整个 NFA,而是在匹配过程中根据实际输入逐步生成状态,能避免一次性爆炸但每个新状态生成有非平凡成本。

extended regular expressions(扩展正则:intersection / complement): 在传统正则的基础上加入交集(intersection)与补集(complement)等算子,虽然表达力更强但在构造 DFA 时可能导致最坏情况的指数或双指数增长。

leftmost-longest: 一种匹配优先级规则:选择最先开始(最左)的匹配,如果同一开始位置有多个匹配则选择最长的;实现上常需特殊算法(如双向扫描)避免回溯。

AllEnds algorithm: 文中/评论提到的用于找出所有可能 match 结束位置的算法思路:先 RTL 扫描收集 start,再 LTR 扫描计算 end,然后配对以得到 leftmost-longest 的所有匹配端点。

Teddy algorithm(Hyperscan 的一部分): Hyperscan 中用于高效字面量/多模式搜索的算法与工程实现(包含 SIMD 优化),擅长把可抽取的字面量模式当作快速路径以极大提升简单模式的吞吐。

SearchValues: .NET 中用于多字串/多值搜索的原语(可借助 SIMD),在评论里被提为把简单字面量情况做为回退快速路径的实现选项。

hash-consing 与 memoization: 共享不可变结构的技术(hash-consing)与记忆化(memoization)用来复用派生产生的语法树与状态,以限制衍生树增长并加速重复输入的匹配。