• GIMP,GNU Image Manipulation Program的缩写,一个图像处理程序,熟悉Linux的人对这个名字应该不陌生,几乎所有Linux的发行版中都有它的身影,它的功能相当于Windows的Photoshop。工作需要,最近开始跟它打交道,悲喜皆有,于是有了一些胡思乱想。

    关于C程序的安装
    在*nix下,标准的安装过程是./configure, make, make install。

    屏幕上闪过一片片让人眼花缭乱的命令。幸福总是相同的,编译成功。不幸的却各有各自的不幸:不幸中的幸运是缺少东西,至少没有立刻宣判死刑,踏上未卜的前路,寻找拼图缺失的一角;更为不幸的是编译错误,稍微幸福一点的见错知意,手到病除,最为不幸的是只有对着屏幕发呆,一个陌生的程序,一大堆令人费解的错误。

    最初拿到GIMP,我试图在Cygwin下编译,我的Cygwin对于它来说并不那么完整。于是翻开INSTALL,寻找缺少的部件,未曾料到竟一路找了下去,GTK+、Glib、ATK、Pango、Cairo……。到最后,我停在了一个不知从何入手的编译错误上。这时,我想到了GIMP的INSTALL上的一句话:除非你对从源码构建软件非常有经验,否则不要尝试自己构建所有这些库。果然是经验之谈!遂转到Linux上,支撑环境的健全使得构建一次成功。

    对比Java的经验,从源码构建应用,C这套方法麻烦更多一些,当然,我这里指的是出问题的时候。因为Java出的问题多是缺少相应的类,一般都可以很容易找到,通常发布的会有源码和JAR两种形式选择,不愿意从头构建,就选择JAR。而对于C,一旦出了编译错误,基本上都很难解决,毕竟不是自己熟悉的。即便是缺少组件,因为C发布的大多是源码,而从源码开始构建本身就是一种风险,不要忘了,我们来自一个没有从源码构建成功的项目。现在各种各样的包管理器试图去协助解决这个问题,但标准的方法是根源,也是应用的最为广泛的形式,所以,问题很难得到完全的解决。

    关于移植性
    找几个C的项目来,你会发现稍微大一些的项目里面都有很多相同的东西,比如对于数据类型的定义,比如内存管理函数的封装,比如对多线程的封装……。这不是偶然,一切都来自C的可移植性。C当年凭借着自己的可移植性成功击溃了汇编语言,成为了一代语言霸主。平台的千差万别都要通过C来屏蔽,最初的时候,没有统一的标准,所以,每个项目都要通过自己的努力来做倒这些,所以,我们会看到类似的数据类型定义,类似的内存管理,类似的多线程……,以致于C程序员认为这是一种天经地义,于是有些年轻的项目一上来也是定义一套属于自己的东西。在程序设计的世界中,Java成功的解决了这些问题,它用虚拟机去解决平台问题,于是,C语言的这些问题随之烟消云散。当然,这并不是说Java世界就那么完美,社会在进步,所以同样的问题在Java世界以更高级的形式体现出来,君不见那么多的Web框架、那么多的持久化手段,那么多的DI解决方案。再造轮子不是坏事,新轮子总会拥有一些旧轮子不具备的东西,而旧轮子如果不甘心退出舞台,那就拿出更好的表现来。人们的选择是最好的评判。

    与Java的跨平台方式不同,C的跨平台常常是通过宏定义对平台的差异进行枚举。枚举的问题显而易见,稍不留神,便会遗漏。所以,对于Java那样,几乎一个平台编译没问题别的平台基本就没什么问题,而对于C,只要编译平台未能覆盖到,就可能出现编译错误。就像bug一样,找到的越多,也许剩下的就越多。

    顺便说一下,C用得越多,我越喜欢宏,它几乎就是一种元语言。现在DSL的概念开始流行,宏是一个不错的DSL载体。当然,它是一把双刃剑,所以,在实际开发中,我也不便多用。

    关于插件结构
    GIMP是一个基于插件的架构,我知道,熟悉Java的人脑子里出现了Eclipse。Eric Raymond在他的《Unix程序设计艺术》中讨论模块化时谈到了它。从我了解的情况来看,对于那些跨越时间的程序都有很强的可扩展性,比如TeX,比如Emacs,比如Unix。时间会改变一切,再好的设计师也无法预测未来的所有变化,与其费尽心力去完成不可能的预测任务,倒不如敞开大门,让程序拥有可扩展性,让未来的人来编写代码满足自己的需求。从这点上来看,Eclipse已经有了很好的基础,至于它是否能够跨越时间,慢慢来。

    设计是为了应对变化,如果不变,再好的设计也是枉然。看过那么多关于设计的书,了解了那么多设计的原则,不妨再向这些拥有悠久历史的软件学习,它们真正体现了程序设计之道。

  • 没有控制住》之后的一个周末也没有控制住:我又一次拜访了书店,原本打算买的书没买,为自己的控制力暗喜,结果是,控制力没有坚持到离开书店,买了本计划外的书。

    这次诱惑我的是Gerald Weinberg的《理解专业程序员》,多好的一个名字,于是,我在一堆书的夹缝中把这本书拿了出来。有一段时间,清华大学出版社一口气出了好些Weinberg的书,我只买过《成为技术的领导者》,因为相对来说,这本比较薄。其它的顶多是在书店的时候偶尔翻翻。不过,我很喜欢他的风格,用程序员的身边事讲述一些道理,虽然这些书大多出版在很多年之前,但其中的很多故事仿佛就在今天,除了那些看上去已经有些久远的名词。在读累了技术书之后,翻看这样的书还是一种比较不错的享受。

    名字之外,《理解专业程序员》让我有兴趣翻看的一个原因在于它的薄,全书也不到200页。做程序员绝对不缺少对厚书的恐怖回忆,所以,一本薄书自然会让人感觉亲切一些。再下来发现的是,这本书的译者是刘天北,这位仁兄的文字能力很强,在《程序员》和网络上读过一些他的文章,总给人一种舒服的感觉,这让我对这个译本有了一些期许。清华大学出版社似乎特别愿意把书放到发霉才出版,至少我知道《Effective STL》和《设计模式精解》就有这种不堪回首的经历,《理解专业程序员》又为这种不良印象增加了注解,gigix的代译者序清楚写着2005年3月,而这本书的出版日期是2006年7月。随便找了一个地方开始了自己的阅读之旅,好书的一个标志就是让人不忍放手,在我读了一些之后,我确定我需要买一本,否则,我很可能干出站在书店把书读完的蠢事,虽然以前确实有过这种经历。

    正如gigix在代译者序所说,读这本书可能会经历一种很复杂的心情:才嘲笑完别人的愚,转而懊恼自己的蠢。那些看似远在天边的例子,实则近在身旁。说说给我留下最深印象的一段吧!第3章的开始探讨了一个让程序员无法回避的话题:身体。程序员的身体常常会有些问题:腰、腿、肩、手……,这些所谓职业病,实际上就是不好的习惯造成的。Weinberg给了一个冠冕堂皇的理由,对工作诚挚的专注,以致于在需要时,我们会虐待自己的身体以完成工作。抛开理由不说,在做程序员的这几年,我确实感觉自己的身体不是很好,虽然我早就知道该注意身体,但死性不改的习惯让自己的身体常常会疼痛难忍,为此搭进去的钱不算,还要搭进去不少时间和精力,所以,有一段时间写的blog特别少。来北京最初的那段时间,疼痛偶尔袭来,往往会让自己坐立不安,于是,强迫自己进行锻炼。没有多大的强度,只是简单的围着居住的小区跑跑步,在小区的一个园子里借用各种器械活动一下。起初的那段时间,有身体的提醒,只要白天一觉得不适,晚上一定会去运动一番。看来当初大夫的忠告是对的,多活动。经过一段时间,疼痛袭来的频率大幅下降,由此带来的副作用是有时候想不起去锻炼。现在大约每周能出去活动两三次,比起之前多长时间不活动一次强许多。不知道随着天气逐渐凉起来,这样的习惯是否能坚持下去。正是因为这样的经历,读到这里,我的感受颇深,无论干什么,身体都是最重要的。yeka最近也讨论过类似的话题。其实,有个好身体不难,只要有个好习惯。

    书的最后一章,有个关于我该做多长时间程序员的话题,我愿意把它视作Weinberg对所谓做程序员到X岁的问题的一个回答:如果你不再学习了,那么你也就到期了。在我看来,支撑一个人不断学习的动力在前进路上的快感:想想把一个虚无的功能实现的感觉,想想痛苦很长时间找到bug的感觉,想想体会到程序之美的感觉……或许,现在的水平还不为人道,但学习可以让我进步,这就是做程序员的快乐。Weinberg说得对,如果到了什么时候,我无法从编程中找到乐趣,无法支撑我继续学习,我会选择离开。

    这里只是简单列举几个我在阅读这本书的感受。如果你和我一样是个程序员,这本书几乎从头到尾都会有一些有趣的思考。这本书有个瑕不掩瑜的问题,刘天北在文中加了不少注释,遗憾的是,没有标明是译注。

  • 因为《没有控制住》,我买了本《算法设计与分析基础》,目的就是为了对算法设计的方法有个基本认识。选择这本书的原因之一,就在于这本书的薄,正文三百多页。或许把三百页的书形容为薄有些过分,但对比于《算法导论》一千多页和《计算机程序设计艺术》的多卷本,你就知道三百页的书着实可以划到“薄”的行列中了。

    原本,我打算一边看一边做,就像去年看《自己动手写操作系统》一样。不同的是,我在这方面的基础显然要好于开发操作系统的基础,至少我学过数据结构的课程,加之我不知道以这种方式阅读自己能坚持多长时间,而且自己读书的目的是了解概貌,于是,我改变了策略,选择囫囵吞枣的方式,尽快的把书看完一遍,这样,可以达到自己最初的目的,如果有兴致,可以再对感兴趣的点进行细扣。或许这就是最近一年受研究风格的影响,先铺面,后找点:每当进入一个新的领域,前期总会找大量的相关领域资料进行阅读,快速建立知识体系,然后,选择适当的着手点,进行细致的研究。

    已经完成第一遍的阅读,这里记录一下阅读的心得。

    按照作者在前言中的说法,这本书最大的价值在于采用了一种全新的算法设计技术分类法。从前的书介绍算法会采用两种方案,一种是按问题分类,也就是常见的排序、查找、图等等,还有一种是按照算法设计技术,比如分而治之、动态规划、贪婪算法、回溯、分支界定等等。之前翻过一些算法书,基本上也没逃脱这个划分。读那些算法书总会给人一种见树木不见森林的感觉,或许通过学习,对某个算法可以比较了解,但算法为何如此设计依然云里雾里,也就是,没能理清算法设计的思路。这本书采用了一个全新的分类方法,把一些曾经不为人重视的设计策略提了出来,比如蛮力法(最直接解决问题)、减治法(减小问题规模)、变治法(把问题变为另外的问题)和时空权衡(时空互换),加上一些原有的设计技术如动态规划、贪婪算法等构成了新的分类方法,填补了算法和设计之间的鸿沟。这正是我最初注意到这本书的一个重要原因。

    一本好书,除了有好的内容,也要有好的表达。我说过,我曾经在读秀上读过这本书的前十几页。在这十几页中包含了一个求最大公约数(gcd)的例子,当然,是那个著名的欧几里德算法,连《计算机程序设计艺术》都用它开篇。这么简单的一个例子是不足以吸引人的,让我动心的是后面给出几个笨拙的gcd算法。为什么会是笨拙的算法?除非训练有素,否则,遇到问题的第一个解决方案常常会是很笨拙的,这恰恰是我遇到的问题。事实上,这样的表达方式在后面不断出现,作者不会像先知一样凭空给出一个设计精良的算法,而常常选择一个看起来很笨的方法进行介绍,然后逐步引出后面的算法,这更符合一个人的思考过程。我喜欢这种不把读者当白痴的介绍方式。不过,有些部分我读起来还是比较吃力的,比如第10章关于P、NP和NP完全问题,应该是自己在这方面的底子比较薄的原因吧!看算法时可以享受阅读的乐趣,着实不易。

    看中文版的好处之一就是快,至少我现在的英语阅读能力还远远无法与中文相提并论。在我看来,这本书的翻译还是比较流畅的,至少在阅读过程中,我很少因为让人无法理解的中文而被打断思路。当然,再好的译本也有一些瑕疵,我几乎每章都能找到一些小问题,我已经把这些问题提到了china-pub上。译者不错,对我找到的这些问题给予了积极的响应,不过,我也因此得寸进尺,发现了更多的问题。来自译者的一个消息是,他正在翻译第二版,不知道会有多少变化。喜欢对中文版吹毛求疵的人,可以选择这本书的影印版

    如果你和我有同样的需求,这本书是个不错的起点。

  • 实现一个将图像所有点按位反转的功能,下面是最直接的一个实现。稍微需要解释一下的是src_step和dst_step,它们是为了存储时可以对齐,所以,可能会与宽度有所差异。

    这里的uchar定义为unsigned char。

    void not_ver1(uchar* src, int src_step, uchar* dst, int dst_step,
    int width, int height) {
    for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
    int src_pos = i * src_step + j;
    int dst_pos = i * dst_step + j;
    dst[dst_pos] = ~src[src_pos];
    }
    }
    }

    过往的经验告诉我,最直接的方法往往是最为笨拙的,但却是一个很好的起点,可以作为下一步的参照物。在C/C++中,指针和数组之间关系密切,但在性能问题上,二者之间还是存在一些差异,下面是一个改进的版本。

    void not_ver2(uchar* src, int src_step, uchar* dst, int dst_step,
    int width, int height) {
    for (int i = 0; i < height; i++) {
    for (int j = 0; j < width; j++) {
    dst[j] = ~src[j];
    }

    src += src_step;
    dst += dst_step;
    }
    }

    如果所上面的改进只是小步幅的前进,那下面这个版本应该是一个大刀阔斧的变革了。它是从OpenCV相关代码拿出来的,OpenCV是Intel提供的一个开源计算机视觉库。因为来自Intel,所以,OpenCV在PC上有着良好的表现。

    void not_ver3(uchar* src, int src_step, uchar* dst, int dst_step,
    int width, int height) {
    for (; height--; src += src_step, dst += dst_step) {
    int i = 0;

    if ((((size_t)src | (size_t) dst) & 3) == 0) {
    for (; i <= width - 16; i += 16) {
    int t0 = ~((const int*)(src + i))[0];
    int t1 = ~((const int*)(src + i))[1];

    ((int*)(dst + i))[0] = t0;
    ((int*)(dst + i))[1] = t1;

    t0 = ~((const int*)(src + i))[2];
    t1 = ~((const int*)(src + i))[3];

    ((int*)(dst + i))[2] = t0;
    ((int*)(dst + i))[3] = t0;
    }

    for (; i <= width - 4; i += 4) {
    int t = ~*(const int*)(src + i);
    *(int*)(dst + i) = t;
    }
    }

    for (; i < width; i++) {
    int t = ~((const uchar*)src)[i];
    dst[i] = (uchar)t;
    }
    }
    }

    让我们来看看这里都做了些什么,对比第二个版本,外层循环变化不大,但这里控制变量由原来“++”变成了“--”,因为有不少机器对JNZ有特别的指令处理,速度非常快。

    内层循环则完全改变了模样。表面上看,似乎循环更多了。

    首先来看if语句,这段话是对内存地址进行判断,看是否两段内存的最后两位是否都为0,换句话说,按4对齐,在下面我们可以看到,后续的处理都是按4字节进行处理(在32位机器上,一个int是4个字节),内存地址的按4对齐,可以保证读读取一个int不必跨越边界造成多次访问。这里实际的输入是uchar类型,也就是一个字节,而在这里处理的是4个字节,也就是实现了一种软件的SIMD(单指令多数据)。看到这里,就可以理解前面参数设置时那两个step的作用了,对齐是为了提高性能。

    在if语句块中第一个循环中,一次处理了16个字节,也就是四个int。这里运用了循环展开的方法,这是一种常见的优化方法,另外,这样的分解可以更好的利用流水线进行计算,提高代码的并行性。有些编译器优化时可以完成简单循环展开,但这里控制循环的存在变量,所以,代码没有依赖于编译器自动展开。另外,我们可以看到,在循环中,还有t0和t1,它们的存在是为了减少代码之间没有必要的读写依赖。

    有了上面的分析,后面的代码就容易懂了,剩下的循环,是在没办法大踏步前进时,选择小步伐前进。至于最后的循环,它既是不对齐的无奈选择,也是不足4象素时的更小步伐。

    优化是要有证据的,下面给出我在自己机器上测试的一个结果,测试环境如下:
    CPU:1.86GHz
    内存:1GB
    OS:Windows XP SP2
    开发环境:Cygwin
    编译器:GCC
    数据规模:10000×10000

    这里使用测试方法是用gettimeofday获取时间,需要注意的是,gettimeofday本身会受到进程调度的影响,但对于这里所做性能估计来说,这种影响是可以忽略的。下面的时间单位为微秒。

    在未优化的情况下,
    ver1 1423000 1450000 1398000
    ver2 867000 838000 901000
    ver3 189000 184000 190000

    在O3优化的情况下,
    ver1 224000 195000 200000
    ver2 213000 192000 195000
    ver3 166000 158000 165000

    可以看出,在未优化的情况下,三个版本的性能差异非常大,而在优化的情况下,三者的性能差异就会缩小许多,但是第三个版本在性能上依然有比较大的优势。

    关于C代码优化方法的讨论,可以参考《C代码优化方案》。

  • 2006-08-06

    没有控制住

    我控制,我控制,对不起,我没控制住。

    希望范伟不会介意我盗用这段经典的台词,我这里指的是买书。四个月了,如果我没记错的话,这是我毕业之后最长的一段时间没有买书,杂志除外。

    有句歇后语,秀才搬家——尽是书。书这东西在搬家的时候最能体现其复杂性,沉啊!为了准备到北京的常驻,我需要处理一下自己的东西,由于离家比较近,所以,当然的选择就是尽可能的运回家。整理一下才发现,装了一个小货车的东西,居然有一半是书。幸好装箱的时候,把书都放在了小箱里,否则,真是搬都搬不动。从那以后,我努力着压抑着自己买书的欲望,终于成功在赴京之前做到一本书都没买。

    初到北京的时候,开销比较大,所以,压缩了许多额外的开支。初来乍到,拜访朋友是不可少的,所以,几个周末就如此被消耗了。最近一段时间,给自己留了一些空闲时间,对周围的环境也越来越熟悉,结果,成功的找到了书店所在。于是,逛书店重又成为我的一种休闲方式。是的,我有逛书店的瘾,或许这与女人们愿意逛街算是异曲同工。

    这次吸引我的是一本算法书,《算法设计与分析基础》。其实来北京之前,我就注意到了这本书,只不过,那段时间我坚决制止买书的非分之想,否则,可以要千里背书了。大学的时候,学过数据结构,但没学过专门的算法课程。随着自己写的程序越来越多,越发觉得自己在这方面基础的欠缺。经常会觉得自己写的程序很笨,因为通常都采用最原始的蛮力法,虽然应付眼皮底下的工作绰绰有余,但自己总觉得别扭。特别是近一年经常编写图像相关的程序,这种笨拙越发让我觉得难以忍受。所以,想买本算法书补一下自己的基础。书的好处就在于它的全面性,类似的事以前也干过,去年为了比较全面了解Linux的使用,专门买了本基础的书,从头到尾的翻了一遍,虽然我早就过了ls的阶段。其实,我也有几本算法的书,包括著名的《计算机程序设计艺术》,但对于想得到概貌的我来说太复杂,做参考书还差不多,而且它们也不在身边。

    这年头书太多,所以,买书得找所谓的经典。不过,说实话,经典也不少,加上出版社的宣传比较精彩,经常会让人产生不买这本书实在可惜的错觉,事实上,就像地球少了谁都一样转,没有哪本书是真正的必读。还是考虑一下自己的实际需要。像我经常性的大脑充血,买回来不少自己认为自己可能会看的书,虽然这些年也看了不少书,但也有相当多的书,买回来便被我束之高阁了,虽然我曾经就此给过自己安慰

    现在做图书的网站不少,到处转转还是有好处的,像china-pubdearbook这样评论比较多的地方也许可以得到一些有用的信息,当然,得自己做过滤器。如果书是国外的,通常可以找到国外网站的链接,比如Amazon。说实话,相比而言,Amazon中灌水的现象可不像国内网站这么严重,所以,信息的价值要大得多。最近比较火的书评网站还有豆瓣,不过,那里的计算机书书评稍微少了一些。这次搜书过程的一个额外发现是读秀,它提供一些书提供了一些样章的在线阅读,这样先读上几页可以让自己对书有个大致的了解。当然,在书店,我们也是可以读的。可能有些朋友喜欢网上购书,不过,我更喜欢在实体书店买书,因为除了买书之外,在书店可以顺手翻看一些书,说不准就有额外的收获,也可以叫额外的付出。

    买了书还是要看的,希望这次的书能够看完。这不,趁着热乎劲,一口气读了五六十页。