代码模块化(一)

今天重新review了一遍代码, 发现模块竟然有几十个之多, 之间引用大概如下:

MODULE_MAIN -> MODULE_A MODULE_B MODULE_C …
MODULE_A -> MODULE_B MODULE_C MODULE_SUB_A1 MODULE_SUB_A2 …
MODULE_B -> MODULE_SUB_B1 MODULE_SUB_B2 …
MODULE_C -> MODULE_SUB_C1 MODULE_SUB_C2 …

也就是说这些模块之间其实并非是同级的, 有些模块只是被某一个上层模块调用罢了.
但是我目前代码里面其实并不能很明显的看到这种引用关系, 如果这种模块多了, 相互间的引用关系在大脑中就容易出现混乱(最起码我是这样), 而且不容易维护.

因此必须要尽最大可能明确这种依赖关系, 并加以限制以使模块之间的依赖不会循环不会交错.

我一般是一个C文件来当做一个模块, 如果一个C文件过长就会将此C文件拆成几个子模块.

总结一下目前我使用的模块创建方式:
//方式1
struct module;
struct module *module_create(…);
int module_do_something(struct module *self, …);

int module_free(struct module *self);
//方式2
int module_init(…);
int module_do_something(…);

方式1由于整个module相当于存在于self所指向的内存区域, 以上面的引用关系来看MODULE_SUB_A1之类的模块很适合采用方式1来编写, 因为此模块只是为更好的完成MODULE_A, 那么也只有MODULE_A才能来使用, 如果使用了方式1, module_create返回的self就只有MODULE_A才能拥有, 就不用担心被其他模块引用, 或错误的引用.

在写代码时应该很少会遇到所有模块只会被引用一次, 一定有些模块用来当作基础模块来使用, 如上面的MODULE_B, MODULE_C这种即被MODULE_MAIN引用又被MODULE_A引用, 那么此时MODULE_B, MODULE_C再使用方式1来实现就势必会出现全局变量了, 全局变量的出现会使程序变得更糟糕, 这时就必须使用方式2了.

但是为了让方式2也能清晰表示出依赖关系, 可以稍加限制来清晰表明模块引用关系, 示例代码如下:

module_a 引用 module_b
module_c 引用 module_b

int module_a_init()
{
….
module_b_init();

return xx;
}

int module_c_init()
{
…..
module_b_init();

return xx;
}

如果每个模块都遵循这种规则, 那很就可以很清晰的看出模块之间的依赖关系. 这时会出现一个问题就是module_b_init可能会被多次调用的问题. 可以在自己模块数据中记是否被init的过来实现, 也可以通过实现一个module_init_once(int (*init)(void))函数来间接调用module_b_init, module_init_once来保证module_b_init只会被调用第一次.

————————————
最近越来越觉得其实代码模块组织应该是树形的.

注:部分灵感来自云风大神

cache命中率对效率的影响

趁着去买菜的空档, 突然间想起来cache命中率失败的代价从来还没测过, 随后又想到网上很多人争论如果两个for循环嵌套, 到底是大循环放在外面效率高还是小循环放在外面效率高. 虽然有大牛说不同情况不同分析, 但到底为什么却没有分析.

于是就for循环与cache命中率写了一段测试代码访问1G的数据来测了一下性能, 竟然有1倍之多.

代码如下:

#include <Windows.h>
#include "assist.h"

//cache line 64 byte
//cache size 4Mbyte
typedef unsigned char cache_line_t[64];

cache_line_t cache_line [1024 * 1024 * 1024 / sizeof(cache_line_t)];

#define FAST 0

int _tmain(int argc, _TCHAR* argv[])
{
int times;
int i, j;
int tmp;

CACL_TAKES_TIME_BEGIN(aa);

#if FAST
for (j = 0; j < ARRAY_SIZE(cache_line); j++) { for (i = 0; i < 64; i++) { tmp = cache_line[j][i]; } } #else for (i = 0; i < 64; i++) { for (j = 0; j < ARRAY_SIZE(cache_line); j++) { tmp = cache_line[j][i]; } } #endif printf("takes:%dmsn", CACL_TAKES_TIME_END(aa)); return 0; }

具体原因可以参考这篇文章. OK买菜去.

------------------------------------------------------------
<<代码大全>>p623中讲到把最忙循环放在最内层, 是因为for循环第一次是需要初始化变量的, 如果最多层越少, 那么i变量被初始为0的次数就越少. 但是从上面的例子表明并非是这样, 这说明当数据量到达一定程度之后, 就要权衡是i被初始化的次数花的代价高还是cache命中率降低的代价更高.

这再次说明了一个道理, 这个世界上没有银弹, 同样说明不加思索的拿来主义在一定程度上是程序效率的拦路虎.

无锁编程

好久没写blog, 最近都在研究无锁算法, 一直在纠结为什么无锁高效, 无锁是否是真的无锁.

看了coolshell的无锁队列后, 曾经专门做过锁的性能测试. 自以为发现了无锁的高效之处.

可是真到我的无锁队列写出之后, 突然间感觉之前的测试太片面了, 无锁对于效率的提升也许并没有我想象的那么高, 也许并非无锁
当然其效率提升其实已经很显著了在某些条件下.

再回过头看重新看了一下coolshell中对的无锁队列下面的那行总结”如果不用专门的车锁, 那么就得自己锁自己”, 突然间发现大牛的观点与我一样.

下面看一段coolshell上的代码:

EnQueue(x) //进队列
{
//seg1
q = new record();

//seg2
q->value = x;
q->next = NULL;

//seg3
do {
p = tail;
} while( CAS(p->next, NULL, q) != TRUE);

CAS(tail, p, q); //置尾结点
//seg4
}

从代码上分析看出, 整个函数可分为3段:

seg1~seg2之间其实肯定是有锁的, 因为分配内存函数必然有锁.
seg2~seg3之间是可以并发执行的, 如果有2核的cpu同时跑2个线程那么这一段代码是可以并行执行的.
seg3~seg4之间作用也与锁等价, 在这段区间内如果cas失败了, 必然会引起线程切换, 否则这个线程永远也不会成功(同时有3个线程执行到seg3之后, cpu只有两个核的情况下).

如果EnQueue写成有锁的话那么seg1~seg4是全段加锁的.

所以比较一下有锁与无锁的本质来看, 可以看出其实虽然是无锁编程虽然号称无锁但其实也是有锁的, 只不过是换了一种形式的锁而已.
从锁的范围来看, 可以看出无锁编程中锁的区域更小, 线程之间碰撞概率更小, 那么引起线程切换的概率会更小, 线程切换带来的开锁会更小.