整理Redis::Fast的痛点以及Redis::Fast为何延迟的原因

总结了在Redis::Fast中遇到的困难以及为什么Redis::Fast速度慢的原因。
或者
对Redis::Fast的困难和速度慢的原因进行了总结。

你好,我是一つ。
一年多之前,我将一个名为Redis::Fast的模块发布到了CPAN。
它是Redis.pm的XS版本,一个纯Perl的Redis客户端。
它与Redis.pm有一定的兼容性,只需将代码中的Redis替换为Redis::Fast即可正常工作。

由于我首次尝试将使用XS的模块发布到CPAN,结果导致了一系列的公开后错误修复问题,让我头疼不已。
经过一年的积累,现在我借此机会进行总结。

整理一下困难的经历。

用minilla进行XS和子模块操作

Redis::Fast是对PurePerl的Redis.pm进行分支,
并将后端替换为hiredis库的结果。
我发现用minilla可以查找CPAN模块的构建方法,
所以在分支时引入了minilla。
然而当时的minilla还不支持XS,
为了使用XS,我通过Module::Build::XSUtil对构建过程进行了各种繁琐的自定义处理。

在解决了XS中的各种问题后,正当准备发布的时候,遇到了一个无法编译的问题,即发布版本的包中没有包含hiredis。
当时使用git-submodule进行嵌入hiredis,但是当时的minilla并不会将submodule包含在发布包中。
由于有了git-submodule支持的fork版本,所以首次发布使用了该fork版本。

现在的 Minilla 已经支持 XS,所以只需要稍作配置即可使用 XS。
而且,我通过 songmu 先生向作者提出了对 git-submodule 的支持请求,并得到了积极响应。

内存泄漏

在Redis::Fast中遇到的内存泄漏可以大致分为两种类型。

有两个问题,第一个是参考计数的操作错误或循环引用。由于不熟悉XS的宏,导致参考计数没有变为0,或者由于频繁使用闭包而不知不觉地创建了循环引用。参考计数的操作错误可以通过使用sv_2mortal函数来避免,只要在创建新值时使用它,通常就可以防止这种情况发生。这样做可以在适当的时候自动释放XS。

SV * s = newSVpv("Hello World",0);  // Perl の文字列オブジェクト
sv_2motral(s) // 揮発性にすることで、使われなくなったら自動的に解放してくれる

在进行av_push(对数组进行添加)或hv_store(对哈希进行赋值)操作时,需要对SvREFCNT_inc进行增加,但在不确定的地方最好不要使用。虽然可以很快地注意到遗漏的地方,但很难注意到内存泄漏…。使用Test::LeakTrace进行编写测试可以进一步放心(在Redis::Fast中的使用示例)。

第二个问题是关于使用sv_2mortal函数将对象设置为可释放的时机问题。
可释放的对象会在”XS写的函数执行完并返回Perl”时释放。
在大多数情况下,这已经足够了,但在Redis::Fast中,有一种情况是”(除非超时)XS写的函数不会结束”的情况,
这样一来本应该被设置为可释放的对象将永远不会被释放。
在这种情况下,需要明确告知该可释放对象的有效范围。

sv_2motral(s);
ENTER;
SAVETMPS;
sv_2motral(v);
FREETMPS;
LEAVE;
// v はココで解放される
// s は生き残ってる

一旦函数结束,内存会被释放,因此在Test::LeakTrace中很难发现内存泄漏的问题,这是一个令人痛苦的地方。Devel::Refcount::refcount 和 Devel::Peek::SvREFCNT 看起来是唯一需要仔细测试引用计数变化的选择。请参考我以前写的博客文章。

重复释放

XS与此无关,但我也苦于双重释放的问题。
由于在正常情况和错误发生时需要释放内存的时机不同,
为了消除内存泄漏,我在常规处理中添加了释放操作,结果导致错误时经常发生双重释放。

    • freeする前にNULLチェック

 

    freeしたらNULLを代入する

我相信一切都会有办法解决的。

对于处理割入问题有困难

Perl不是立即处理信号处理程序,而是在能够安全执行信号处理程序的时机执行。
在PurePerl的世界中,Perl处理系统会考虑“能够安全执行的时机”,但在XS中不是这样。
如果不正确地告知“能够安全执行的时机”,信号处理程序将永远不会被执行。
特别是对于Redis::Fast,存在“(除非超时)由XS编写的函数不会结束”的情况,这是严重的。
在XS中,可以使用PERL_ASYNC_CHECK宏来提供正确的时机。
请参考我之前写的博客文章。

为什么Redis::Fast很慢?

这个地方已经是一个已解决但曾经令人痛苦的事情了。
从现在开始,我想要把焦点放在未解决的问题上。
因为有了一个更快的模块叫做Redis::Jet,所以我想要解释为什么Redis::Fast无法胜出,并征求能够解决这个问题的人的帮助。

自动加载/wantarray/代码引用

Redis::Fast 可以以以下三种方式进行调用:

my $hoge = $redis->command_name(‘args’) スカラコンテキスト

my @hoge = $redis->command_name(‘args’) リストコンテキスト

$redis->command_name(‘args’, sub{}) Pipelining

为了以这种形式调用,我们必须使用AUTOLOAD功能或者使用wantarray来区分调用方法,这就意味着我们不得不编写一些Perl代码并且会产生相应的开销。我认为这可能是导致无法完全赶上Redis::hiredis的原因之一。也有可能是因为我对Perl的能力还不够,所以请有经验的人帮我一下。

自动重新连接

Redis::Fast具有在与redis连接断开时自动重新连接的功能。由于在写入通信缓冲区后可能会发生重新连接,因此很难重用通信缓冲区,需要每次都重新分配缓冲区。最近似乎malloc也很快,但会积少成多。我认为这可能是与正在重用通信缓冲区的Redis::Jet之间的速度差异。

被雇佣的redis

我們在後端使用hiredis,為了使它能夠通用,我們將從redis收到的回應解析結果放入自訂的資料型別中。因此,從redis回應到hiredis的自訂資料型別,再到Perl的資料型別之間需要進行轉換,所以相比直接將解析結果放入Perl資料型別的Redis::Jet,速度會變慢。

最近几年来,hiredis的版本没有更新了,不知道升级是否会让它变得更快一些…(有点侥幸的想法)

最终

Redis::Fast 0.14 已经发布。
这个版本集成了 Redis.pm 1.976 的更改。
我们将持续改进,敬请期待。