使用XS实现55倍速度提升之路

最近我的客户一直在关注提高应用程序的速度,所以我自然开始考虑XS,Perl的C宏语言。通过XS,您可以编写C代码并从Perl中调用它。
为了试探一下,我使用C编写了一个简单的URI编码/解码器,并通过一些尝试和错误,成功地创建了URI::Encode::XS,这是一个使用它的模块。“这很简单!”我想着,兴奋地输入了一个基准测试脚本。我将我的模块与URI::Escape进行了基准测试,它是一个古老的但相当慢的纯Perl URI编码/解码器。你可以想象当我看到基准测试结果,发现所有的努力只带来了20%的速度提升时,我是多么沮丧。我怀疑Perl的字符串函数是否非常快,以至于很难改进。
重燃希望
现在出现了URI::XSEscape,这是一个“快速而简单”的(作者的话)XS实现的URI::Escape。它在上个月上传到了CPAN。你可以亲自查看作者的基准测试,但在我的测试中,它似乎比URI::Escape快了大约18.5倍。这不是一个错误 - 在我的笔记本电脑上,它每秒编码了2.75m个字符串,而URI::Escape是138k。那么他们是怎么做到的?
首先,让我们看看我的简单的C编码实现
void *uri_encode (char *uri, const char *special_chars, char *buffer)
{
int i = 0;
/* \0 is null, end of the string */
while (uri[i] != '\0')
{
int encode_char = 1;
int j = 0;
while (special_chars[j] != '\0')
{
if (uri[i] == special_chars[j])
{
/* do not encode char as it is in the special_chars set */
encode_char = 0;
break;
}
j++;
}
if (encode_char == 1)
{
char code[4];
sprintf(code, "%%%02X", uri[i]);
strcat(buffer, code);
}
else
{
char code[2];
code[0] = uri[i];
code[1] = '\0';
strcat(buffer, code);
}
i++;
}
}
基本上这个函数是遍历uri
字符串,查找在special_chars
字符串中的字符,如果找到匹配项,则使用sprintf
将字符进行百分编码,并将结果追加到buffer
,这是编码后的字符串。与URI::XSEscape
的编码函数进行比较(我稍作简化)
Buffer* uri_encode(Buffer* src, int length,
Buffer* tgt)
{
int s = src->pos;
int t = tgt->pos;
while (s < (src->pos + length)) {
unsigned char u = (unsigned char) src->data[s];
char* v = uri_encode_tbl[(int)u];
/* if current source character doesn't need to be encoded,
just copy it to target*/
if (!v) {
tgt->data[t++] = src->data[s++];
continue;
}
/* copy encoded character from our table */
tgt->data[t+0] = '%';
tgt->data[t+1] = v[0];
tgt->data[t+2] = v[1];
/* we used up 3 characters (%XY) in target
* and 1 character from source */
t += 3;
++s;
}
/* null-terminate target and return src as was left */
src->pos = s;
tgt->pos = t;
buffer_terminate(tgt);
return src;
}
这段代码遍历输入字符串,但不是将当前字符与另一个特殊字符字符串进行比较,而是进行表查找。这比遍历另一个字符串要快得多。注意它也没有使用sprintf
- 所有的十六进制码都预先计算在uri_encode_tbl
中。最后,不是创建一个新的字符串并将其连接到输出字符串上,而是直接将输出复制到输出字符串的内存位置。
这段代码还避免了与我实现相关的微妙错误:Perl字符串可以包含空字符,但在C中,空字符用于终止字符串。因为URI::XSEscape的编码函数接受一个长度参数,它可以编码包含空字符的字符串,而我的版本不能。
更快地运行
在此之后,我将URI::Encode::XS中的编码/解码函数更新为基于表的,这带来了巨大的性能提升,使URI::Encode::XS比URI::Escape快了约25倍(URI::Encode::XS不支持用户定义的转义值,所以它比URI::XSEscape简单)。我认为25倍的改进已经足够好了,我打算结束这个模块的开发,这时我联系到了Christian Hansen(Time::Moment的作者)。Christian对我的简单XS代码进行了彻底的改进,使其更安全、更快。这就是uri_encode
C函数的成果
size_t uri_encode (const char *src, const size_t len, char *dst)
{
size_t i = 0, j = 0;
while (i < len
{
const char * code = uri_encode_tbl[ (unsigned char)src[i] ];
if (code)
{
memcpy(&dst[j], code, 3);
j += 3;
}
else
{
dst[j] = src[i];
j++;
}
i++;
}
dst[j] = '\0';
return j;
}
本版本在预计算的表中查找字符值,然后使用memcpy
将其追加到输出字符串(避免3次单独赋值)。它还返回编码字符串的长度,这很有用。经过Christian的优化后,我的基准测试脚本显示URI::Encode::XS的编码函数比URI::Escape快55倍(大约每秒编码8m个字符串)。大部分的改进都来自于对xsub的优化。
C语言的力量,Perl的乐趣
对我来说,XS代码最神奇的地方就是可以从Perl中调用它
use URI::Encode::XS 'uri_encode';
my $encoding = uri_encode($some_string); # super fast
因此,用户可以方便地编写Perl代码,同时获得更快实现的好处。Perl已经很快速了,但某些操作的成本很高。如果你在Perl应用程序上工作,如果你能将所有瓶颈提高55倍,它会快多少呢?
学习XS
如果你想了解更多关于XS的信息,我强烈推荐Steven W McDougall的这篇系列文章。这是我了解的最好的介绍。
XS is Fun是一个更现代的XS编程介绍,它带你一步步编写XS模块并导入C库。
高级Perl编程第一版中的第18章“扩展Perl:入门课程”对XS有一个很好的介绍。它涵盖了标量、数组和散列最常用的宏,这很有用(第二版没有涵盖XS)。扩展和嵌入Perl进一步深入,有关于通过XS调用和接收数据的几种不同方法的教程。这两本书都有些过时,但我认为它们很有价值,比官方文档更容易阅读。
官方Perl文档有有用的参考资源:perlxs、perlapi和perlguts。还有perlxstut,但我会选择上述资源而不是它。
我多次发现Perl代码中使用了XS宏,但这些宏在任何文档中都没有解释(例如dXSTARG
)。在这些情况下,拥有Perl的源代码副本会有所帮助——只需grep源代码,你就能找到它的定义和注释(通常在pp.h
中)。
关于基准测试的说明
本文中的基准测试都是在我的笔记本电脑上运行的,一台运行Fedora 23的戴尔XPS 13,8GB内存。不同的硬件将产生不同的结果(Christian的基准测试显示URI::Encode::XS比URI::Escape快90倍!)。
脚本计算每个模块每秒可以编码多少次字符串。但不同长度的字符串,或具有不同数量的保留字符将产生不同的基准测试。例如,在我的笔记本电脑上,空字符串的基准测试显示URI::Encode::XS仅快9倍。
模块版本是URI::Encode::XS v0.08和URI::Escape v3.31。Perl版本是5.22。
感谢
非常感谢Christian Hansen和Jesse DuMond对URI::Encode::XS的帮助。如果没有你们的贡献,这个模块将不会像现在这样。
本文最初发布在PerlTricks.com。
标签
反馈
这篇文章有什么问题吗?请在GitHub上为我们打开一个问题或拉取请求。
- More commenting... maybe?
github.polettix.it - Perl Weekly Challenge 121: Invert Bit
blogs.perl.org - Web nostalgia: MojoX::Mechanize
github.polettix.it - On the eve of CPAN Testers
blogs.perl.org - PWC121 - The Travelling Salesman
github.polettix.it - PWC121 - Invert Bit
github.polettix.it - Floyd-Warshall algorithm implementations
github.polettix.it - Perl Weekly Challenge 120: Swap Odd/Even Bits and Clock Angle
blogs.perl.org - How I Uploaded a CPAN Module
blogs.perl.org - App::Easer released on CPAN
github.polettix.it