pbc 库的 lua binding

来源:互联网 发布:淘宝儿童玩具电子琴 编辑:程序博客网 时间:2024/06/08 01:39

http://blog.codingnow.com/2011/12/pbc_lua_binding.html


前几天写的 pbc 初衷就是想可以方便的 binding 到动态语言中去用的。所以今天花了整整一天自己写了个简单的 lua binding 库,就是很自然的工作了。

写完了之后,我很好奇性能怎样,就写了一个非常简单的测试程序测了一下。当然这个测试不说明很多问题,因为测试用的数据实在是太简单了,等明天有空再弄个复杂点的来跑一下吧。我很奇怪,为什么 google 官方的 C++ 版性能这么差。

我的 lua 测试代码大约是这样的:

local protobuf = require "protobuf"addr = io.open("../../build/addressbook.pb","rb")buffer = addr:read "*a"addr:close()protobuf.register(buffer)for i=1,1000000 do    local person = {        name = "Alice",        id = 123,    }    local buffer = protobuf.encode("tutorial.Person", person)    local t = protobuf.decode("tutorial.Person", buffer)end

100 万次的编码和解码在我目前的机器上,耗时 3.8s 。

为了适应性能要求极高的场合,我还提供了另一组高性能 api 。他们可以把数据平坦展开在 lua 栈上,而不构成 table 。只需要把循环里的代码换成

    local buffer = protobuf.pack(        "tutorial.Person name id",         "Alice", 123)    protobuf.unpack("tutorial.Person name id", buffer)

就可以了。这个版本只需要耗时 0.9s 。

一个月前,我曾经自己用 luajit + ffi 实现过一个纯 lua 的版本(没有开源),我跑了一下这个 case ,那个版本也很给力,达到前面的接口的功能,只需要 2.1s 。

不过我相信我新写的 binding 慢主要还是慢在 lua 上, 我换上了 luajit 跑以后,果然快了很多。

table 版本的耗时 1.7s , 平坦展开版是 0.57s.

看来 luajit 的优化力度很大。

btw, 我去年早些时候还写过一个 lua binding ,今天也顺便测了一下,在 luajit 下跑的时间是 1.2s 。没有这次写的这个版本快。

最后,我随手写了一个 C++ 的版本。应该有不少优化途径。不过我想这也是某中常规用法。

#include <iostream>#include <sstream>#include <string>#include "addressbook.pb.h"using namespace std;int main(int argc, char* argv[]) {  GOOGLE_PROTOBUF_VERIFY_VERSION;  for (int i=0;i<1000000;i++) {      tutorial::Person person;      person.set_name("Alice");      person.set_id(123);      stringstream output;      person.SerializeToOstream(&output);      output.str();      tutorial::Person person2;      person2.ParseFromIstream(&output);      person.name();      person.id();  }  google::protobuf::ShutdownProtobufLibrary();  return 0;}

这段代码在开了 -O2 编译后,在我的机器上依旧需要时间 1.9s。若是这么看,那简直是太慢了 (比 luajit + c binding 还慢)。很久没研究 C++ 的细节,也懒得看了,如果谁有兴趣研究一下为什么 C++ 这么慢,我很有兴趣知道原因。


12 月 16 日

留言中 lifc0 说这段 C++ 代码中开销最大的是 stringstream 的构造和销毁, 所以我改了一段代码:

stringstream output;stringstream input;for (int i=0;i<1000000;i++) {    output.clear();    output.str("");    tutorial::Person person;    person.set_name("Alice");    person.set_id(123);    person.SerializeToOstream(&output);    input.clear();    input.str(output.str());    tutorial::Person person2;    person2.ParseFromIstream(&input);    person2.name();    person2.id();}

这样更符合现实应用, 每次初始化 stringstream 而不构造新的出来.

这样运行时间就从 1.90s 下降到 1.18s 了.

COMMENTS

不支持扩展吗?

坏处就是要附带.proto 文件,协议容易被破解

坏处就是要附带.proto 文件,协议容易被破解

在游戏服务器的io序列化中使用protobuf是低效的,测试结果显示更简单直接的序列化代码可以比protobuf快5-10倍。而这些序列化代码,通过协议数据结构定义可以自动生成,很容易就写出这样的工具脚本。如果追求效率,游戏服务器处理消息并不适合使用probobuf。惟一潜在用途是数据库。因为数据结构频繁改动,使得读取老数据有问题,而probobuf则很适合这个场合。

推荐使用显式的销毁 buffer 的接口。 gc 那个只是 5.2 支持顺手加上的。

一个小项目需要lua+protobuf,研究了纯c的nanopb、protobuf-c、upb和lua的lua-pb、lua-protobuf、protoc-gen-lua都不太合适。upb只能decode没encode,其他一些要么需要代码生成,要么没有lua绑定,纯lua的方案则不方便和c代码交换消息。
本打算花点时间给nanopb加上lua绑定,后来在google groups上搜索lightweight、fast等关键字时无意看到http://comments.gmane.org/gmane.comp.lib.protocol-buffers.general/7972,去github研究之后觉得几乎就是我理想中的模式(和nanopb思路差不多,但实现了lua绑定),仔细看原来是云风大侠的作品。再搜相关资料来到这个页面,居然去年还过来踩过一脚。

给pbc提个小建议:lua绑定有几处用setmetatable给table绑定__gc元方法,但lua 5.1似乎只处理userdata的__gc,不知是否5.2做了相关调整,等有空再去研究。

for (int i = 0; i < 1000000; i++)
{
Person person;
person.set_name("Alice");
person.set_id(123);
std::string s = person.SerializeAsString();
person.ParseFromString(s);
}

要源码的同学就是不肯去 github 自己取?难道要人打包好 email 才行么?

请问能否将你的bind库开源呢?

@tearshark

把一小短测试代码, 针对性的优化是没有意义的.

实际不可能这样连着用,因为这样的代码段其实什么事情都没有做.

对于任何语言的任何代码片断, 都是以最舒适和直观的写出来为最常规的用法.

对于一个通用库来说尤其如此,因为它是给许多不同的人在不同的场合用的.

就这段代码而言. 一个常规的用法 :

比如在数据编码阶段是, 准备一个输出流, 准备一个待序列化的结构, 装填结构的数据, 序列化到流, 流输出.

这些步骤在实际用的时候是在不同流程,不同时机去做的, 甚至不是一个人来维护, 在同一模块里出现.

测试代码应体现这些流程和步骤, 而不是想办法放在一起,再看看哪里可以优化, 这样得到的优化结果没有太多意义.

for (int i=0;i<1000000;i++) {
output.clear();
output.str(""); //构造/拷贝/析购string,释放内存,分配内存

tutorial::Person person; //构造person
person.set_name("Alice");
person.set_id(123);

person.SerializeToOstream(&output);

input.clear();
input.str(output.str()); //构造/拷贝/析购string,释放内存,分配内存

tutorial::Person person2; //构造person2

person2.ParseFromIstream(&input);

person2.name();
person2.id();
}

这段代码测试什么的呢?内存分配?
input和output用相同的对象,然后通过seek操作重用数据不更好?
另外,stringstream笨拙的可以,还不如vector<>.clear()----至少我见过的vector<>的实现,clear()都不会真正的删除内存.
如果把stringstream替换成vector<>的实现,则我心目中理想的写法是

vectorstream<char> input;
tutorial::Person person;
tutorial::Person person2;

for (int i=0;i<1000000;i++) {
input.clear();
person.set_name("Alice");
person.set_id(123);

person.SerializeToOstream(&input);

input.seek(0);
person2.ParseFromIstream(&input);

person2.name();
person2.id();
}

这样可以尽量避免input的反复内存分配导致的效率低下.
C++真不是适合新手使用的库,到处都是陷阱,特别的STL的IO实现部分.还不如C.


原创粉丝点击