pbc 库的 lua binding
前几天写的 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++ 的版本。应该有不少优化途径。不过我想这也是某中常规用法。
这段代码在开了 -O2 编译后,在我的机器上依旧需要时间 1.9s。若是这么看,那简直是太慢了 (比 luajit + c binding 还慢)。很久没研究 C++ 的细节,也懒得看了,如果谁有兴趣研究一下为什么 C++ 这么慢,我很有兴趣知道原因。
12 月 16 日
留言中 lifc0 说这段 C++ 代码中开销最大的是 stringstream 的构造和销毁, 所以我改了一段代码:
这样更符合现实应用, 每次初始化 stringstream 而不构造新的出来.
这样运行时间就从 1.90s 下降到 1.18s 了.
Comments
云风大神,用pbc注册pb文件的时候失败,发现是用到import xxx的时候失败,是跟注册顺序有关系吗?需要先注册import xxx中的xxx的pb文件?
Posted by: shawn | (44) November 21, 2017 10:37 AM
我这边用了大侠你的pbc方案,lua中对整型数组好像不能解析啊。 麻烦大侠看看~
Posted by: zhucheng | (43) October 9, 2017 04:46 PM
Why not use std::vector<char> instead of std::stringstream ?
Posted by: ScottDong | (42) May 5, 2015 11:02 AM
看你 proto源码中的 float类型都强转成double类型了 这个真有点苦呀!我们java后台接收到关于float类型的数据直接奔掉 查了好久才知道是这个问题
Posted by: vic | (41) May 6, 2014 06:05 PM
补38楼:原来有protobuf.check...开始没看到
Posted by: hkspirt | (40) April 24, 2014 09:23 PM
补38楼:原来有protobuf.check...开始没看到
Posted by: hkspirt | (39) April 24, 2014 09:23 PM
在协议中存在嵌套时,decode并没有一次解析完,而且repeated和嵌套字段均为table,区别不开啊~~现在想到的办法就是1、repeated固定命名为xxx.list,2、导出_pbcP_get_message接口,判断table[1]是否需要继续解析~~感觉都不是太好~~求方案
Posted by: hkspirt | (38) April 24, 2014 08:39 PM
不好意思 风大侠
今天不甘心 看了你里面的源码也支持枚举类型
我就产生怀疑是不是我proto文件枚举问题
果然正着 我枚举是从0开始的 我换成1开始 立马让我乐乐起来
Posted by: vic | (37) April 23, 2014 04:41 PM
大侠 这个 lua的pbc protobuf 不支持枚举类型呀 丢pb进行解析直接报解析错误 找了好久以为pbc问题呢 请注明哟
Posted by: vic | (36) April 22, 2014 11:08 AM
extend google.protobuf.MessageOptions{
optional CGMessageType cg_message_type = 50001;
optional GCMessageType gc_message_type = 50002;
}
请问pbc对这种扩展是否支持呢?
Posted by: joey | (35) March 7, 2014 06:18 PM
不支持扩展吗?
Posted by: kudoo | (34) September 6, 2013 12:27 PM
坏处就是要附带.proto 文件,协议容易被破解
Posted by: fdsaf | (33) June 24, 2013 05:10 PM
坏处就是要附带.proto 文件,协议容易被破解
Posted by: fdsaf | (32) June 24, 2013 05:09 PM
在游戏服务器的io序列化中使用protobuf是低效的,测试结果显示更简单直接的序列化代码可以比protobuf快5-10倍。而这些序列化代码,通过协议数据结构定义可以自动生成,很容易就写出这样的工具脚本。如果追求效率,游戏服务器处理消息并不适合使用probobuf。惟一潜在用途是数据库。因为数据结构频繁改动,使得读取老数据有问题,而probobuf则很适合这个场合。
Posted by: pirunxi | (31) May 26, 2012 09:30 AM
推荐使用显式的销毁 buffer 的接口。 gc 那个只是 5.2 支持顺手加上的。
Posted by: cloud | (30) February 18, 2012 01:29 PM
一个小项目需要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做了相关调整,等有空再去研究。
Posted by: lifc0 | (29) February 18, 2012 09:19 AM
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);
}
Posted by: Anonymous | (28) December 22, 2011 02:28 PM
要源码的同学就是不肯去 github 自己取?难道要人打包好 email 才行么?
Posted by: Cloud | (27) December 22, 2011 10:39 AM
请问能否将你的bind库开源呢?
Posted by: Anonymous | (26) December 22, 2011 09:33 AM
@tearshark
把一小短测试代码, 针对性的优化是没有意义的.
实际不可能这样连着用,因为这样的代码段其实什么事情都没有做.
对于任何语言的任何代码片断, 都是以最舒适和直观的写出来为最常规的用法.
对于一个通用库来说尤其如此,因为它是给许多不同的人在不同的场合用的.
就这段代码而言. 一个常规的用法 :
比如在数据编码阶段是, 准备一个输出流, 准备一个待序列化的结构, 装填结构的数据, 序列化到流, 流输出.
这些步骤在实际用的时候是在不同流程,不同时机去做的, 甚至不是一个人来维护, 在同一模块里出现.
测试代码应体现这些流程和步骤, 而不是想办法放在一起,再看看哪里可以优化, 这样得到的优化结果没有太多意义.
Posted by: Cloud | (25) December 18, 2011 02:08 AM
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.
Posted by: tearshark | (24) December 17, 2011 04:05 PM
路过学习。
Posted by: N | (23) December 16, 2011 03:30 PM
万恶的stringstream。这个东西极其低效,用它做格式化不论在哪个平台上都比printf低一个数量级。当年有个项目被它打败过。
Posted by: 李扬 | (22) December 16, 2011 02:45 PM
小弟愚见: 因为protobuffer编码的时候默认会加tag,而解码的时候根据tag来确定数据类型进行解码,它这样做为了灵活性而牺牲性能,并不是每个场景都需要。
Posted by: kaka11 | (21) December 16, 2011 10:44 AM
现实情况中stringstream对象可复用。定义放循环外,用前或用后调output.str("")、output.clear()清空并复位,在我机器上运行时间从4.5s缩短到2.5s。
Posted by: lifc0 | (20) December 15, 2011 10:14 PM
所有不信的都可以自己调整代码,然后试试, 代码都在的.
ps, 我也写过 10 来年 C++ 代码. 纸屑了解这些表层的开销.
我是故意这样写的.
Posted by: Cloud | (19) December 15, 2011 06:09 PM
c++效率不高的原因就是你用了太多的临时对象,而这个在lua中可能是有一个缓存来处理的。不信你可以把那些个临时对象放外面,重复使用,这样效率估计会比lua高很多。
Posted by: walle | (18) December 15, 2011 06:01 PM
lua 不是一个适合抠性能的语言, 适当注意就好了. 那些机械性的优化策略应该交给 jit 去处理.
关注每条指令背后的机器码,是在 C 层面做的.
Posted by: Cloud | (17) December 15, 2011 02:30 PM
@Cloud
又分析了一下,发现不是luajit有对table查找做了优化,而是jit有一定的优化能力.
而用luajit的解释器就能发现local化的明显更快,列出字节码也能看出确实指令少了.
Posted by: dwing | (16) December 15, 2011 02:09 PM
@Cloud
反效果应该只限于ffi中的函数, 但貌似第一段程序中的protobuf不是ffi.load返回.
非ffi的函数我刚测试过,性能几乎没有差别,可能luajit有对table查找做了优化.
Posted by: dwing | (15) December 15, 2011 01:49 PM
要是这样说,的确很慢。
我自己写的序列化引擎,你这2个字段的序列化和反序列化100万次加起来的事件也少于1s.
当然,这一定情况上取决于机器的性能,我的机器是家用PC双核的。
也不知道为啥官方的PB会那么慢,我还是使用的.NET语言呢~
Posted by: Kevin.Kline | (14) December 15, 2011 01:15 PM
@dwing
你说的这个在 luajit 上有反效果.
Posted by: Cloud | (13) December 15, 2011 01:04 PM
其实lua代码有很多极端的优化手段, 比如尽量local化.
第一段lua代码里, 应该把encode和decode local化, 每个循环省了2个table查找,
实际应用中即使不能在函数内local化, 也要在upvalue上local化, 也比查table快.
luajit的实现有内置的快速内存分配器, 不过我没详细看是否对常用的对象做对象池.
官方lua有接口使用自己的分配器. 对象池的实现涉及到很多细节问题, 官方不提供也很正常.
Posted by: dwing | (12) December 15, 2011 12:59 PM
测了一下官方的Python版,35秒,,,泪流满面。
Posted by: L' | (11) December 15, 2011 10:38 AM
官方的实现不符合STL惯例。符合STL惯例的做法是提供一个模板函数序列化到一个iterator. 而官方的实现搞了个什么ZeroCopyOutputStream就是抄袭Java IO那一套,用虚函数桥接各种输出目标。
而且std::ostream其实是用来做文本格式化的。谷歌实现提供输出到ostream其实是错误的。如果要输出到二进制流,而又不想用模板函数输出到iterator,就应该使用streambuf,会减少一些间接层。
谷歌实现还提供输出到std::string,这也是错的。std::string是给文本用的。许多std::string实现都为字符串专门优化过,比如方便的添加'\0',某些实现甚至还实现了Copy-On-Write。这些优化都增加了额外的间接层,在不需要这些功能时有损性能。输出二进制应该输出到std::vector,更轻量级一些。
Posted by: Atry | (10) December 15, 2011 01:13 AM
@wenbo
定义在循环体外是不符合实际应用环境的.
这里模拟 1M 个包, 显然不可能是用的同一个对象.
要测试的流程包括了构建和销毁包和流本身.在对等的 lua 版本中也做了这些操作.
Posted by: Cloud | (9) December 15, 2011 12:38 AM
试了一下, 把person, person2, output定义在循环体外可以省去差不多一秒
Posted by: wenbo | (8) December 15, 2011 12:34 AM
@kai
显然 lua 不是你想像的那样工作的, 即使你重新实现也做不到那样.
Posted by: Cloud | (7) December 15, 2011 12:16 AM
@Cloud
我对lua的GC不熟。如果是我来实现lua的vm,我肯定会针对表的大小做fixed memory pool。另外对表里面的成员的大小也会有fixed memory pool,我觉得这是lua可以做的很基本的优化,因为lua元素的组成大小比较固定,对内存上没有连续的要求。如果这个假设成立,lua表的连续创建,可以不涉及到堆操作。
Posted by: kai | (6) December 15, 2011 12:12 AM
@kai
刚才我调整了 lua 代码,把 table 放在循环内部, 这样需要临时构造百万次 table ,且会触发 gc.
即使这样, luajit 加速后,依然比 c++ 版本快.
Posted by: Cloud | (5) December 15, 2011 12:03 AM
猜测lua快是GC的功劳。C++如果使用fixed memory pool也许会有明显提升。不熟悉系统默认的堆的操作,不知道如果连续申请释放同样大小的内存,使用的是同一块内存还是不同的。
Posted by: kai | (4) December 15, 2011 12:00 AM
这是模拟现实应用:
现实应用中, 处理每个包, 就需要做这么多事情.
不过即使这么改了也没有提升太多.
为了和 lua 对应且公平, 我调整了 lua 代码重新测试了.
Posted by: Cloud | (3) December 14, 2011 11:57 PM
把 stringstream output 放到循环外试试。stringstream属于比较重量级的对象,频繁创建、销毁有会耗费不少cpu时间。
Posted by: firesxp | (2) December 14, 2011 11:34 PM
风哥,你c++代码中,在循环里多做了几件事:
1创建了 两个Person
2调用了Person各个属性的get/set方法各一次
要是把这些放循环体外,能快不少吧
我不懂lua,提点拙见
Posted by: Anonymous | (1) December 14, 2011 11:30 PM