Recently in 软件开发 Category

代码review

| | Comments (1) | TrackBacks (0)
我:我们有两台机,都是一样的 xeon E5620 2.4G 的CPU,但是,跑算Pi的程序,5000个迭代,一个机器跑48秒,另一个只需要32秒

coly:CPU核数一样吗?

我:一样的。不一样应该没关系,我这是单进程。

coly:内存速度不一样?

我:就一个算Pi,总共就占8MB内存,还没有CPU的L3 cache大呢

coly:要不这样,你写个死循环加法,我们看看两边的速度是不是一样

(公司的盒饭送来了,晚饭时间)

coly:你先吃,我来写吧....一个死循环我应该还是能写出来的....你要review我的代码哟~
我:....

(coly把代码写出来了

int main(void)
{
        int a=0;
        while ( a < 0) {
                a++;
        }


        return 0;
}


coly:为什么我运行瞬间就结束了?
我:应该是编译器把你的死循环优化掉了,因为a这个变量在整个main函数里都没有被其它地方使用
coly: 喔~~,我改改,在结尾加个printf把a打出来

(程序一运行还是瞬间结束)
陈同学:(看了看代码)是不是加法太快了,看不出来?咱们加几个嵌套循环
coly:好~

(程序还是瞬间结束)

coly:怪了....靠,a的值是0啊,根本进不了循环!
我,陈同学齐声:喔~~~

coly:你们两个!怎么review代码的!




后记:

死循环程序写出来了,两台机器CPU的计算速度确实有差异,原因最后由柯旻同学揭示:

跟华为2285机器的cpu 频率控制有关
CPU不开启cpuspeed就是1.6G的主频,需要

/sbin/modprobe acpi-cpufreq
echo ondemand |sudo tee /sys/devices/system/cpu/cpu13/cpufreq/scaling_governor

后,主频才上到真正的2.4G
这一阵子的工作是在给ext4加feature,就是“让ext4的extent length的单位由block变为cluster“。一边写代码一边加测试用例,这个测试用例我就简单的用shell实现的,就用dd命令来写文件。

顺序写的时候还好,等到偏移写(seek一段再write)的时候出了kernel crash,原因是某个extent的length突然变成了0。一开始当然是怀疑我自己哪里写错了,于是加断点,调试,直到我去南京出差周末在宾馆也在调试(感谢马涛同学提供的虚拟机镜像,不然连不上vpn就没法调了),发现事情很神奇:我的extent开始length是2,dd之后还没干别的呢,length就变了,于是终于给dd加了个strace,一看,我的天,dd自己会做ftruncate!而我还没有实现truncate的新feature

这dd真是体贴得我流泪啊,我一直把它当纯write使啊,没想到dd会“智能“的多加一个truncate操作。去翻了一下dd的代码,只要是 of= 一个文件,dd必然会先truncate之,除非带上conv=notrunc。

看来工具不熟就是误事啊。

附dd用法(seek 8k然后write 8k,不要truncate):

dd if=/dev/zero of=/test seek=2 bs=4K count=2 conv=notrunc 
这几天用shell编程做一个简单的自动回归测试,需要用到shell的函数功能,大约是这样:


sum ()
{
    return `expr $1 + $2`
}

sum 3 4
echo $?        #result


一直用的不错,直到今天加了一个case,echo出来的返回值($?)就不对了,set -x调了一番,加法没错,但echo出来就是不对,最后暴力改改,让返回值变为直接 return 1024,咦?echo的东西变了,于是一番google,找到了这篇

原来shell里面函数的返回值是用的errno,而errno只支持0-255,所以如果返回值太大就返回的不对了。

替代方案是干脆用“全局变量”


RES=0

sum ()
{
RES=`expr $1 + $2`
}

sum 133 244
echo $RES
sum 333 444
echo $RES

intel

上周五下午去参加了intel的软件开发大会。我参加过去年的大会,那时候强调的是TBB,这次强调的是并行优化。
先是屠鹏先生介绍Cilk,不知道这门语言在业界怎么样,反正我是第一次了解。用cilk写出来的Fibonacci算法如下:

int fib(int n) {
if (n<2) return n;
int x = cilk_spawn fib(n-1);
int y = fib(n-2);
cilk_sync;
return x+y;
}

上面的cilk_spawn表示“指定的子函数可与父调用方法并行执行”,而cilk_sync表示“在所有衍生的子函数返回之前,控制不能通过这一点“,其实跟pthread里的pthread_creaet和pthread_join有点相似。
还有就是for循环:

for(int i=0; i<8; i++) {
do_work(i);
}

变成

cilk_for(int i=0; i<8; i) {
do_work(i);
}

这样就是cilk就可以把循环变成并行的,把do_work变成8个任务,分配给多个核。cilk还支持方便的矩阵运算。
这是并行计算里典型的“语言级”的解决方案,个人感觉相比TBB这类“库级”解决方案,平台兼容性和移植性会比较差。何况cilk的强项——矩阵运算/数值运算我在公司几乎从未遇到。

然后是一位仁兄演示Vtune的使用,客观的说,讲得有点琐碎,就看见鼠标飞舞,在界面上乱点,满屏红条条绿条条,眼晕。有空还是自己下载一个evaluation版来试用吧。

之后是一些优化案例,我才知道intel有一堆团队专门给大客户做应用程序的优化,想来这活儿八成比较枯燥,但是成就感应该还不错。
其中一个案例,用至强5600系列机器自带的一些指令来优化AES加密,给一个网游公司提速了不少;还可以用SSE指令来帮忙优化数据压缩的速度等,如下:

intel,AES

给我印象最深的是陈健先生的演讲,他的速度很快,思维敏捷。主讲内容摘录如下:
方法一:尽可能使用最新版的intel编译器
1. Intel c/c++ Composer 12.0
2. 使用编译选项
2.1 针对通用的性能优化:-O3 -fast
2.2 针对快速除法和开方:-no-prec-dev / -no-prec-sqrt
2.3 针对向量化:       -xS, -xT(Core), -xP(NetBurst)
2.4 针对CacheMiss:   -fno-alias
2.5 高级优化选项:     -ip/-ipo -prof-gen/-prof-use
2.6 其它常用选项:     -static -unroll(n) -recursive
方法二:尽可能使用最新版的Intel MKL数学库(10.3)
1. 由Intel资深工程师针对Intel硬件平台充分优化
2. 最大限度的避免了Cache Miss/Branch Miss Prediction等性能问题
3. 最大限度的使用了处理器内部的资源,比如SSE处理单元,64位扩展寄存器等
方法三:修改应用程序来辅助编译器优化程序
1. 通过Intel VTune Amplifier查看程序运行热点函数和热点代码段

呵呵,毕竟是以intel为主题的,不算是很通用的优化方法。

intel

向量

还在我刚工作的时候,我的一个头头就说希望能有一个像仪表盘的服务器监控系统,但是他也没有具体想法,因为如果一台机器就展示为一个仪表盘,那一百台,一千台当如何展示?如今陈健先生给了些启发,他强调:表盘也好,数字也好,都不如颜色给人的感觉直观,所以,干脆就让每一台机器都变成一个颜色块,颜色块里还分内存监控/CPU监控/硬盘监控等等,以不同颜色表示不同监控项,就比较直观了,虽然做不到一眼就看出每台机器的细节,但是一瞥之下找出运行不正常的机器还是很容易的。由于会场很暗,我只有一张不太清楚的照片:

监控

那些绿快快里面还有很多细节,鼠标移上去会自动放大。确是不错的想法。

tmux 滚屏

| | Comments (3) | TrackBacks (0)
我的tmux把Ctrl + a设为prefix,如果想滚屏,可以 Ctrl + a再按"[",此时进入所谓的copy-mode,然后就可以用上下键或PageDn/PageUp浏览屏幕了。想退出copy-mode直接按"q"。
我用惯了vi,对上下键不习惯,tmux号称是支持vi快捷键的,不过需要在.tmux.conf里加一行:
setw -g mode-keys vi
我试了一下,tmux-1.1是不支持这个配置项的,必须装 tmux-1.3或更高版本(另还需要libevent-1.4),才能顺利用vi按键方式滚屏。

今天用 Ctrl + a 加"?"看了tmux的所有快捷键,还支持分屏,这个我居然从来没用过,老土了。

代码小比较

| | Comments (8) | TrackBacks (0)
判断上百万个4k的buffer是否为全0,我最先想到的办法是:
zero_buffer = malloc(4096);
memset(zero_buffer, 0, 4096);

/* 循环百万次读取buffer */
    if (!memcmp(zero_buffer, buffer)) {
        /* 全0 */
    }

做完以后跑了一下,速度凑合。由于好奇,看看shell工具cp的代码,它的解决办法是:

/* 循环百万次读取buffer */
    long *wp = buffer;
    while (*wp++ ==0 && wp<buffer + 4096)
        continue;

    if (wp >= buffer + 4096) {
        /* 全0 */
    }

cp的代码真称得上千锤百炼,这比我的方法要快,因为我的办法需要CPU访问两块内存(buffer和zero_buffer),而cp只问一块内存,CPU的cache会高效得多。
判断一块4k buffer当然看不出快慢,我们循环上百万次,立刻就能看出差别:cp的算法比我的快了3倍。


=== 2012.04.19 ===

首先感谢 bernie 的提问,我自己写了完整的代码来验证这个事儿:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define SIZE (1024*1024*1024)

void my_process(void *a, void *b)
{
if (memcmp(a, b, SIZE))
printf("wrong\n");
}

void cp_process(void *a)
{
long *wp = a;
while (*wp++ == 0 && wp < (long *)(a + SIZE))
continue;
}

int main(int argc ,char *argv[])
{
struct timeval start, end;

char * zero_buffer = (char*)malloc(SIZE);
memset(zero_buffer, 0, SIZE);
char * buffer = (char*)malloc(SIZE);
memset(buffer, 0, SIZE);

gettimeofday(&start, NULL);
my_process(buffer, zero_buffer);
gettimeofday(&end, NULL);
printf("my:%lu\n", (end.tv_sec * 1000000 + end.tv_usec) -
(start.tv_sec * 1000000 + start.tv_usec));

gettimeofday(&start, NULL);
cp_process(buffer);
gettimeofday(&end, NULL);
printf("my:%lu\n", (end.tv_sec * 1000000 + end.tv_usec) -
(start.tv_sec * 1000000 + start.tv_usec));
return 0;
}

绝对能编译,(大家注意,我只算内存比较的时间,什么malloc什么memset只是我的准备工作,不算在运行时间内的)我在一台开发机上跑了一下,结果是:

my:3428159
cp:611168

cp命令的算法显然比我的快。大家可以自己在机器上试试,如果结果不一样,咱们再研究研究 :)

另外,要注意那句
if (memcmp(a, b, SIZE))
printf("wrong\n");
不能直接写成
        memcmp(a,b,SIZE);
因为只写“memcp“的话my_process函数会被编译器优化掉(因为函数没有返回值,也没有改全局变量,也就是说,没有任何”输出“),我猜 bernie 遇到的就是这个问题