亲爱的小伙伴们我又回来写正则表达式的东东啦.
有很多小伙伴觉得正则表达式的执行效率低下, 不如自己写的字符匹配. 其实嘛, 这个问题和引擎的实现关系相当大, 现在的很多通用正则表达式引擎在确保功能性的条件下都是使用NFA引擎, NFA引擎在算法时间复杂度上比DFA引擎要高, 特别是在遇到很多回溯的情况下, 差异尤为显著, 关于NFA引擎的优化就显得十分重要啦. 我在之前的文章如何让你的正则表达式拥有更好的性能里面介绍了一些关于NFA引擎的优化方案, 效果是十分显著哒. 过了一段时间我对我的正则表达式引擎又增加了一些优化流程, 效率更高了. 这里再次拿出来和大家分享.
上一篇文章限制于篇幅没有对优化的流程做很详细的介绍, 只是描述了执行思想, 这里我使用一个具体的例子来为大家讲述我的正则表达式引擎是如何进行静态分析和优化的.
首先呢还是要介绍正则表达式的执行模型, 这个在之前的文章正则表达式与AOT编译和如何让你的正则表达式拥有更好的性能里提到过多次啦, 这里对细节不再赘述, 想要了解的小伙伴请看之前的文章吧.
我们的执行模型是建立在一个非递归的虚拟机上的. 一个典型的虚拟机有以下几条指令:
匹配单个字符,
匹配某个范围的字符(如数字
为
)
和
条件分支, 匹配成功或失败时进行跳转, 如
为匹配字符
成功跳转至地址
否则跳转至地址
.
分裂当前的匹配状态, 使用于分支
, 分裂当前匹配进程, 其中一条进行匹配
另一条匹配
.
和
条件分裂当前的匹配状态, 匹配成功则进行分裂, 否则直接跳转到后一个地址.
无条件跳转.
匹配锚点, 对应于
.
向后位移一位, 无条件跳过当前匹配的字符.
匹配成功,
匹配失败放弃当前状态.
当然啦, 这个虚拟机模型是为了阐述原理而高度简化的, 不过核心思想是一样哒.
文章下面使用一个非常典型的正则表达式展示优化过程. IP匹配是正则表达式中比较常见和典型的例子, 表达式为:
这个表达式有点点长, 不过不是很难懂, 我们发现它有四个重复的部分用于匹配0到255的数字, 我们取其中一个进行分析:
剩下的部分做一次循环展开后是完全相同的.
最开始是词法分析, 语法分析, NFA构建和指令生成, 这里不做过多介绍, 我们主要关注的是接下来的优化(想了解的小伙伴可以去看第一篇文章正则表达式与AOT编译).
生成的未优化的指令如下:
接下来开始应用优化
跳转合并
连续的无条件的跳转可以合并为一条跳转指令
进行合并后为:
这是一个非常常见的优化啦.
分支合并
第一步开始我们发现 中有共同的匹配前缀
, 将公共表达式提取出来, 即等价于将表达式变换为
, 得到的指令如下:
条件分裂/分支
我们发现如果使用分支之后第一个字符进行提前匹配, 这样可以减少一些不必要的回溯, 例如在第一个字符为1的情况下没必要进入地址为3的分支对2进行匹配, 可以直接跳转到23, 开启条件分支之后的指令如下
注意在例如 匹配成功进入分支之后我们无需再匹配一次
, 因为由
的条件知道当前字符
一定匹配, 所以将
替换为
直接跳过当前字符, 减少一次重复匹配.
在分支9和15上, 即对应表达式 , 我们知道这两个表达式匹配的第一个字符没有相交, 所以不需要分裂状态, 直接使用条件分支
.
在分支44和47上, 若匹配 成功后匹配成功, 不需要回溯, 即可使用条件分支.
分支分析
在有了条件分裂和分支的信息之后我们可以对连续的条件分裂进行分析, 减少重复匹配.
例如:
在这里我们知道如果进入了分支 后匹配失败了, 例如匹配
, 那么回溯之后会到地址23上, 进行匹配
, 但是我们已经知道第一个字符匹配了
, 这里的匹配一定会失败, 将直接跳转到地址36, 故可以使用条件分支进行更细致的状态分裂.
变换后的指令如下:
留意对 的匹配成功与失败的分支的不同.
分支延迟
在上一步分析中我们得到了:
在执行地址4的 时已经匹配了字符
另外一条分支39中立即进行匹配的是
, 由于
是
的子集, 若该分支匹配失败进行回溯, 那么回溯后的
将会一定匹配成功, 这时便可将
推迟到
之后, 得到的指令如下:
在接下来的条件分支 中, 可以将该分支42推迟进入8和15中, 进一步延迟分支, 对于分支
亦可进行同样的操作, 得到的指令如下:
对于 和
的两条分支仍然可以继续向后推迟分支. 直到无法继续推迟为止, 我们得到了高度优化的指令:
这里还有一个小优化就是把直接跳转到 的
指令全部替换为
这样可以节省一条跳转指令.
注意啦, 优化完成后的指令是不含回退的, 一次性匹配, 效率是非常高的. 另外这一份指令很容易自动翻译成C++代码进行AOT编译进一步提升效率.
到这里当然少不了的就是benchmark啦, 接下来是激动人心的时刻啦, 来看看优化后的指令和其他正则表达式库的效率对比吧:
这里我选了PCRE和RE2作为对比:
Test case
RegExp ^(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$
Match 222.34.191.23
My implement
VM: 7.1625 ns
C++ Native: 1.1776 ns
Comparison
PCRE2: 28.5163 ns
PCRE2 JIT: 4.6530 ns
RE2: 13.7799 ns
可以看到使用C++编译执行是PCRE2 JIT的3倍多的效率, 这份代码的效率已经非常接近甚至可以说等于手工编写的自动机了, 就算是通过VM解释执行的效率也不错, 达到了PCRE2 JIT的64%.
最后就是C++与VM实现的源码啦:
其中的byte code都是自动生成的.
VM解释执行:
#include <string>
#include <vector>
#define MINI_REGEX_MAX_STACK_SIZE 256
enum BYTE_CODE : size_t
{
MATCH_CH,
MATCH_RANGE,
IF_CH,
IF_RANGE,
INC,
BEGIN,
END,
SPLIT,
JMP,
ACCEPT,
HALT,
};
const std::vector<size_t> byte_code =
{
/*addr_0 */ BEGIN,
/*addr_1 */ IF_CH, '2', 5, 33,
/*addr_5 */ INC,
/*addr_6 */ IF_CH, '5', 10, 19,
/*addr_10 */ INC,
/*addr_11 */ IF_RANGE, '0', '5', 16, 61,
/*addr_16 */ INC,
/*addr_17 */ JMP, 61,
/*addr_19 */ IF_RANGE, '0', '4', 24, 55,
/*addr_24 */ INC,
/*addr_25 */ IF_RANGE, '0', '9', 30, 61,
/*addr_30 */ INC,
/*addr_31 */ JMP, 61,
/*addr_33 */ IF_CH, '1', 37, 52,
/*addr_37 */ INC,
/*addr_38 */ IF_RANGE, '0', '9', 43, 61,
/*addr_43 */ INC,
/*addr_44 */ IF_RANGE, '0', '9', 49, 61,
/*addr_49 */ INC,
/*addr_50 */ JMP, 61,
/*addr_52 */ MATCH_RANGE, '0', '9',
/*addr_55 */ IF_RANGE, '0', '9', 60, 61,
/*addr_60 */ INC,
/*addr_61 */ MATCH_CH, '.',
/*addr_63 */ IF_CH, '2', 67, 95,
/*addr_67 */ INC,
/*addr_68 */ IF_CH, '5', 72, 81,
/*addr_72 */ INC,
/*addr_73 */ IF_RANGE, '0', '5', 78, 123,
/*addr_78 */ INC,
/*addr_79 */ JMP, 123,
/*addr_81 */ IF_RANGE, '0', '4', 86, 117,
/*addr_86 */ INC,
/*addr_87 */ IF_RANGE, '0', '9', 92, 123,
/*addr_92 */ INC,
/*addr_93 */ JMP, 123,
/*addr_95 */ IF_CH, '1', 99, 114,
/*addr_99 */ INC,
/*addr_100*/ IF_RANGE, '0', '9', 105, 123,
/*addr_105*/ INC,
/*addr_106*/ IF_RANGE, '0', '9', 111, 123,
/*addr_111*/ INC,
/*addr_112*/ JMP, 123,
/*addr_114*/ MATCH_RANGE, '0', '9',
/*addr_117*/ IF_RANGE, '0', '9', 122, 123,
/*addr_122*/ INC,
/*addr_123*/ MATCH_CH, '.',
/*addr_125*/ IF_CH, '2', 129, 157,
/*addr_129*/ INC,
/*addr_130*/ IF_CH, '5', 134, 143,
/*addr_134*/ INC,
/*addr_135*/ IF_RANGE, '0', '5', 140, 185,
/*addr_140*/ INC,
/*addr_141*/ JMP, 185,
/*addr_143*/ IF_RANGE, '0', '4', 148, 179,
/*addr_148*/ INC,
/*addr_149*/ IF_RANGE, '0', '9', 154, 185,
/*addr_154*/ INC,
/*addr_155*/ JMP, 185,
/*addr_157*/ IF_CH, '1', 161, 176,
/*addr_161*/ INC,
/*addr_162*/ IF_RANGE, '0', '9', 167, 185,
/*addr_167*/ INC,
/*addr_168*/ IF_RANGE, '0', '9', 173, 185,
/*addr_173*/ INC,
/*addr_174*/ JMP, 185,
/*addr_176*/ MATCH_RANGE, '0', '9',
/*addr_179*/ IF_RANGE, '0', '9', 184, 185,
/*addr_184*/ INC,
/*addr_185*/ MATCH_CH, '.',
/*addr_187*/ IF_CH, '2', 191, 219,
/*addr_191*/ INC,
/*addr_192*/ IF_CH, '5', 196, 205,
/*addr_196*/ INC,
/*addr_197*/ IF_RANGE, '0', '5', 202, 247,
/*addr_202*/ INC,
/*addr_203*/ JMP, 247,
/*addr_205*/ IF_RANGE, '0', '4', 210, 241,
/*addr_210*/ INC,
/*addr_211*/ IF_RANGE, '0', '9', 216, 247,
/*addr_216*/ INC,
/*addr_217*/ JMP, 247,
/*addr_219*/ IF_CH, '1', 223, 238,
/*addr_223*/ INC,
/*addr_224*/ IF_RANGE, '0', '9', 229, 247,
/*addr_229*/ INC,
/*addr_230*/ IF_RANGE, '0', '9', 235, 247,
/*addr_235*/ INC,
/*addr_236*/ JMP, 247,
/*addr_238*/ MATCH_RANGE, '0', '9',
/*addr_241*/ IF_RANGE, '0', '9', 246, 247,
/*addr_246*/ INC,
/*addr_247*/ END,
/*addr_248*/ ACCEPT,
};
struct state
{
size_t IP;
size_t index;
};
std::vector<state> state_stack;
bool match(const std::string& str)
{
size_t off = 0;
while (off < str.length())
{
state_stack.clear();
state_stack.push_back({ 0, off++ });
fail_loop:;
while (!state_stack.empty() && state_stack.size() < MINI_REGEX_MAX_STACK_SIZE)
{
auto IP = state_stack.back().IP;
auto index = state_stack.back().index;
state_stack.pop_back();
next_loop:;
switch (byte_code[IP])
{
case BYTE_CODE::MATCH_CH:
if (index < str.length() && (str[index] == byte_code[IP + 1]))
{
index++;
IP += 2;
goto next_loop;
}
goto fail_loop;
case BYTE_CODE::MATCH_RANGE:
if (index < str.length() &&
str[index] >= byte_code[IP + 1] &&
str[index] <= byte_code[IP + 2])
{
index++;
IP += 3;
goto next_loop;
}
goto fail_loop;
case BYTE_CODE::IF_CH:
if (index < str.length() && str[index] == byte_code[IP + 1])
{
IP = byte_code[IP + 2];
goto next_loop;
}
else
{
IP = byte_code[IP + 3];
goto next_loop;
}
case BYTE_CODE::IF_RANGE:
if (index < str.length() &&
str[index] >= byte_code[IP + 1] &&
str[index] <= byte_code[IP + 2])
{
IP = byte_code[IP + 3];
goto next_loop;
}
else
{
IP = byte_code[IP + 4];
goto next_loop;
}
case BYTE_CODE::INC:
index++;
IP++;
goto next_loop;
case BYTE_CODE::BEGIN:
if (index == 0)
{
IP++;
goto next_loop;
}
goto fail_loop;
case BYTE_CODE::END:
if (index == str.length())
{
IP++;
goto next_loop;
}
goto fail_loop;
case BYTE_CODE::SPLIT:
state_stack.push_back({ byte_code[IP + 2], index });
IP = byte_code[IP + 1];
goto next_loop;
case BYTE_CODE::JMP:
IP = byte_code[IP + 1];
goto next_loop;
case BYTE_CODE::ACCEPT:
return true;
case BYTE_CODE::HALT:
default:
return false;
}
}
}
return false;
}(这个虚拟机的实现还有优化空间, 例如使用direct threading优化指令分发, 大概还能快10%)
C++编译执行:
#include <string>
#define MATCH_CH(CH) \
do { \
if(!(index < str.length() && str[index] == CH)) \
return false; \
index++; \
} while(0)
#define MATCH_RANGE(CH1, CH2) \
do { \
if(!(index < str.length() && str[index] >= CH1 && str[index] <= CH2)) \
return false; \
index++; \
} while(0)
#define IF_CH(CH, THEN, ELSE) \
do { \
if(index >= str.length() || str[index] != CH) \
goto ELSE; \
else \
goto THEN; \
} while(0)
#define IF_RANGE(CH1, CH2, THEN, ELSE) \
do { \
if(index >= str.length() || str[index] < CH1 || str[index] > CH2) \
goto ELSE; \
else \
goto THEN; \
} while(0)
#define JMP(TO) \
do { \
goto TO; \
} while(0)
#define INC() \
do { \
index++; \
} while(0)
#define BEGIN() \
do { \
if(index != 0) \
return false; \
} while(0)
#define END() \
do { \
if(index != str.length()) \
return false; \
} while(0)
#define ACCEPT() \
return true;
bool match(const std::string& str)
{
std::string::size_type index = 0;
addr_0 : BEGIN();
addr_1 : IF_CH('2', addr_5, addr_33);
addr_5 : INC();
addr_6 : IF_CH('5', addr_10, addr_19);
addr_10 : INC();
addr_11 : IF_RANGE('0', '5', addr_16, addr_61);
addr_16 : INC();
addr_17 : JMP(addr_61);
addr_19 : IF_RANGE('0', '4', addr_24, addr_55);
addr_24 : INC();
addr_25 : IF_RANGE('0', '9', addr_30, addr_61);
addr_30 : INC();
addr_31 : JMP(addr_61);
addr_33 : IF_CH('1', addr_37, addr_52);
addr_37 : INC();
addr_38 : IF_RANGE('0', '9', addr_43, addr_61);
addr_43 : INC();
addr_44 : IF_RANGE('0', '9', addr_49, addr_61);
addr_49 : INC();
addr_50 : JMP(addr_61);
addr_52 : MATCH_RANGE('0', '9');
addr_55 : IF_RANGE('0', '9', addr_60, addr_61);
addr_60 : INC();
addr_61 : MATCH_CH('.');
addr_63 : IF_CH('2', addr_67, addr_95);
addr_67 : INC();
addr_68 : IF_CH('5', addr_72, addr_81);
addr_72 : INC();
addr_73 : IF_RANGE('0', '5', addr_78, addr_123);
addr_78 : INC();
addr_79 : JMP(addr_123);
addr_81 : IF_RANGE('0', '4', addr_86, addr_117);
addr_86 : INC();
addr_87 : IF_RANGE('0', '9', addr_92, addr_123);
addr_92 : INC();
addr_93 : JMP(addr_123);
addr_95 : IF_CH('1', addr_99, addr_114);
addr_99 : INC();
addr_100: IF_RANGE('0', '9', addr_105, addr_123);
addr_105: INC();
addr_106: IF_RANGE('0', '9', addr_111, addr_123);
addr_111: INC();
addr_112: JMP(addr_123);
addr_114: MATCH_RANGE('0', '9');
addr_117: IF_RANGE('0', '9', addr_122, addr_123);
addr_122: INC();
addr_123: MATCH_CH('.');
addr_125: IF_CH('2', addr_129, addr_157);
addr_129: INC();
addr_130: IF_CH('5', addr_134, addr_143);
addr_134: INC();
addr_135: IF_RANGE('0', '5', addr_140, addr_185);
addr_140: INC();
addr_141: JMP(addr_185);
addr_143: IF_RANGE('0', '4', addr_148, addr_179);
addr_148: INC();
addr_149: IF_RANGE('0', '9', addr_154, addr_185);
addr_154: INC();
addr_155: JMP(addr_185);
addr_157: IF_CH('1', addr_161, addr_176);
addr_161: INC();
addr_162: IF_RANGE('0', '9', addr_167, addr_185);
addr_167: INC();
addr_168: IF_RANGE('0', '9', addr_173, addr_185);
addr_173: INC();
addr_174: JMP(addr_185);
addr_176: MATCH_RANGE('0', '9');
addr_179: IF_RANGE('0', '9', addr_184, addr_185);
addr_184: INC();
addr_185: MATCH_CH('.');
addr_187: IF_CH('2', addr_191, addr_219);
addr_191: INC();
addr_192: IF_CH('5', addr_196, addr_205);
addr_196: INC();
addr_197: IF_RANGE('0', '5', addr_202, addr_247);
addr_202: INC();
addr_203: JMP(addr_247);
addr_205: IF_RANGE('0', '4', addr_210, addr_241);
addr_210: INC();
addr_211: IF_RANGE('0', '9', addr_216, addr_247);
addr_216: INC();
addr_217: JMP(addr_247);
addr_219: IF_CH('1', addr_223, addr_238);
addr_223: INC();
addr_224: IF_RANGE('0', '9', addr_229, addr_247);
addr_229: INC();
addr_230: IF_RANGE('0', '9', addr_235, addr_247);
addr_235: INC();
addr_236: JMP(addr_247);
addr_238: MATCH_RANGE('0', '9');
addr_241: IF_RANGE('0', '9', addr_246, addr_247);
addr_246: INC();
addr_247: END();
addr_248: ACCEPT();
return false;
}谢谢大家的阅读啦~
代码生成和指令优化器是使用的是我的千雪的生成器和优化器, 其中做了一些小改动.
如果有需要做正则表达式的静态编译的小伙伴欢迎来讨论喔.