给silly增加热更新

最新抽了点时间给silly增加了一个silly.patch模块,用于对热更新提供一些有限的支持。

热更新最麻烦之处莫过于“数据迁移”, 即怎么使新函数(要更新的函数)以“运行时数据”的状态运行。

其实http这类无状态协议是最为简单的,因为他们不需要“数据迁移”的过程。http的这种架构,使得所有的函数都是无副作用的,所有的数据在请求结束给出Response的同时, 数据就已经存入了数据库。当需要热更新函数时,根本就不需要考虑数据的问题,直接替换就可以完美解决。

与此相对的是,通常的服务器或应用程序都会有“非局部变量”的存在(所有生命周期不是在函数被call时建立,ret时销毁的变量都可以认为是非局部变量,比如lua中的全局变量或上值)。

对于这类程序,在热更新时,就必须要小心处理这类数据,使热更新之后的函数安全的以“运行时数据”所代表的状态运行。当涉及数据结构或功能变化较大时,这种“数据迁移”的安全性很难面面俱道,也很难提出一个通用的解决方案。

再考虑一下解bug的场景,大多数情况下,bug可能只会出现在某几行代码或某几个函数之中,一般会延续之前的设计而不太会有大量数据结构或大量代码的改变。

在这种情况下,热更新实现的复杂度就可以降低不止一个数量级。在实现上也可以有更好的保证。

因此,这次新增的silly.patch模块也仅仅对热更新bugfix做了一些支持。


silly.patch模块只提供一个功能,即将a函数中的所有“非局部变量” patch 到b函数中去,以便b函数以a当前的运行时状态继续运行。

借助luaVM提供了一组调试函数,使得我们可以方便的对a,b两个函数进行“数据迁移”。

比如,使用debug.getinfo来遍历出a,b两个函数的所有上值,然后使用debug.getuservalue和debug.upvaluejoin将b函数的所有上值均引用至a函数的上值。这样就可以使逻辑以b函数的代码以a函数的数据去运行。

但是这里面有些问题需要处理.

a,b两个函数必然不会相同(不然也不会去更新了),那也就不能保证a函数的第一个上值意义与b函数的第一个上值意义完全相同。比如下面代码:

local foo = "hello"
local bar = "world"
local function f1()
	print(foo)
	print(bar)
end
local function f2()
	print(bar)
	print(foo)
end

如果a函数是f1, b函数是f2。f1的第二个上值是foo(因为使用了print,所以第一个上值是_ENV),而f2的第二个上值是bar。如果按上值的id去patch,上述代码就会出现很诡异的bug。

因此silly.patch模块做了一个简单的约定,如果要拿b函数去修复a函数,就必须保证a函数中使用的所有上值在b函数中必须不得改变其意义。

有了这个约定,silly.patch就可以根据“非局部变量”的名字去进行“数据迁移”。 比如f1使用了上值foo, f2中只要foo的意义不变,不管他属于f2的第几个上值,都可以保证“数据迁移”的正确性。

还需要注意的是,如果a和b函数的上值变量是函数时,需要递归对其上值函数进行“数据迁移”。


silly.patch仅仅是对热更新做了支持,他并不是一个完整的热更新模块。还需要对silly.patch进行一定的封装才可以使用。

在进行封装时一般的步骤为,生成新函数b,找到等修复函数a, 执行silly.patch, 将a的数据迁移到b函数上, 然后使用b函数替换为a函数。

生成新函数b,一般是通过load/loadfile来生成一个chunk, 然后调用chunk来生成.

为了避免在调用chunk函数时有副作用,一般在调用load/loadfile时,会传入一个新的_ENV表作来将chunk置于沙盒之中,如果有顾虑新的函数b会使用新的全局变量(即函数a从没使用过的全局变量),可以在整个热更新的最后, 将load/loadfile时传入的_ENV表有选择性的合并运行时环境中(只合并运行时环境不存在的变量)。

比较麻烦的是怎么找到要修复的函数并进行替换,lua中提供的debug接口中并不能获取一个chunk中的所有函数。

当然就算提供了这样一个接口也很难使用。在lua中,function是first class, 这就意味着当你定义两个变量指向同一个函数时,这个函数就拥有了两个名字。

因此在我们约定不给可能会热更新的函数起别名的情况下,有两种实现方式。

一种不太通用的实现是,在每个chunk中实现两个函数,一个函数提供通过名字对chunk中的任意函数进行定位,一个函数提供通过名字对chunk中任意函数进行替换。这种方法比较麻烦,而且容易出错。

另一种方式依赖于一个事实,一般每一个lua模块都会导出一些接口函数供其他函数使用,那么就从这些接口上做文章。

比如我们想要将module模块中a函数热更新为b函数,我们可以直接require “module”得到module模块的接口函数表,然后再根据名字定位到相应的函数,如果替换则直接将module模块函数表中的相应字段重新赋值就可以了。

如果想要热更module中某个local函数,就比较很麻烦,但是也可以办到。在上文silly.patch的实现中可以得知,如果一个函数有上值,而且上值是一个函数的情况下,silly.patch同样会对上值函数进行“数据迁移”,这也意味着,上值函数同时也会被热更新到最新。所以,在需要热更新某个local函数时,可以通过热更调用他的模块接口函数来实现。

但是需要注意的是,这里有一个坑。如果函数module.a1和函数module.a2同时引用了module中的一个名为foo的局部函数。如果只热更module.a1的话,module.a2将依然会使用旧的foo函数。

基于通用考虑,在silly的console模块提供了一个patch命令, 这个命令正是基于方式2来实现的。

为什么要有头文件

我在写C文件时,一般会首先确定这个模块需要哪些功能,然后在头文件中定义相应的接口函数。之后才是在C文件中实现,在实现过程中除非有遗漏的接口,不然是不会再切回头文件的,一般辅助函数我都是直接以static的方式定义在C文件中。

在写C++代码时,这些代码辅助类的函数,都必需要以private的方式在头文件中声明。这会导致在写代码时,需要频繁在h/cpp之间切换,极度令人不舒服。

因此每次在写C++代码时,都免不了在心里抱怨几句为什么不把private函数直接定义在cpp文件中,或者干脆像java一样不要头文件算了。

前两天把这事跟朋友抱怨了一下,结果竟然得到了一个反问为什么需要头文件,头文件的作用到底是什么?

有人说头文件是为了展现接口,以头文件和.a或.so发布时,别人只要看头文件就可以知道提供了什么接口,然而对于java这类没有头文件的语言来讲只要一个工具同样可以提取出来class文件中的public接口信息。因此我觉得还是需要从编译器角度来分析一下头文件的用途。

那么C语言的头文件到底起到什么作用呢?且看下面一段代码(ps.为了使这段代码在任何平台上效果都一样,使用了stdint.h中的可移植类型):

////////////compile: gcc -o a a.c b.c
////////////b.h
#ifndef _B_H
#define _B_H
#include <stdint.h>
struct test {
        uint8_t a1;
        uint8_t a2;
        uint8_t b1;
        uint8_t b2;
};
#endif
////////////b1.h
#ifndef	_B1_H
#define	_B1_H
#include <stdint.h>
struct test {
        uint16_t a;
        uint16_t b;
};
extern struct test T;
#endif
////////////b.c
#include "b.h"
struct test T = {.a1 = 1, .a2 = 2, .b1 = 3, .b2 = 4};
////////////a.c
#include &lt;stdio.h&gt;
#include "b1.h"
int main()
{
        printf("a:0x%x b:0x%x\n", T.a, T.b);
        return 0;
}

这段代码的运行结果很有意思,是’a:0x201 b:0x403’。

这段代码编译器不会报任何错误,甚至连警告也不会报。为什么会这样呢?这要从几个.c文件变成elf(linux)/exe(win)文件过程说起。

从c文件到可执行文件至少要经过两个阶段,即‘编译’和‘链接’。
‘编译’会将相应的C代码转换成相应的汇编,但保留符号名(如上述代码中的T)然后生成.o文件.
而‘链接’会收集所有的.o文件然后为每个符号分配地址,并将.o文件中的相应的符号换成相关地址并生成相应格式的可执行文件(ps.上面的流程并不严谨).

以a.c中的代码为例,在编译时T.a语句其实就已经转换为了与*((uint16_t*)((uint8_t *)&T+0))等效的汇编代码,相应的T.b的语句等效*((uint16_t*)((uint8_t *)&T+sizeof(uint16_t))).这一点其实可以通过gcc -S来反汇编证明。

那么头文件的功能就呼之欲出了,在‘编译期间’为了保证能生成正确的汇编代码,必须要头文件指明struct的字段分布,及此c文件中引用的符号在外部有提供(这就是声明的意义)。

当然头文件所带来的问题也正像上述代码中描述的一样,当给出一个错误的头文件时编译器并不会察觉,在没有源码的情况下,这种错误极难发现。

那么java是如何实现的呢?我尝试着写了两个类编译了一下。猜测,他应该是在编译时自动提取出本类的声明信息,然后放在.class文件中,当javac编译时用到某个类时就去找当前目录下打开‘类.class’文件,从其中提出取类的声明信息,从而达到与有头文件有相同的效果。

ps. 我怀疑.class的声明信息与其反射机制有密切关系,但是jvm的代码量有20多万行,找到其反射部分的实现还是比较麻烦的,天气这么冷还是先放一下:)

pps. 如果C语言也这么搞的话,似乎是行不通的,java中的类的概念,可以约定使用哪个类就从‘类名.class’文件中寻找,如果是C呢,找一个struct test的布局去test.o中找?那找一个函数helloworld应该去哪个文件中找呢?


在与Qwerty交流后发现,查找helloworld函数时可以根据此c文件import的模块来实现,那么似乎为C引用import机制也并不是不太可能。

简单思考了一下,在不改变现有编译流程的情况下,似乎可以为c文件引入一个轻量级import机制来代替头文件。

我们可以在编译器(如:gcc)之上包个壳,假设叫xcc,然后为.o为文件也加上一个壳叫.m。

.m文件其实包含了其源文件中的public的函数接口定义和一个完整的gcc编译出来的.o文件内容。

xcc的执行流程大概如下:

1. 使用xcc编译A.c文件时,xcc首先分析出A.c文件中的声明信息’T’及import的模块’M’。
2. 从M.m文件中提取出M.c文件中的声明信息T,并生成M.h文件,之后将a.c中的import ‘M’替换为#include “M.h”,并另存为到/tmp/A.c.tmp。
3. 调用相应的编译器如’gcc’编译/tmp/A.c.tmp生成A.o
4. 将A.c的声明信息T追加到A.o的最后(之所以追加到最后,是因为A.m可以被当作A.o直接传给ld, 这样我们就不用为ld再包一个壳了)

有一个特例,一个struct是否要导出,必须要等分析完所有的导出接口之后才能决定,如果在导出接口参数中有用到,那么我们就可以将其导出到头文件中。

如此,我们就有了一个兼容import的C语言。

实现了一个lualint

在使用动态语言的过程中,由于其运行时检查特性,很多手误并不能在刚运行时暴露出来。虽然并不会导致程序个程序crash掉,但总归是很麻烦的。

网上找了一个lualint试了试,效果还不错。这个工具的原理就是基于在写lua代码时并不会使用全局变量。在检测到全局变量时直接报警即可。

仔细研究了一下这个lualint的原理。

lua自带的工具luac是可以把编译后的OPCODE打印出来的,更关键的是自带注释。

利用OPCODE和注释就可以分析出在哪一行哪些变量是使用了什么全局变量。只要我们在代码中不使用全局变量这一事实成立,那么所有使用了全局变量的地方都可能是手误打错了。

但对于我来说这个lualint有2个缺陷。

1. 但这这个lualint是并不支持lua5.3版本。因此在使用如’//’这类lua5.3新支持的特性时,这个lualint工具就不能很好的工作了。
2. 如果我require了一个表,并且使用了这个表中不存在的字段,那么他并不会报警,这对我来说很不习惯。因为大部分模块调用都是使用通过使用module.member的方式来调用的,如果member的名字打错(如A.l1打成了A.ll), 这个工具并不会报警出来。

基于以上原因,我基于lua53重新实现了一个lua分析工具修复了上述缺陷。虽然依然叫lualint, 但为了简化实现复杂度,这个lua检查工具在部分地方使用了动态分析。

在lua53中,对全局变量的作法做了些改动。

所有全局变量都是放在_ENV表中存放的,而一个代码块的所有代码是共享一个upvalue的。而访问全局变量是通过类似 ‘GETTABUP 0 0 -1 ; _ENV “print”‘的OPCODE来完成的。因此检测访问全局变量的OPCODE要改为监测使用GETTABUP/SETTABUP 对于_ENV表中的字段进行操作的代码。

考虑如下代码:

local b = require "testb"
--code1
print(b.CONST_A)

function hello()
        --code2
        print(b.CONST_A)
end

对于b.CONAT_A中的访问检测则稍微有些麻烦。

在code1处对模块testb的成员CONST_A的访问OPCODE和在code2处的OPCODE并不相同,因为code1属于main chunk而code2属于function chunk, 变量b与code1是平级的,即相当于code1的局部变量,而对于code2来说b变量为函数hello的upvalue, 因此在main chunk中访问模块testb的成员CONST_A使用的是GETTABLE而在function chunk使用的是GETTABUP. 顺便插一句,lua之所以能够实现的如此简洁,应该与其这种设计的统一性不无关系。

如果通过分析OPCODE来检测模块B是否有CONST_A变量虽然也可以做到,但是会很麻烦。因此这里取了个巧,采用了动态加载的方式。模拟require的行为,然后将模块testb的返回值接收下来并保存,当访问testb的成员时直接查看testb返回的表中是否有这个成员即可。

当然事情不可能这么简单,在函数中访问testb中CONST_A时会生成的OPCODE如’GETTABUP 1 1 -3 ; b “CONST_B”‘。 这里存在两个麻烦:一个是如果b是某个模块的别名,那么怎么确认他是哪个模块的别名,,因为生成require的OPCODE并不会有别名b的信息。一个是怎么找到这个b到底是模块testb的别名还是此函数的另外一个上值。

对于第一个问题,其实在分析require的过程中,我会重新再解析一遍当前代码文本,require中的那一行找到相关模块’testb’的别名即为b,做一个反向映射,如果GETTABUP指令访问b就把b当做别名找出真正的模块名’testb’,从而找出模块’testb’的表引用。由于需要分析require,因此顺带着就把整个project的依赖关系给解析了,在使用时也就不需要使用如find . -name ‘*.lua’ | xargs grep ./lualint 之类的命令来使用了。设置完ENTRY之后,直接使用./lua lualint.lua就可以自动分析完整个project是否警告信息。

对于第二个问题出于对实现的简化,采用了鸵鸟政策,碰到此类情况均当做是对模块’xxx’的变量的访问。当然做了一点小小的优化,如果从别名找不到真正的模块名,就认为访问的不是模块的成员变量,直接忽略。

ps.由于急用,实现的有些丑陋,因此附一篇文章大致说一下思路:D

使用lua过程中需要注意事项

最近一段时间写了不少lua代码,由于这一次的代码量比以往都多,因此也就遇到了以前没有遇到问题, 需要说明的是,这就是一篇lua问题大集合, 并不涉及其他。

先说说遇到的问题及解决方案。

在lua中没有多余的数据结构,全靠一张表,这也是他简洁的根本所在。

当索引为全为数字时即可当数组使用,然而需要注意的是lua假设数组的索引一定是连续的,在此基础上lua尽可能快的优化了取数据长度的算法,因此lua对‘#’操作符的描述是“当table[i]不为nil, 而table[i+1]为nil时,则数组的长度为i”.

因此如果table = {1, nil, 2, 3}, 那么使用#table得到的结果会是1.因为table[1]不为nil, 而table[1+1]为nil。其实这一点在”Programming in Lua”是有明确说明的,只不过在写代码时我忘记了。

如果想删除数组中的元素而又保证#table的正确性,可以使用table.remove函数来删除指定位置的值,table.remove所做的事情其实就是把所有元素向前移动一位,因此在使用table.remove时一定要估算好移动元素带来的开销是否超出了预期。如果超出了预期就要考虑其他方案,而其他方案也与‘#’引起的问题没有关系了。

‘#’的语义带来的麻烦还不仅仅于此。考虑下面一段代码:

        function test1(...)
                local param = {...}
                local p1, p2, p3, p4 = table.unpack(param)
        end

当调用test(1, nil, 2, 3)之后,得到的结果是p1 = 1, p2 = nil, p3 = nil, p4 = nil。

之所以这样是因为table.unpack(param) 等价于table.unpack(param, 1, #param), 而’#’的语义导致了#param得到的结果是1,所以table.unpack其实只unpack出了第一个值而已。

即然存在此类问题,显然也存在着解决方案。lua的table库中提供了一个函数table.pack。table.pack(…)与{…}惟一不一样的地方就是,table.pack会自动识别…中有几个参数,并把这个值赋给返回的表中的”n”字段。

所以上述代码要想支持传递nil就必须改成如下:

        function test1(...)
                local param = table.pack(...)
                local p1, p2, p3, p4 = table.unpack(param, 1, param.n)
        end

再说说优化问题。

3R在很多文章上都有提到,就不再多说什么了,但是对于Reuse需要注意的是,并不是所有的值都值得Reuse,需要考虑这个表的Reuse的代价有多大。

考虑一个表从产生到销毁的过程(不包含对元表的__gc函数的调用过程)。

local xx = {} 就代表着要构造一个新表,这就代表要调用malloc, 当这个表不再被使用时,就会被gc回收free掉。

如果我们可能重用xx表,那么就可以省去malloc/free的开销,要知道malloc的开销是相当大的。

但是这里需要注意的一个问题是,xx已经有了预分配槽位,假设之前xx表中被设置过xx[1]~xx[100]个数据,那么就意味着重用xx表后,向xx[1]~xx[100]的位置赋值是是不会触发malloc进行扩容的。

之所以说这是一个问题而不是一个优点,是因为在不同的情况下,这种重用有时有利有时有害。这取决于被重用的xx表的用途,如果xx几乎是固定大小,那么显然这种不需要扩容就是有利的,反之如果xx大部分情况下都很小,就会浪费很多空间。

当重用的表是一个模块的upvalue时,还需要考虑的是当你为这个表的每一个槽位都赋为nil的代价与构造一个表的代价哪个更高,如果无法评估则应该选取语言更简单的模式“直接构造一个表”。

next函数常用来进行无序遍历整张表,同样你必须知道他的底层实现,才能够很好的使用它。从lua源码中可以找到next函数具体实现,当传递给next的key为nil时,事实上next函数会从table的第一个空槽位开始遍历,直接找到第一个值为非nil的key并返回。因此在使用next进行遍历时,必须对整个table中的布局做到心中有数。

考虑一种极端情况,有一个table有1M个数组空间,只有在索引接近1M时,值才不为nil,其他值都为nil,如果这时候需要频繁的调用next来遍历整个表,势必会造成程序性能急剧下降。这种情况下就需要在表的基础上构造出相应的数据结构来提高程序性能,比如记录第一个非nil的起始索引。

在lua中有很多基本库和函数是存在于_ENV表中,比如table库,math库等,如果调用这些函数很频繁,可以考虑将函数局部化,能显著提高程序性能。

下面对比一下两段代码,和编译后的OPCODE

--使用全局函数
local tbl = {}
for i = 1, 100 do
        table.insert(tbl, i)
end
--OPCODE
main <a.lua:0,0> (12 instructions at 0x7fc748c043f0)
0+ params, 8 slots, 1 upvalue, 5 locals, 4 constants, 0 functions
	1	[1]	NEWTABLE 	0 0 0
	2	[3]	LOADK    	1 -1	; 1
	3	[3]	LOADK    	2 -2	; 100
	4	[3]	LOADK    	3 -1	; 1
        --for i = 1, 100 do
	5	[3]	FORPREP  	1 5	; to 11
	6	[4]	GETTABUP 	5 0 -3	; _ENV "table"
	7	[4]	GETTABLE 	5 5 -4	; "insert"
	8	[4]	MOVE     	6 0
	9	[4]	MOVE     	7 4
	10	[4]	CALL     	5 3 1
	11	[3]	FORLOOP  	1 -6	; to 6
        -- end
	12	[5]	RETURN   	0 1

--全局函数局部化

local tinsert = table.insert
local tbl = {}
for i = 1, 100 do
        tinsert(tbl, i)
end
--OPCODE
main <b.lua:0,0> (13 instructions at 0x7fb5a94043f0)
0+ params, 9 slots, 1 upvalue, 6 locals, 4 constants, 0 functions
	1	[1]	GETTABUP 	0 0 -1	; _ENV "table"
	2	[1]	GETTABLE 	0 0 -2	; "insert"
	3	[2]	NEWTABLE 	1 0 0
	4	[3]	LOADK    	2 -3	; 1
	5	[3]	LOADK    	3 -4	; 100
	6	[3]	LOADK    	4 -3	; 1
        -- for i = 1, 100 do
	7	[3]	FORPREP  	2 4	; to 12
	8	[4]	MOVE     	6 0
	9	[4]	MOVE     	7 1
	10	[4]	MOVE     	8 5
	11	[4]	CALL     	6 3 1
	12	[3]	FORLOOP  	2 -5	; to 8
        -- end
	13	[5]	RETURN   	0 1

对比可以看出使用局部变量tinsert代替table.insert之后在for循环内少了两条对table的操作(GETTABUP, GETTABLE) 效率大概提高了10%

当语言内置操作和标准库一样时,应该优先选择语言内置操作。其实这很好理解语言内置操作执行一起一般比调用标准库要生成更少的OPCODE,在功能一样的情况下一般OPCODE数目越少,应该就越高效才对。
在循环100W次的情况下table[#table+1]=i要比table.insert(table, i)快30%左右。

btw, 在看lvm.c的过程中,越来越觉得脚本语言是比C/C++更高层次的抽象,不过也只有这样才能达到简少代码的目的。然而高层次抽象就意味着我们失去了很多优化的能力。

C++默认构造函数

在C++中,如果不为某个struct/class实现一个构造函数,那么编译器就会自动为这个类添加一个默认构造函数,而这个默认构造函数什么也不干。

但是我却从来不知道,默认构造函数在不同的情况下,会出现不一样的效果(当然这是C++03之后的标准).

先看一段代码:

struct test {
int a;
int b;
};


void *operator new(size_t sz)
{
void *p = malloc(sz);
for (size_t i = 0; i < sz; i++)
((char *)p)[i] = 0x01;
return p;
}
int main()
{
struct test *t1 = new test;
struct test *t2 = new test();
printf("t1:%x-%x\n", t1->a, t1->b);
printf("t2:%x-%x\n", t2->a, t2->b);
return 0;
}

重载new操作符是为了把分配出来的内存弄脏。然后观察new test和new test()的区别。

结果出人意料,t1的值就是内存中被污染的值,然后t2的值却全部被清0,而造成这种现象的惟一的区别就是new之后类型是否带有括号。

这就是C++03版本的新增内容,当没有实现构造函数时,编译器为你自动生成的构造函数是有两种用途的。当你在使用new T()构造对象时,默认的构造函数会对各变量执行清0操作,其实应该就是memset为0. 而在使用new T来构造对象时,其默认构造函数是任何事也不做的。

总觉得这么做违背了语言的一致性的设计, 但是不管怎么说有了这个,在写struct定义时在一定的情况下就可以省掉默认构造函数了。

迭代器模式

在写C++代码时,首先接触的就是迭代器。甚至于设计模式都有一种模式叫迭代器模式。虽说网上到处都说迭代器用于隐藏数据结构的细节,但我却一直没有真正搞明白为什么需要迭代器去隐藏数据结构细节。

在写C++代码时,一般我每用一个数据结构都会去查一下,他大致是如何实现的(不然用起来不太放心:D)。

因此一般情况下我在c++下都是使用类似类似for(size_t i = 0; i < vector.size(); i++)的方式去遍历vector的每个元素。

直到最近的一次重构我才大概明白什么时候去使用迭代器模式。


首先大致说一下zproto的作用。

zproto是一个序列化/反序列化的工具,类似google protobuf,但是实现更简单,更轻量级。

在实现之初,我是希望zproto.c可以实现syntax和serialize/unserialize的核心功能,然后再为每种不同的语言写一组操作本语言数据结构的函数,即可通过zproto.c来实现bind功能。但我又不想做成callback接口。

由于zproto所有字段都是可选的,因此某一字段可能是不存在的,在encode和decode时,zproto.c必须知道上一次有效的字段是哪一个。

因此接口就变成了这个样子:

void zproto_encode_tag(struct zproto_buffer *zb, struct zproto_field *last,
struct zproto_field *field, int32_t count);
void zproto_encode(struct zproto_buffer *zb, struct zproto_field *last,
struct zproto_field *field, const char *data, int32_t sz);
struct zproto_field *zproto_decode_tag(struct zproto_buffer *zb, struct zproto_field *last,
struct zproto_record *proto, int32_t *sz);

从接口就可以看出,在写bind代码时,每次都需要维护上一个有效有字段,这其实从一定程度上暴漏了zproto.c作为core的实现细节而且加重了写bind代码的负担,这一度让我觉得很恶心。

几次想把last放入zproto_buffer字段中,但这样就需要在zproto_buffer中维护一个栈的结构,因为record(结构体)中有field(字段),field又可能是record类型。 这样一来就增加了实现复杂度,与我的初衷不符。因此也就迟迟没有动手。

直到最近在一次写C++的代码时,再一次用到迭代器(遍历map中的所值)时,突然觉得这个问题可以用迭代器来解决。

使用迭代器实现后的接口如下:

void zproto_encode_array(struct zproto_buffer *zb, struct zproto_field_iter *iter,
int32_t count);
void zproto_encode(struct zproto_buffer *zb, struct zproto_field_iter *iter,
const char *data, int32_t sz);
int zproto_decode_field(struct zproto_buffer *zb, struct zproto_record *proto,
struct zproto_field_iter *iter, int32_t *sz);
int zproto_decode(struct zproto_buffer *zb, struct zproto_field_iter *iter,
uint8_t **data, int32_t *sz);

明显可以看出,接口和内聚性都提高了很多。在写bind代码时再也不需要维护上一次有效的字段了,因为在encode时已经被记入iter了。

当然代价肯定了也是有的,就是多实现了一组迭代器的函数,不过我认为这代代价是值得的。

由于zproto已经完全屏蔽了细节,因此在写bind代码时可以只关心本语言的数据结构的存取,而不会由于不懂zproto的实现细节导致传入错误的last_field造成zproto.c工作异常,大大降低了心理的包袱。


可以结总出迭代器一般用于提供给其他模块遍历时才使用(其实从标准库就可以看出,只是我自己没领会到^_^!),迭代器数据结构也不一定只存储用于遍历的数据,只要方便达到目的也可以存一些冗余数据,比如zproto.c中的迭代器会存储上一次有效的字段。

现在再反思为什么以前写代码都不会用到迭代器模式其实都很清楚了。

在写C语言时,模块一般都是提供某种属性或某种行为的而不会提供遍历的特性,这种情况下也就没有必要去为此模块提供一个迭代器。

而在模块内部的数据结构, 其实也没有必要去实现一个迭代器,有时候直接操作成员去访问可以更直观更高效。

同样在C++开发时也并非说,实现了一个容器类数据结构就需要去为他实现一个迭代法器,如果此数据结构仅仅为优化特定模块而生。那么直接去访问有时后反而代码更少更简洁,也并不会造成什么坏的影响。

还是那句话,没有银弹,什么时候合适什么时候不合适还是要靠自己判断。

模板的高级用法

一直以来都是通过C用基于对象的设计方法来写代码。即使工作中使用C++, 也是尽可能少的使用超出C的一些特性。当然这并不是C++不好,而是C++实在太复杂了。以我的脑力来讲, 如果使用C++过多的特性, 很容易让我过于陷入语法特性之中, 而忽略了设计。因此, 对于C++的一些高级特性, 如模板等并没有深入研究过。

模板对我来讲, 仅限于知道可以实现泛型。至于怎么巧妙的利用泛型来实现其他特性, 从来没有深入研究过。最近工作中,碰到了一些看起来比较高端的模板用法,令人有一种耳目一新的感觉,因此就记录一下。


如果实现一个单纯的组播模块, 该模块提供的组播接口可能类似:

void multicast(func, arg1, arg2, arg3, ...);

在不用模板的情况下,假设每种参数有可能有m种类型,如果参数的个数为n个。 那么针对参数为n个的函数就需要手写m^n个重载板本。

使用模板推导功能,对于参数个数为n个的函数,就仅仅只需要实现一个实现即可。

示意代码大概如下:

template
int call(void (*func)(T1 p1, T2 p2), T1 p1, T2 p2)
{
func(p1, p2);
}
void func1(int a, float b)
{
printf("%d, %f\n", a, b);
}
void func2(double a, const char *b)
{
printf("%lf, %s\n", a, b);
}
int main()
{
call(func1, 3, 3.5f);
call(func2, 5.3, "hello");
return 0;
}

这样使用模板忽略掉了参数类型,仅仅针对参数个数进行重载, 会大大提高代码的编写效率。


对于C++来讲,其实RAII应该算是惯用手段了。

在不使用模板的情况下,如果我们使用RAII来管理指针,我们就需要为每一个类实现一个指针类。而如果使用模板,仅仅实现一个指针类就可以了。

比如:

template
class PTR {
public:
PTR(T a) {
ptr = a;
};
~PTR() {
if (ptr == NULL)
return;
delete ptr;
};

T get() {
return ptr;
};
private:
T ptr;
};

class test {
public:
test() {printf("test\n");}
~test() {printf("~test\n");}
void hello() {printf("%s\n", h);}
void set(const char *n) {h = n;}
private:
const char *h;
};

int main()
{
PTR p(new test);
test *t = p.get();
t->set("hello");
t->hello();

return 0;
}

其实上面的代码大致就是C++11提供的std::unique_ptr实现的功能了.


在网络通信过程中,定义数据结构总是一件很烦的事,使用纯C的数据结构便于传输,但是会将结构定义的很死, 而且数据种类有限。

如果使用vector/unordered_map并且相互嵌套时传输就会很麻烦。一般这时候就不得不采用protobuf的方式来进行传输。

但是有时候你要传输的数据结构仅仅就是vector/unordered_map等动态数据结构的一些嵌套, 这时候去使用protobuf就稍嫌重量了。

这时其实可以结合C++的模板推导及参数重载来实现一个简易的数据序列化库。

大概试了一下vector,用起来还是很方便的。

template void
serial(const T &a, std::string &res)
{
res.append((char *)&a, sizeof(a));
}

template size_t
unserial(T &a, const char *p)
{
a = *(T *)p;
return sizeof(T);
}

template void
serial(const std::vector &a, std::string &res)
{
size_t n = a.size();
res.append((char *)&n, sizeof(n));
for (size_t i = 0; i < n; i++) serial(a[i], res); } template size_t
unserial(std::vector &a, const char *p)
{
size_t pos;
size_t n = *(size_t *)p;
a.reserve(n);
pos = sizeof(size_t);
for (size_t i = 0; i < n; i++) { T tmp; pos += unserial(tmp, &p[pos]); a.push_back(tmp); } return pos; } int main() { std::vector src1 = {1, 2, 3, 4, 99, 100};
std::vector src2;
std::string dst;

serial(src1, dst);
unserial(src2, &dst[0]);

for (size_t i = 0; i < src2.size(); i++) printf("%d ", src2[i]); return 0; }


从上面三个模板的例子上看,基本上模板就是强类型语言用来在写代码时弱化类型的一个折中。如果有过动态语言编写经验就会明显感觉到,模板明显是为了有限的支持动态语言在编写时的一些优点。如果能够把握这一点,也许才能够更合理,也更巧妙的去运用模板。

c语言部分的开销测试

最近在写c代码时底气越来越弱,原因在于某些调用的开销,我心里并不是十分明确。写起代码来由于纠结也就变的畏畏缩缩。

今天终于抽时间测了一下,仅测试了最近常遇到的一些调用的开销。

测试环境如下:

CentOS release 6.7(Final)

CPU:Intel(R)Xeon(R)CPU E3-1230 V2 @ 3.30GHz

采用gcc(glibc)编译,未开任何优化。
在测试时,大部分操作cache均会命中,因此如果cache经常命中失效,还需要另外考虑。

测试结果如下:

可以看出for循环是最廉价的。

malloc是开销最大的,这还是在单线程的情况下,如果再多线程由于锁的开销malloc的代价将会更甚。

在栈上分配1M次内存的代价几乎等同于for了1M次,在栈上分配内存开销最低。

而copy了64M内存也才花费了5ms而已。

lua gc的使用

主流的垃圾回收一般都是基于引用计数和标记清除算法.

从内存占用量上来讲, 引用计数无疑是有优势的, 当引用计数为0时, 直接就会将相应的对象清除, 典型的应用就是C++的智能指针. 但是基于引用计数的gc有一个坏处, 它无法解决循环引用问题. 如果A引用B会导致B的引用计数+1, B引用A也会导致A的引用计数+1, 这样A,B对象永远也不会删除. 记得在OC中似乎使用了弱引用来解决这个问题, 但总感觉这样会给代码中埋下坑.

标记清楚算法可以完美的解决循环引用问题, 其做法是从root遍历所有可达的指针, 然后将不可达的区域回收. 这样即使A和B循环引用, 只是没有其他对象来引用A和B, 那么A和B对象即是不可达. 此时A和B对象即可删除. 但是标记清除算法有一个致命缺陷, 就是在gc时需要停顿整个程序来进行mark-sweep.

最近抽了点时间研究了下go语言, 果然可能是由于太年轻, go也有这个坑. 在搜索资料时发现很多人都在吐槽go的gc部分, go的主要应用领域就是服务器部分,然而据说go的gc会造成不可预知的停顿,这对于服务器来讲是非常致命的.

看到这, 我突然非常担心lua也有此问题, 而在silly中也是以lua来写上层逻辑的, 很担心lua中的gc是否也会造成stop-the-world. 于是找来手册重新认真的读了一下, 发现以前对于lua的gc的工作方式太想当然了, 以致于造成了对lua的错误使用.

lua中的gc算法是采用增量标记清除算法, 通过一次执行一小步来将一次完整的gc时间分摊在每一步上, 提高了整个程序的实时性.(具体如何做的,还要等看完源码才知道:D)

在lua中是分为手动gc和自动gc的.

通过设置垃圾收集器间歇率和垃圾收集器步进倍率来控制自动gc的运行方式.

有某些特殊的条件下, 如果我们需要手动控制gc的时机, 这时就可以LUA_GCSTOP来停止自动垃圾回收器. 然后在恰当的时机手动调用LUA_GCSTEP来发起一次增量垃圾回收.

这样看来, 至少在gc的策略上, lua应该做的比go要好. 当然具体情况还是要等看完实现才能更确切的比较.

btw, 作为一门现代语言, gc已经是一个不可缺少的部件. 然而gc的问题在于, 在平时开发时完全体会不到问题, 一旦在大量数据面前就有可能会出现各种情况. 因此熟悉一门语言绝不仅仅是熟悉其语法就好了, 还必须要深刻了解该语言的Gc机制, 才能做到心中有数.

lua的debug_hook接口应用

lua本身并没有提供调试器, 但是提供了一组称为debug_hook的API, 这组API可以用来编写自己的调试器或者一些其他的东西。

这两天的工作主要就是研究了一下debug_hook这组接口, 写了一个极简化的lua调试器和一个lua性能分析器.


lua调试器现在仅支持break point, step, print, coutinue这几个功能。

由于最近写lua已经习惯于不用调试器了, 因为实现的比较简单。

下断点时, 将要断点所在的文件与行号存入表中。

在不考虑效率的前提下, 直接用line mode来hook中每一行的运行,然后在每个line_hook函数中去check当前文件名和行号是否与断点信息中进行匹配, 如果匹配则进入调试模式。

在调试模式下, 可以step/print/continue来控制程序做单步/打印变量/继续运行等操作。

在打印变量时, 首先搜寻局变量是否存在此变量, 如果没有则搜寻上值中是否存在些变量, 如果都没有则提示没有此变量。

然而,作为一个能用的调试器来说, 这个调试器还缺的太多。如:打印表中内容, 修改变量, 支持call stack的切换, 非法断点提示, 运行到指定行, 打印堆栈回溯信息等。

由于平时写代码不太依赖调试器,所以暂时不打算继续完善。这里先把思路记述一下,有需要再继续完善。

在简化版本实现中,仅支持打印如integer/string 这样的非table数据, 如果有table数据的话则只打印一个指针。

可以增加对变量类型的判断,如果是table则递归打印表中内容, 这里可能存在的一个问题是直接去访问一个表会触发__index元方法, 因此一定要根据具体情况考虑是否使用rawget。

修改变量与打印变量类似,修改可能会触发元方法__newindex,因此要具体考虑是否使用rawset来修改。

将一个debug模式下手动输入的表赋于某个变量时, 为了更方便输入, 可以支持将表以字符串的形式输入,然后内部使用load来执行这段字符串,然后将产生的表赋于某个变量。

在call stack切换时使用lua_getstack切换到相应的call stack中,然后调用lua_getinfo就可以得到当前的stack中相应的环境。只是这里仅能支持print变量功能。

如果要增加非法断点提示就比较麻烦了,初步的想法是,为每一个文件维护一个可断行号表, 在对某个文件第一次下断点时,过滤掉这个文件的空白行和注释行, 然后把有效行置入可断行号表,再去查询要下的断点是否是有效行。如果不是有效行, 则可以根据情况进行提示, 或向上/向下偏移断点行号。

运行到指定行则比较简单, 下一个临时断点,到达指定行后删除此断点即可。

打印堆栈回溯信息直接使用luaL_traceback函数即可。

最后就是关于调试器效率的优化了。

直接设置line hook效率比较低,可以首先设置为call hook,每次call hook时判断当前函数是否有被下过断点。如果有断点则设置为line hook。每次进入line hook时除了要判断是否命中断点外,还需要判断当前函数中的断点是否已经全部步过,如果所有断点全部步过,将hook设置为call hook。

由于lua有tailcall支持,因此上述优化在某些情况下并不一定能如想象中的那么顺利, 只有等到实际优化时碰到问题再来解决。


相比极其简单的lua调试器来讲,lua性能分析器我花了将近一天的时间来写。

在linux下有gprof可以在很方便的测度整个程序的性能热点, 在windows下VS自带的性能分析器也非常强大。

但是在写lua想测试性能时,要测哪一个函数的时间开销就得去哪一个函数定义处加时间测量代码。

我希望实现一个非侵入式lua profiler,就像gprof一样。

在《Programming in lua》一书中讲解debug_hook时,演示了一下怎么使用debug_hook(call/ret)来测试函数的调用次数,这给了我很大的启发。

我希望这个profiler可以即统计调用次数,又统计时间开销,。

由于lua自带的debug lua接口时间开销很大,想要精确统计出函数的运行时间就必须用debug_hook的C接口来实现。

大概实现如下:
每一个函数单独维护一张表, 表中存有进入此函数的当前cpu时间, 此函数上次ret时已经运行了多少时间,当函数已经被调用了多少次。

设置call hook和ret hook, 在call hook时更新函数的调用次数及进入此函数的cpu时间,在ret hook时根据当前cpu时间和进入此函数的cpu时间即可更新此函数已经运行的总时间。

实现完之后,放到silly中去用即刻就发现问题了。没错是coroutine和闭包这些高级特性。 silly是用大量coroutine和闭包组成的一整套框架,如果不解决coroutine和闭包在profile中的影响,那这个profiler几乎没法使用。

在《Programming in lua》中, 作者使用函数的指针来作为profile数据的索引, 但是在碰到闭包之后, 这种方法就会显得有些麻烦了。

假如有一个函数A可以生成另一个函数, 如果调用A两次分别生成函数B1和B2, 那么B1和B2的函数地址是不同的,在做profile时表中就会有两个此函数的信息, 但是其实两个函数的定义是一模一样的。而且由于使用了函数指针作为索引,为了不影响被分析代码的GC形为,必须使用weak-table来cache住这些闭包的性能数据。但是闭包函数的生命周期一般较短(这取决于代码作者),因此有可能来不及打印此函数的性能数据,此项数据就被GC干掉了。

为了解决这个问题,我不再使用函数指针作为profile数据的索引,而使用“文件名:行号”来做为函数性能分析数据的索引,这样不管有多少个相同函数定义的闭包,其执行时间都是算到同一个函数定义的性能数据上。

coroutine可以使用yield/resume来让出/恢复运行,但是显然从让出到运行这断时间并不能算在这个函数的执行时间开销上, 所以怎么跳过这断开销非常重要。

想要解决这个问题只能在coroutine.yield和coroutine.resume这两个api上做文章。

由于在C代码中调用yield之后下次再被resume时,会直接走到lua代码中,并不会回到上次调用yield的C代码处。为此我不得不使用lua对C接口实现的lprofiler做了一个wrapper。

在lua实现的profiler.start代码中将coroutine.yield和coroutine.resume备份并换成自定义yield/resume函数。
在执行自定义yiled/resume之前调用lprofiler.yield,更新当前call stack上所有函数的运行总时间。在执行自定义yield/resume之后调用lprofiler.resume来更新当前call stack上的所有函数开始执行的cpu时间。

如果多个coroutine共用一个函数,那么每一个函数定义对应于一个性能分析数据就不能再满足需求了.
如co1和co2共用一个函数, 当co1执行到50%时yield让出,如果co2接着执行就会再次时入此函数的call hook, 会重新刷新此函数的函数进入时间,这样当co1执行完成之后,得到的时间开销仅是实际开销的一半。

为了避免这个问题,也为了可以针对某一个具体coroutine进行profile。

可以为每一个coroutine单独存储每个函数的性能分析数据。 由于使用coroutine来做为索引, 因此一定要使用weak-table来存储与coroutine相关的性能分析数据。

在打印性能分析报告时,可根据实际情况,合并所有coroutine中的相同函数定义的时间开销。

由于coroutine一旦被回收其性能分析数据均会被GC掉,因此打印性能分析报告的时机一定要选择好。

如果最终确定不需要对单个coroutine进行profile,可以仅仅为每个coroutine维护所执行函数的开始执行cpu时间。这样即可避免当coroutine被GC后,相关的profile数据丢失问题。