寻找更快的平方根倒数算法

机器学习的模型相关计算中,有很多诡异的运算。单个运算的开销很不起眼,但如果这些运算的量足够大,也会对性能产生一定的影响。这里谈的就是一个简单的运算:

a = b / sqrt(c);

对于 C/C++ 语言的程序员来说,sqrt 已经是非常基础的库函数,它的底层实现也仅仅是简单的一句 FSQRT (双精度是 SQRTSD) 指令,看起来没有什么优化的余地。但事实上 intel 提供了一个更快的指令,那就是 SQRTSS,利用这条指令,平方根倒数的计算速度可以达到 sqrt 版本的两倍(实测,与[1]相同)。你可以这样使用它:

#include <xmmintrin.h>
...
__m128 in = _mm_load_ss(&c);
__m128 out = _mm_sqrt_ss(in);
_mm_store_ss(&c, out);
a = b/c;
...

但这就是优化的尽头了么?不,单就求平方根倒数来说,还有一个神奇的近似算法,叫做 Fast Inverse Square Root平方根倒数速算法)。一个神人在 Quake III Arena 游戏中使用了一个神奇的数字 0x5f3759df,创造了这个神奇的算法,这个算法可以将平方根倒数的计算速度提升到 sqrt 的 3 倍多(实测,效果比[1]差)。

float Q_rsqrt( float number )
{
        long i;
        float x2, y;
        const float threehalfs = 1.5F;
 
        x2 = number * 0.5F;
        y  = number;
        i  = * ( long * ) &y;                       // evil floating point bit level hacking
        i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
        y  = * ( float * ) &i;
        y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
//      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed
 
        return y;
}

但 3 倍就是优化的尽头了么?很不幸,邪恶的 Intel 提供了这样一条指令 RSQRTSS,从硬件上支持了这个近似算法。利用这条指令,平方根倒数的计算速度能够达到 sqrt 版本的 6 倍以上!!!

#include <xmmintrin.h>
...
    __m128 in = _mm_load_ss(&c);
    __m128 out = _mm_rsqrt_ss(in);
    _mm_store_ss(&c, out);
    a = b*c;
...

虽然平方根倒数速算法只是一种近似算法,并且只有单精度版本,但是对 RSQRTSS 指令的简单测试发现大部分情况下误差在万分之一以下,指令说明书中给出的误差是 ±1.5*2^-12[2],在非精确数值计算的工程系统中已经足够用了。

它带来的一个更有趣的后果是:如果使用 RSQRTSS 计算出来 c 的平方根倒数,然后再乘以 c,就得到了 c 的平方根近似值。用它可以反过来加速 sqrt 的运算![1]

注1:编译相关程序时,需要打开优化开关,以实现函数的 inline
注2:RSQRTSS 和 SQRTSS 均有一个向量版本,如 RSQRTPS,可以同时计算 4 个 float 的平方根倒数;

[1] Timing square root
[2] RSQRTSS

马套峡谷-水头长城-镇边城徒步

这是跟随“梦在珠峰”这个绿野团队的第二次活动,同样的时间地点集合,遇到类似的问题:司机因看到禁行大车的标志而不肯直接开到镇边城,于是本来的“镇边城-水头长城-马套峡谷”穿越,变成了“马套峡谷-水头长城-镇边城-马套峡谷”的往返,距离也从 15 公里增加到 25 公里。这两次活动真是充分证明了户外运动的不确定性。本来以为路途很轻松所以把登山杖丢在家里,而带上了单反相机,后来证明有些失策。

马套峡谷原来叫黑沟,近期在进行旅游资源开发,改名叫做“南石洋大峡谷”。峡谷中间的路已经整修的比较规范,徒步几乎没有难度。水头长城是一段野长城,有很强的纪念意义,抗日战争时日军曾从这里突破,包抄国军后路,导致南口战役的失败。从历史照片来看,现在的水头长城还基本维持着当年的原貌,只是城墙坍塌了一些。镇边城也是南口战役激战的一个地点,现在看起来只是一个小村子。

水头长城靠近马套峡谷那面坍圮的比较厉害,爬起来很费劲,而且有些危险。但是在烽火台上吹着山风,看群山起伏,长城绵延,景色真是美不胜收,非常棒!

水头长城全景

我以前一直有一个错觉:长城没剩多长了,剩下的应该都被整修成了景点。这次水头长城徒步以后,我仔细地看了一下卫星图,才发现长城居然还有那么长!走完野长城才感叹,修长城的劳动人民实在是太伟大了!下图中央坍圮的位置,就是当年日军通过的水头隘口,旁边竖着一面纪念碑。同时这段长城的圆角矩形烽火台,据说也是一大特色。

水头长城

另一个方向的长城,几乎不成样子了,但遗迹仍然伸向远方。

水头长城西侧

烽火台上

下面是这次徒步的轨迹,其中有一段走岔了走了回头路,然后尾巴上一大段 10 公里走的是公路。这段公路一直走到天黑,7点多些才走到大巴车的位置。

路线:http://www.foooooot.com/trip/102227/

走了几次户外才发现北京周边还有如此地好景色,在人迹罕至的地方徒步实在是胜过景点太多了!

小海陀一日穿越

知道有绿野这个论坛让我欣喜不已。以前总发愁想郊游没办法,从没想过网上可以聚集起这样一批人。为了让自己的周末过得充实些,9 月 7 日初体验了一把自由结队的活动,参加了绿野论坛上“梦在珠峰”团队的“大、小海陀一日往返”,这是今年爬的第二座千米峰。

通告是 7 点 10 分在阜成门附近集合,但还是不可避免地有人迟到。7 点半左右大巴发车前往大海陀村,京藏堵到一塌糊涂,11 点左右才到海陀山附近。不幸地是前往大海陀村的公路限高 2.3 米,大伙儿只好在 X012 县道靠近阎家坪附近的限高架那下车,徒步从阎家坪登山,这下整段路程的强度和难度一下加大了不少,也为后面的困难埋下了伏笔。

本来是 29 人,刚上山就有一位女生晕车导致呕吐,加之看到居然是走野路,她的三个同事就陪她撤了下去。剩下的人继续登山,走了大约 8 公里才到山顶,中间集体仅休息一次吃饭。整个队伍被拉长到超过一公里。由于走的是山脊,有起伏和树木遮挡,从队中间都看不到两头。手机信号也很差或者没有,只能靠手台通信。幸好我为了这次活动买了个手台,事实证明发挥的作用还不小。

我上到松山(小海陀)山顶的时间大约是3点,大小海陀中间的鞍部平地是北京著名的扎营去处,营地能容纳几百顶帐篷。由于下山驴友反馈大海坨山有持枪民兵把守,再加上时间不是很够,领队就决定放弃登顶大海坨山,于是也没有真正走到鞍部去观察一下宿营的盛况。行程也由“大、小海陀一日往返”变成了“小海陀一日穿越”。

4 点一刻开始下山,没有从原路返回,而是下到一个垭口后从著名的销魂坡下去,然后到西大庄科。从后来的gps数据来看,销魂坡的平均坡度角应该超过了 45 度,部分路段相当惊险。要时刻提防可能被上方驴友不小心踢下来的碎石块,搞得我想着下次有必要带个头盔过来。

虽然有陡坡,但下山的路程一点都不短,轨迹显示大概有 7.6 公里。 由于天色已晚,大家都埋头赶路,前后队距离拉得更长。这条路线与上山完全不同,山脊的路线大部分是高山草甸,树很少,很空旷风景很美;下山路上全是茂密树林和灌木,根本看不了多远。我在前队里,总算在天黑前 7 点赶到了山下。只有我带了头灯,留给岔路口等后队的同学,他们一直等到快 21 点才全部下来。后来跟我反馈说头灯作用很大,能照亮一排人下山。

21点多大巴开始回城,23 点 15 才到阜成门,连地铁都没有了。

路线:http://www.foooooot.com/trip/100972/

事后我总结了一下:登山杖、手台和头灯起了很大作用,为了安全得常备;路线的临时更改导致运动强度加大,需要有一定的心理和体力准备;爬山不应该穿短裤,除了概率低的被划伤或被虫咬之外,草枝或石子会老往鞋里进更让人不舒服。最后,奉上山顶傻笑照片一张:

松山(小海陀)山顶

自驾云蒙山

最近看 zhiqiang 老晒登山、户外照片,搞得自己心里也痒痒的。正好我的车到了首保时间才开了两千公里,于是和几个同事好友约了一起自驾去爬云蒙山,顺便也磨磨车。后来约到 8 个人,两辆车。

周六早上 6 点起床,6 点半左右出发,分别在阜石路和北三环接了两个朋友,7 点上京承,7 点 20 到收费站,然后到第一个服务区与另外一辆车集合,重新分配人员。因为起得早,市区基本没堵车,只是在崔各庄收费站前堵了一点点。

后面是半程高速,怀柔桥转京密高速然后上京加路,在导航到目的地之前注意“云蒙山森林公园”路标即可。京加路比较好走,只是到云蒙山的岔道急弯比较多,距离不长。

我们9点左右到的景区,门口买票,开车进景区停车。9 点 15 分开始爬山,一路上大伙儿那是相当地欢乐。由于之前在六只脚大致查过轨迹,所以一路上都用六只脚看行走距离和海拔,我们戏称为“查看进度条”。对目的地的预判对分配食物、饮水和体力有不少的帮助,不过后来还是发现普遍食物带的多,饮水略有些少。

最终大约 12 点 30 分到达主峰山顶,拍了会儿照。后来重看照片,觉得从山顶看风光角度比半山腰略差一点,主要是绿色的山峰较多,岩石斑驳的山坡较少。云蒙山得名“小黄山”,还是斑驳的地方有看头。

拍完照片找了一个树荫吃东西,聊了会儿天,13点30开始下山,在一个分叉路发生了分歧,走了一段岔路后决定还是原路返回,大约浪费了半小时。刚下了几百米,就发现天色阴暗,偶尔有雷声传来。所有人都没带雨具,于是大家都开始加速下山,速度快了许多。中间的确滴了几点雨,但在树叶遮蔽下没淋到多少,倒是后来下到山脚发现山脚雨应该更大。走到半山腰厕所处太阳重新出来,正好一个朋友抽筋了,这才停下来等走得慢的,集合继续往下走,16点左右下到停车场。下面是爬山行程的整个轨迹:

路线:http://www.foooooot.com/trip/99623/

16点20分开车返城,在离收费站大约10公里的地方开始行驶缓慢,最终18点左右到达京承崔各庄收费站。从百度地图上看各环路一片红,于是趁收费站到5环间还没有堵死,快速从来广营桥出口出去走辅路转市内道路了。送了两个朋友到地铁站,差不多快7点转到3环回家,北三环西侧和西三环一路畅通,7点10分顺利到家。

7个小时爬山,5个多小时开车,路上还挺精神,但到家下车以后觉得脑袋有些疼。安排得有些满了,以后爬山的话,还是尽量争取参加集体包车的活动。

std::inner_product的简单性能测试

最近团队产品中用到了一些机器学习方面的算法,涉及到求向量内积,采取的是最朴素的实现方式(元素乘积循环相加)。有一天路上想到 STL 提供了一个模板函数 std::inner_product ,就好奇 libstdc++ 实现上是否对该算法做了什么优化呢?

于是做了个简单的实验:1000 维 double 类型向量乘积,用 std::inner_product 和朴素方法分别计算10000次,g++ -O2优化。第一轮使用原生 double 类型数组,第二轮使用 vector<double> 容器,分别在三个机器环境下进行了计算。

// Processors | physical = 2, cores = 32, virtual = 12, hyperthreading = no
//     Speeds | 12x2400.186
//     Models | 12xIntel(R) Xeon(R) CPU E5645 @ 2.40GHz
//     Caches | 12x256 KB
//        GCC | version 3.4.5 20051201 (Red Hat 3.4.5-2)
	   
a*b     : std::inner_product(27.934ms), for loop(40.061ms)
a_v*b_v : std::inner_product(27.878ms), for loop(40.04ms)

// Processors | physical = 2, cores = 12, virtual = 12, hyperthreading = no
//     Speeds | 12x2100.173
//     Models | 12xAMD Opteron(tm) Processor 4170 HE
//     Caches | 12x512 KB
//        GCC | version 3.4.5 20051201 (Red Hat 3.4.5-2)

a*b     : std::inner_product(31.242ms), for loop(47.853ms)
a_v*b_v : std::inner_product(31.301ms), for loop(47.815ms)

// Processors | physical = 1, cores = 0, virtual = 1, hyperthreading = no
//     Speeds | 1x2572.652
//     Models | 1xIntel(R) Core(TM) i5-3320M CPU @ 2.60GHz
//     Caches | 1x6144 KB
//        GCC | version 4.7.2 (Ubuntu/Linaro 4.7.2-2ubuntu1)

a*b     : std::inner_product(41.76ms), for loop(33.165ms)
a_v*b_v : std::inner_product(35.913ms), for loop(32.881ms)

可以看出不同环境下 std::inner_product 的表现不尽相同,与朴素的方式相比有优有劣。瞄了一眼 gcc 4.8 的 libstdc++ 的代码,没有注意到 std::inner_product 对基本类型做什么 SSE 指令的优化。不过倒是有个并行计算的版本,可能对超大的向量计算有帮助。

虽然从性能上没有看到明显的优势,但毕竟 std::inner_product 可以简化一个循环的编码,至少可以少测一个分支嘛。而且配合重载函数的后两个 functor 参数,可以做一些有趣的事情,比如算一组数的平方和,比较两个字符串相同字符的数量等。以后呢可以多尝试一下用标准库的算法而不是自己写循环。

寻找最快的Python字符串插入方式

在 MapReduce 分布式计算时有这样一种场景:mapper 输入来自多个不同的数据源,共同点是每行记录第一列是作为 key 的 id 列,reducer 需要根据数据源的不同,进行相应的处理。由于数据到 reducer 阶段已经无法区分来自什么文件,所以一般采取的方法是 mapper 为数据记录打一个 TAG。为了便于使用,我习惯于把这个 TAG 打到数据的第二列(第一列为 id 列,作为 reduce/join 的 key),所以有这样的 mapper 函数:

def mapper1(line):
    l = line.split('\t', 1)
    return "%s\t%s\t%s" % (l[0], 'TAG', l[1])

这样给定输入:

s = "3001	VALUE"

mapper1(s) 的结果就是:

s = "3001	TAG	VALUE"

这是一个潜意识就想到的很直白的函数,但是我今天忽然脑子转筋,陷入了“这是最快的吗”思维怪圈里。于是我就想,还有什么其它方法呢?哦,格式化的表达式可以用 string 的 + 运算来表示:

def mapper2(line):
    l = line.split('\t', 1)
    return l[0] + '\t' + 'TAG' + '\t' + l[1]

上面是故意将 '\t' 分开写,因为一般 TAG 是以变量方式传入的。还有,都说 join 比 + 快,那么也可以这样:

def mapper3(line):
    l = line.split('\t', 1)
    l.insert(1, 'TAG')
    return '\t'.join(l)

split 可能要消耗额外的空间,那就换 find:

def mapper4(line):
    pos = line.find('\t')
    return "%s\t%s\t%s" % (line[0:pos], 'TAG', line[pos+1:])

变态一点儿,第一个数是整数嘛,换成整型输出:

def mapper5(line):
    pos = line.find('\t')
    pid = long(line[0:pos])
    return "%d\t%s\t%s" % (pid, 'TAG', line[pos+1:])

再换个思路,split 可以换成 partition:

def mapper6(line):
    (h,s,t) = line.partition('\t')
    return "%s\t%s\t%s" % (h, 'TAG', t)

或者干脆 ticky 一点儿,用 replace 替换第一个找到的制表符:

def mapper7(line):
    return line.replace('\t', '\t'+'TAG'+'\t', 1)

哇,看一下,原来可选的方法还真不少,而且我相信这肯定没有列举到所有的方法。看到这里,就这几个有限的算法,你猜一下哪个最快?最快的比最慢的快多少?

先把计时方法贴一下:

for i in range(1,8):
    f = 'mapper%d(s)' % i
    su = "from __main__ import mapper%d,s" % i
    print f, ':', timeit.Timer(f, setup=su).timeit()

下面是答案:

mapper1(s) : 1.32489800453
mapper2(s) : 1.2933549881
mapper3(s) : 1.65229916573
mapper4(s) : 1.22059297562
mapper5(s) : 2.60358095169
mapper6(s) : 0.956777095795
mapper7(s) : 0.726199865341

最后胜出的是 mapper7 (tricky 的 replace 方法),最慢的是 mapper5 (蛋疼的 id 转数字方法),最慢的耗时是最慢的约 3.6 倍。最早想到的 mapper1 方法在 7 种方法中排名——第 5!耗时是最快方法的 1.8 倍。考虑到 mapper 足够简单,这个将近一倍的开销还是有一点点意义的。

最后,欢迎回复给出更快的方法!

北京生活 TIPS - 谈谈日常理财

接上篇:北京生活 TIPS - 银行服务篇

真正意识到要摆脱单纯定期存款的理财方式,是在付按揭首付款时。由于并未提前做买房的计划,几乎所有的存款都是一年期(≥3.25%),提前支取的结果是利息按照活期(≤0.5%)计算,大概损失了几千的利息——虽然现在看来房价的涨幅让这点儿损失不值一提。

感谢水木社区的“金融产品与个人理财版”(俗称“钱包版”),让我能充分地学习网友们的智慧。这个版上的网友对各项金融业务的熟悉和专业程度,绝对超过大部分银行/证券公司自己的柜员。举几个例子:看到版上有网友说中信网银能够查询个人信用报告,我就去开了个,柜员信誓旦旦地告诉我这项服务已经关闭了——事实上一直可用;看到版上有网友说民生银行办理资金归集送U盾,我也去办一个,大堂经理完全不知道这件事情,还专门向上级请示才给办了。

很多人对理财收益多个 1%、免费送个 U 盾、免费转账/取款这种“钱包版”流行的小便宜很不屑,但我是这样看这个问题:第一,我内心承认有便宜占会让自己心情很好;第二,我把这种出于对活动、渠道、规则的充分了解,并合理利用获取收益的做法视为生活能力的锻炼,也有助于常识的培养。下面不会列举我了解到的所有理财方式,只是简单谈谈我现在怎么管钱。

怎么存钱

随时可能会使用到的资金(一般小于 5w),存在货币基金里,随时取用。简单了解一下货币基金和其历史收益,你就会知道这是一个基本无风险的投资渠道。近几年其收益基本都跑赢了定期存款,例如“博时现金收益”最近一年的收益是 4.07%,“华夏现金增利”最近一年的收益是 3.93%。而货币基金最大的优势,其实在流动性,一般申购和赎回都是 T+1 完成,即赎回款在第二个工作日到帐。华夏、嘉实等 7x24 小时 T+0 赎回方式的推广更是让这种流动性发挥到极致,7x24 T+0 赎回意味着你基本是以活期存款的流动性获得超过定期存款的收益——你还能奢望得到什么呢?

想弄清楚货币基金的操作,需要先搞明白以下几个关键词:货币(市场)型基金、风险评估、基金直销、基金代销、申购、赎回、T+0、T+1、T+2、红利转投、万份收益、7日年化收益。这里可以比较所有货币基金的收益,个人推荐购买华夏基金网站直销的“华夏现金增利”,也叫“华夏现金宝”,赎回时选择“快速赎回”就能实时到帐。记得装华夏基金手机应用,这样就相当于把卡带身上了。

一段时间内用不到的较大量资金(大于 5w),购买低风险低门槛的银行理财,一般年化收益能达到 4%~5.5%。但一定要注意区分银行理财和银行代销的理财,不要贪图收益高,6% 以上年化收益的不要动心,极可能有陷阱。对于生活变动较大的年轻人来说,不要买超过 6 个月封闭期的理财,要预备可能产生的大额支出。一般来说,小银行的理财收益高于大银行,各银行收益如何可能需要自己去探索。如果畏惧银行间搬钱太麻烦或有手续费,可以看一下这篇文章。个人推荐平安银行(和盈、强债)和民生银行(非凡)的理财产品。

关于炒股

对生活质量影响不大的一部分钱,可以尝试一下买股票。开户时需要比较一下交易手续费,一般万五(万分之五)到千一较为合适,千一以上有些高了,万五以下一般人也谈不下来。很多人视炒股为洪水猛兽,我把它视为一种社会实践。而且随着时代的发展,中国的股市肯定会越来越成熟,如果我拒其千里之外,这份知识下一代还得从别处探索,我会觉得这是一种教育失败。炒股还会让你格外关注经济形势和新闻,更深切地感受中国经济脉搏的跳动。不过一定要谨慎,我的看法是投资不宜占比过高(30%总资产以内),不宜追加投资(不是专业炒股)。关于如何炒股,我也在学习和探索,目前主要学习价值投资,关注雪球论坛上的讨论。

关于还贷

有闲钱的情况下,要不要提前还住房按揭贷款?我觉得如果按揭利率低的话,没有必要提前还。以公积金贷款为例,5年以上名义按揭利率为 4.5%,按月折算实际年化利率约为 (1 + 0.045/12)^12 - 1 = 4.594%。如果有收益不低于年化 4.6% 的投资方式(很多银行理财能够达到),完全没有必要提前还按揭贷款。因为一旦你提前还款,不仅生活中可支配资金减少,还损失了超过 4.6% 这部分的收益。

信用卡的作用

有人视信用卡为额外消费的罪恶之源,有人嫌信用卡还款麻烦有风险——逾期产生信用问题,有人惯用信用卡薅各种羊毛,而我把信用卡看成收支调节的一种工具。这样我不必带太多现金在身上,或者留在储蓄卡里,可以放心地把它们放到货币基金或理财里产生收益。至于羊毛,信息零碎又不集中,薅不薅得到就随缘了。

6年以后的变化

我都忘记曾写过这样一篇文章《Google总让我惊喜》,居然被网友翻出来评论了一把。Google Reader 要关闭这件事,的确让人伤心,因为:

From your 189 subscriptions, over the last 30 days you read 258 itemsclicked 7 itemsstarred 13 items, and emailed 0 items.

Since October 30, 2006 you have read a total of 62,661 items.

其实这心,在 Google Reader 中的 Share 按钮被改成 +1 以后,已经伤了一次了。在我这样一个中国用户眼里,Google Reader 曾是 Google 最好的 SNS 产品,就是因为 Note 和 Share。

不太能理解 Google 放弃 Reader 的决策。基于自己的很多优秀产品(Gmail, Reader, Android, Google+),Google 吸引了很多忠实的注册用户,这一点被其它很多公司羡慕——包括百度,除了腾讯。产品推广的时候,吸引用户注册使用往往是一件很困难的事情,这时就能凸显出用户基数大的好处了。好产品越多,用户的忠诚度肯定越高。如果只有单一的产品,产品没落后用户就会轻易地流失,从这个角度看,我觉得保留 Google Reader 看起来不是一件坏事。

无论如何,Google Reader 是快没了,总要寻找其它的替代品。从目前我的探索来看,Feedly 像是一个比较好的选择,但它的连接不是 https 的,可能会被关键词过滤(或者直接被封掉)。鲜果阅读器看起来也还可以,起码比 QQ 阅读看着更像 Google Reader。豆瓣 9 点就不说了,同步的速度是个渣,也没用心做。

北京生活 TIPS - 银行服务篇

接上篇:北京生活 TIPS - 医疗服务篇

以前想过写这个,但总觉得似乎庸俗了点儿。可是好多次同事谈起这些个话题,发现大家对一些生活门道近乎毫无了解,那我想给这些刚刚开始帝都生活的朋友们分享我的一些生活经验应该是有价值的。

因为本人财力有限,下文列出的所有银行卡均为门槛在 5 万以内卡片,办卡要求 5 万以上资产的,不在本文覆盖范围内。且本人以满足个人当前需求出发,并未尝试体验所有银行服务,所以以下为不完全统计。

1. 关于免费跨行、异地转账

如果不考虑通货膨胀的话,大部分情况下都是我们薅银行的利息。大多数人被银行薅,往往发生在异地、跨行转账时。水木钱包版的前辈探索过不少免费转账的方式,我这里不一一列出,感兴趣的可以去水木看置顶或提问。

目前大部分跨行、异地转帐免费的途径均受益于央行搞的超级网银通道,有每笔不超过 5 万的限制。但是大部分银行还保留着原来的转帐通道,而且对超银通道的叫法不同,例如民生把超银通道叫做“跨行账户管理-本行转它行”,北京银行把超银通道叫做“快速转帐”。所以如果你看到下面提到某银行转帐免费,而实践的时候没有仔细去选择超银的免费通道而被收费的话,那只能怪自己不细心。

  • 招商银行金普卡:手机银行跨行、异地转账免手续费,网银、ATM 收费;
  • 民生银行普卡:手机银行、网银(需U盾)跨行、异地转帐免手续费,ATM 收费;
  • 华夏银行普卡:网银(需U盾)跨行、异地转帐免手续费;
  • 中信银行普卡:网银(需U盾)跨行、异地转帐免手续费;
  • 交通银行普卡:手机银行跨行、异地转帐免手续费;
  • 北京银行普卡:网银(动态口令)跨行、异地转帐免手续费;
  • 深发展(平安)银行金卡:网银、ATM 跨行、异地转帐每月免前 20 笔,非超银通道;

2. 关于免费跨行、异地 ATM 取款

虽然跨行、异地 ATM 取款大家很少用,但我想并不是大家不愿意用。相信大家都经历过这样的事情:

  • 需要钱时满大街地寻找银行卡对应的 ATM 机,为了省两块钱走很远的路;
  • 开学、回家或出去旅游的时候带上很多现金,为了节省几十块钱的异地取款费;
  • 取钱时在某银行 ATM 机前排队,尽管旁边的 ATM 机一个人也没有。

那么,看完这篇文章,希望你知道该怎么做了。如果说跨行、异地转帐功能很多股份制银行还不分伯仲的话,跨行异地取款功能基本上就可以淘汰大部分银行了。

  • 华夏银行普卡:跨行、异地 ATM 取款(全球范围、要求银联合作)每天第一笔免手续费;
  • 深发展(平安)银行金卡:跨行、异地 ATM 取款(全国)每月前 20 笔免手续费;
  • 广发银行信用卡:跨行、异地 ATM 取溢缴款免手续费;(关键词:信用卡、溢缴款)

剩下的银行跨行、异地 ATM 取款要么不免费,要么免前 3 笔,且大部分只支持本地跨行,不支持异地本行或异地跨行免费,完全不具有可比性,就不提了。

3. 关于免年费和小额账户管理费

很多人没有注意过储蓄卡的年费和小额账户管理费。这个钱并不多,比如像工行普卡是年费 10 元,季度日均存款小于 300 元,季度末收 3 元小额账户管理费。但蚊子腿再细也是肉,如果能不被薅,自然是最好。

  • 招商银行金普卡:代发工资卡免年费和小额账户管理费;否则免年费,小额账户管理费普卡小于月均 1 万收 1 元,金卡小于月均 5 万收 10 元;
  • 深发展金卡:每季度要求连续一个月月均 5 万,免小额账户管理费;
  • 民生银行普卡:免年费和小额账户管理费;
  • 华夏银行普卡:免年费和小额账户管理费;
  • 中信银行普卡:免年费和小额账户管理费;
  • 建设银行普卡:开通基金直销账户,免年费和小额账户管理费;

4. 关于免费送网银安全工具

大部分人把它叫做 U 盾、U key,总之说的就是这类东西。安全工具大部分只是在推广期免费,目前北京免费送安全工具的银行有:工商银行、建设银行、招商银行、华夏银行、民生银行(开三方存管、资金归集)、广发银行。

5. 关于免费超级网银、资金归集

看到这里,我想大家对股份制小银行比传统大行的优惠有了更深刻的认识。但很多人不愿意成为这些银行客户的原因,竟然是“网点太少,存钱麻烦”!

这时候不得不介绍一下超级网银了,科普大家可以看链接。但它之所以被俗称为“超级网银”,就是可以用一家银行的网银,管理多个银行卡,使资金在各个银行卡之间(自动)流转,这也就不存在“网点太少,存钱麻烦”的问题了。

大部分超级网银关联的要求是:关联两家银行卡时,必须有两家银行的网银安全工具(U 盾或 key)。一旦完成了查询和转帐的关联,你就可以随意调动多个银行卡资金了,而且还可以使用资金归集功能自动扣款。超级网银操作只向一方收费,所以只要你有一方支持免费超级网银,所有操作就都是免费的。

  • 招商银行普卡:资金归集(即扣款)免费,对外转帐收费(可以用手机银行免费绕过);
  • 民生银行普卡:所有超银功能都免费;
  • 华夏银行普卡:所有超银功能都免费;
  • 中信银行普卡:所有超银功能都免费;

6. 特色银行服务

北京中信银行有一项特色服务值得一提,即可以通过网银(需U盾)免费查询个人信用报告。由于人民银行对安全的考虑,个人信用报告查询只能自己到月坛南街的营业部线下查询,目前在线查询功能独有中信一家提供。

7. 总结

总结一下,不考虑银行理财、信用卡、网银易用性等因素,仅从上文列出的免费服务出发,个人认为最值得拥有的储蓄卡是:

  • 华夏银行普卡:送U盾,超银扣款、转出免费(含跨行异地),全球取款手续费每天免 1 笔,无年费和小额账户管理费;
  • 深发展金卡:无超银,但跨行、异地 ATM 取款每月免前 20 笔(比每天 1 笔更实用)、跨行、异地转帐也免前 20 笔(不受超银 5 万限制);

考虑到网银易用性,第三方支付工具接受程度,民生银行的普卡也是一个好选择,只是在跨行、异地取款上较逊色于华夏。

北京生活 TIPS - 医疗服务篇

以前想过写这个,但总觉得似乎庸俗了点儿。可是好多次同事谈起这些个话题,发现大家对一些生活门道近乎毫无了解,那我想给这些刚刚开始帝都生活的朋友们分享我的一些生活经验应该是有价值的。

1. 关于医保存折

对于在北京为员工缴纳正规五险一金的公司来说,社保中医疗保险部分个人缴纳2%,公司缴纳10%。其中对于 35 岁以下职工,划拨 2.8% 到医保个人账户,即北京银行医保存折。医保个人账户是让职工用于日常门诊、买药使用,即覆盖社保卡门诊报销起付线 1800 元以下部分的医疗开支。部分省市个人账户是绑定在社保卡上的,持社保卡就医直接扣减,不可取现(这也是有些地方药房几乎和超市一样的原因)。北京市的个人账户是存折形式,可以直接取现。

那医保存折里面最多会有多少钱呢?以 2012 年为例,2011 年北京市平均工资为 4672 元,那么 2012 年社保缴存上限是 4672x300% = 14016元,医保个人账户的上限是 14016x2.8% = 392.45元。这也就是说在 2012 年你的医保存折中最高每个月可以收入 392.45元,一年最多 4709.4元。

医保存折可以怎么取现呢?很多人只知道可以到北京银行柜台取现,但那个排队时间太长了。部分北京银行网点有医保存折 ATM 机,可以直接用医保存折机器取款,但只能取到百元整数。很少人知道,医保存折可以直接在“北京农商银行”柜台取款,甚至可以在农商银行换折,这个小银行排队的人非常少;更少人知道,可以在北京银行建国支行(北京站)、九龙山支行办理医保存折关联北京银行储蓄卡,然后可以通过北京银行“动态密码版网银”“快速转帐”到他行,零手续费。

2. 关于定点医院

持北京社保卡就医,必须在 4 家定点医院、19 家 A 类医院或者专科医院才能够报销。但很多人不知道定点医院是可以改的,因为居住地与公司定下的初始定点医院较远,所以总是去那 19 家 A 类医院就医。由于 A 类医院全都是三甲医院,就医的质量保证了,但是就医过程非常痛苦。

A类医院有限,离家的路程会比较远,拖着病体跑那么远也是受罪;而且其医疗资源比较稀缺,所以各地来看病的人非常多,挂号排队是一件痛苦的事情,对于特殊的科室来说,比如牙科之类,等待时间会更长;可能三甲医院的医生有创收压力,头疼感冒的小病开的药往往也非常贵,最贵的是一些中成药,还有挂针的服务费、材料费。所以小毛病去三甲医院看病拿药完全是找虐,不如去社区的二甲或者卫生所。

北京规定社保卡指定的 4 家定点医院其中至少有 1 个二级,至少有 1 个三级或以下。所以一般可以这样分配,选择离家近的一个卫生所和一个二甲医院,选择离家近,或者在某些科上比较先进的两家三甲医院(A 类必是三甲;三甲未必属于 A 类)。所有的定点医院名录和编码可以在北京市人力资源和社会保障局网站上查询,然后将 4 家医院的名称和编码提供给公司的 HR。有的公司是 HR 负责后续变更事宜的办理;如果想快的话,可以用 HR 提供的加盖公章的申请表、U盘拷贝变更内容文件,直接到社保中心办理,当天生效。以前是理论上每年只允许变更一次定点医院,今年年中时有个新闻说不再有此类限制,不知道是不是落实下来了。

3. 关于急诊

有时候为了救命,急诊时只能选择就近的医院。按北京规定急诊可以选择就近的二级以上医院,属于医保范围。但是急诊住院只有前 7 天费用可报销,如果为了省钱,可能就得忍受转院的痛苦。其实从操作上,由于社保中心修改定点医疗机构是即时生效的,所以完全可以在 7 天内将定点医院改为急诊所在医院(除非悲催地在某个 25 日左右出事,因为每月 25 日以后社保中心不办公),就可以避免转院了——这可能需要公司 HR 的理解和支持。

接下篇:北京生活 TIPS - 银行服务篇

溜走的2012年

隔壁传来小孩儿的夜啼,身边躺着熟睡的妻子,偶尔会踢蹬我两下。这是我今年焦躁不安夜晚的其中一个,只是有个外国名字叫做 Christmas Eve 让它显得略有些特殊。今天晚上我错过了食堂的晚饭,又是在陶然亭地铁口的沙县小吃对付了一顿。茶树菇排骨馄饨和蒸蛋,味道还不错!

这是个寒冷的冬天,也是我穿得最少的一个冬天。其实我还想穿得再少点儿,可惜我没有车,还要上班,再穿少可能就有机会坐救护车了。前天在灵山上可真的冻死了两个人。

上下班单程,我得花一个半小时,实际上这大概是个期望值。本来我还想装逼地用泊松到达来准确分析一下时间的分布,后来想想,三段路呢,别找不自在了。再说,我统计学得又不好。

自从搬到现在的办公楼,我就不怎么开心。就像正住着五星级忽然被撵到偏远的快捷酒店一样,关键是还不给退钱。天杀的,刚开始让我住地下室多好!

年初换了个完全不同的工作方向,业务的比重大一些,技术的比重小一些。喜欢谈不上,不喜欢也谈不上,事实上我也没有能谈喜不喜欢的资本。只感慨一点,评绩效时要讲业务,评职称时要讲技术,难道这就是苦逼互联网码农的宿命?在大公司里做事,有太多的无可奈何。在没能力改变的时候,只能顺势而为;无法追求做得更好时,追求做得别那么糟也不会是件坏事。

各种躁我都有一些,焦躁、烦躁、浮躁 。跟那些老成持重的教导不同,我认为这是一种自然现象,年轻时不躁恐怕就老了。但这并不能缓解想到这些躁带来的躁,一点儿也不。我尝试用各种方式来转移注意力,吉他、篆刻、手工、读书、摄影、焊电路。结果表明,哦,慢着,没有结果。对于意志力不坚定的家伙来说,这几乎是必然结果。

所以这一年伴随我的,有不少是沮丧和对自己的失望。

哦,还是有一些喜事儿,我结婚了,相信明眼人从第一段就看出来了。我“悄悄地”在老家办了个婚礼,除了对自己的喜宴饭菜很失望外,这个老式的婚礼没给我带来太多烦恼。除了老家的朋友外,我一个同学也没通知。我知道他们也不太可能来,但更重要的原因是我也很少参加同学的婚礼。理由很简单,我不愿意吃亏也不想占便宜,而记礼金实在是太累。

这一年里我们这个小家庭最显著的成就,可能就是倾尽两人的所有积蓄,在帝都西南五环外买了个小房子。我跟朋友借了些钱付首付,但没有啃老——实际上父母们也没钱给我们啃。虽然这是个还没盖好的期房,虽然它离我的公司有四十多公里,虽然它在一个我之前都没印象的房山区,但毕竟是个房子。首付有限,没有什么更好的选择。在楼市一波又一波的涨潮冲击下,这个两年后才能到手的房子让我们心中略微能安稳一些。

这个2012年,我过得可以说漫无目的,也可以说目的很单纯——就是赚钱(结婚、买房、还钱)。但是心中的道德律总是让我在痛苦地挣扎,我必须得提炼点儿什么,明确点儿什么,才能对得起这一年年虚度的光阴。这,也许就是我将要做的事。

那些害人的编码“神谕”

同其它领域一样,计算机科学和工程领域也是群星璀璨,有些耀眼的星光甚至刺得我们无法直视,只能匍匐在地上聆听神谕。也正如其它领域一样,虽然大家听到的是同样的话,却有各式各样不同的理解。我这里想讲的,就是我观察到的不同理解引发的现象。

“过早优化是万恶之源。” 这是 Donald Knuth 的一句名言。虽然大部分人都不知道,或者会忘掉前面半句:“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil.” Knuth 说出这句话时,可能想不到这句话会多么地流行,多么根植在很多人心中,以至于成为程序员偷懒的借口,阻碍进步的动力。因为有了这句话,在你指出别人代码中可以优化的问题时,还必须浪费口舌来解释这样的优化是必要的,不是过早优化或者过度优化。

就我的观察而言,对很多程序员来说,其能力还远远达不到过早优化的地步。但他感觉自己受到了 Knuth 的神启,仿佛具有了某种魔力,不优化代码反而成了一种优越感!关于大多数人是否具备过早优化代码的能力,我可以举几个至今我还觉得神奇的例子。

我供职的公司内部有这样一个模块,隔一两个星期总会挂掉几台服务器,现象是内存占满导致服务器假死或者宕机,但事实上根据请求推算根本不会同时使用那么多内存。最后的排查结果发现,每个线程都有这样一个数据结构,它的内存是只增不减的。当你调用它的 clear 接口,它只会把所有的内存还回自己的内存池里,而不是还给系统。这就导致可供分配的内存越来越少、越来越少...

还是这个模块里,仅仅加载一个几 K 的配置文件,就能够占用超过 1G 的内存。为什么呢?因为它用 char str[MAX_CONF_LEN] 保存配置字符串,用 struct xx_t xx[MAX_XX_NUM] 读取配置,而且这个 struct 中还有嵌套的 struct yy_t y[MAX_YY_NUM] 数组。

该模块是个个例吗?还是这家公司,一个全公司使用的公共日志库,LOGGING 宏定义中直接传一个需要系统调用的函数作为参数,导致无论关不关该级别日志都要进行一次系统调用

这家公司好歹也位列国内顶尖的互联网公司之一,工程师的招聘要求也是极其高的,还会普遍出现这种肆意浪费资源的情况。那么我想对于大部分工程师来说,谈避免“过早优化”、“过度优化”,还为时尚早。

还有一句名言“好代码本身就是最好的文档。当你需要添加一个注释时,你应该考虑如何修改代码才能不需要注释。” 这是 Steve McConnell 说的。同样,大部分人都不知道,或者忘掉后面半句:Good code is its own best documentation. As you're about to add a comment, ask yourself, "How can I improve the code so that this comment isn't needed?" Improve the code and then document it to make it even clearer. 如果你是程序员,回想一下多少次跟别人讨论代码是不是必须要注释时,这句话被引用到;有很多次在写代码时喜爱这句话,又多少次改别人的代码时痛恨这句话。

还是从我个人的观察来看,对很多程序员来说,其编码能力还不足以达到“代码本身就是最好的文档”的地步,包括我自己。敝司招聘过很多顶尖的工程师,有传说中的各种杰出前辈,可能在各种学校、公司内部事迹广为流传。但若是你哪天继承了他的代码遗产,就会发现很多传说中的明星跌落凡尘。成百上千行没有注释,使用一个公共库函数时要么接口就根本没注释只能基本靠猜,要么即使注释也语焉不详让你踩到未注明的大坑。每到这个时候你心里总会暗暗骂娘,后面别人再谈到他的光辉事迹时,你跟随讪笑时心中暗自腹诽:“牛逼个锤子!”

但我想很多人争论的焦点是:“注释是不是不可省略的、要强制执行的?”即使个别人能力真能达到“代码本身就是最好的文档”的地步(我还没见过),我也不建议在团队中传播“注释可以省略”这一想法。因为如果你说“注释可以省略”,可能你会发现大家都理解和实践成“终于可以不写注释了”。如果一个刚刚大学毕业、脑袋里从来没有过 documentation 概念、从来没写过注释的新人进入公司,就“终于可以不写注释了”,那么我想他的代码会很难达到“代码本身就是最好的文档”这个级别。因为他根本没有机会懂得什么叫做 documentation。

在公司里,代码注释深远地影响着团队合作的每个人,以及软件生存期里所有的维护者,甚至会影响自己的职业声誉。所以无论别人怎么想,我对注释这个问题的答案始终是:“注释是不可省略的,越完善越好的,甚至强制执行矫枉过正也没关系的!”

一种抵御 DDoS 攻击的 IP 追踪技术

在拾掇家务时,发现一页我在 2008 年 10 月做的备忘录,记录了一个可用于抵御 DDoS 攻击的 IP 追踪技术。当时因为觉得 idea 太小,不值当写成文章投出去。纸张放那里总是占地方,在博客里电子化一下,然后就能销毁了。

Differential Deterministic Packet Marking

这个 idea 是在 IP 协议的基础上做一些扩展,可以帮助用户在 DDoS 攻击时识别攻击数据包和定位攻击者。在理解这个 idea 之前,可能需要先看几篇参考文献:

[1] Z. Gao and N. Ansari, "Tracing cyber attacks from the practical perspective, " IEEE Communications Magazine, vol. 43, no. 5, pp. 123-131, 2005.

[2] A. Belenky and N. Ansari, "On deterministic packet marking, " Computer Networks, vol. 51, no. 10, pp. 2677–2700, 2007.

[3] Y. Xiang, W, Zhou, and M. Guo, "Flexible deterministic packet marking: an iP traceback system to find the real source of attacks, " IEEE Transactions on Parallel and Distributed Systems, vol. 20, no. 4, pp. 567-580, 2009.

这个 idea 主要是在 [3] 基础上做的改进,其 motivation 是仅仅使用标记段 [3] 内容太保守,用标记段再结合 IP 头中已有的信息,可以做得更好。简单来说就是运营商的接入路由器在 IP 头中增加一些标记,服务器在遭遇到 DDoS 攻击时,可以根据接入路由器增加的标记再结合 IP 头中已有的信息,识别攻击流量,以及确认攻击源。

下文的内容基于三个假设:

  1. Source IP 和 ingress router 的接入 interface 的 IP 经常在同一个网段中;
  2. 大部分网络流是正常的网络流而非 DDoS 的网络流。
  3. ingress router 在可控域中,未被入侵。

图1: 典型网络拓扑示意图,来源于[2]
由图中可见,如果主机使用真实IP 的话,Host 1 发出数据包的 source IP 和 router interface 1 的 IP 仅在最后八位不同,Host 2 发出数据包的 IP 和 router interface 2 的IP 也是仅仅在最后八位不同。

由假设有大部分数据包的 source IP 与 router interface IP 在同一个网段中,这样 ingress router 只需在标记段中标记与 source IP 不同的位即可进行追踪。

图2: IP 头和标记段,标记段与[3]相同
在我的 idea 里,Mark 使用的是 IP 头中的 IDENTIFICATION 域,共 16 位(我们也可以用上 TOS,这样就有 19 或者 24 位),其中各个位的作用如图 3 所示:

图3: 标记段内容

入口路由器上执行的标记算法是:

Algorithm:( 16-bit Mark case, RI for Router Interface)

if (SourceAddr RIAddr ) ⊕ & 0xffff8000 = 0 // Case 1
    Mark := SourceAddr ⊕ RIAddr // 1 packet
else if (SourceAddr ⊕ RIAddr ) & 0xff000000 = 0 // Case 2
    Digest := hash(RIAddr)
    for i=0 to 2 // 3 packets
        Mark[i].M := 1
        Mark[i].A := 0
        Mark[i].seg := i
        Mark[i].digest := Digest
        Mark[i].address_diff := ((SourceAddr ⊕ RIAddr) >> (i*8)) & 0xff
else // Case 3
    Digest := hash(RIAddr)
    for i=0 to 3 // 4 packets
        Mark[i].M := 1
        Mark[i].A := 1
        Mark[i].seg := i
        Mark[i].digest := Digest
        Mark[i].address_diff := ( RIAddr >> (i*8)) & 0xff

我提出的新 idea 对 [3] 的改进能够带来以下几个好处:

  1. 大部分数据包仅靠 1 个包就可以 traceback。根据上面的假设,正常的数据包应该仅仅在 IP 地址的低位与路由器接口 IP 不同,这样仅仅需要在 mark 中的低 15 位与源 IP 异或就能得到路由器接口 IP(case 1)。
  2. 加快路由器对正常数据包的处理速度。由于正常的数据包仅需要 1 个包就能 traceback,不需要计算地址的 hash 值,大大加快了路由器对正常数据包,也是大部分数据包的处理速度。
  3. 不影响正常 IP 数据包的 fragment 策略。由于大部分正常的数据包仅仅需要一个包就能 traceback,那么修改 IDENTIFICATION 域对这些数据包的 fragment 策略毫无影响。对于非正常的数据包,本算法需要多个包才能 traceback,所以对其 fragment 策略会有影响,但由于它是非正常的数据包,不用考虑后果。

之所以觉得这个 idea 小,有两个原因:

  • 第一是在别人方案基础上做的改进,创新不够;
  • 第二是需要部署到全网(至少某个运营商内部)所有接入路由器上,(尤其在 IPv6 已增强安全性的前提下)不太可能实现。

这也符合我对很多研究的看法,虽然有意义,但在工程上基本价值不大,基本上就自己 YY 一下。

消息队列

整理浏览器书签时候发现以前对 Message Queue 产生过一些兴趣,以后说不定还会仔细做一下调研。在这里先简单记录一下,也好精简书签数量。

1. MemcacheQ

使用 memcache 协议的消息队列,存储看是使用的 Berkeley DB。受限于 Berkeley DB,对消息大小有一些限制——不超过 64K,但简单易用,缺点还包括缺乏复杂的消息队列特性。

2. ØMQ (ZeroMQ)

专门的消息队列实现,支持多种语言,支持较多的高级特性,也可用于进程内通信。类似于 TCP,不假设消息格式,需要用户自己处理消息封包、解包。文档很清楚,看起来很酷的样子,未调研是否支持分布式扩展。

西安印象

2012 年 9 月下旬,我有幸被分配到了西安参加公司的软件研发工程师校园招聘工作。校园招聘完成之后,我又在西安多逗留了几天,和老婆小逛了一下这个城市。

我是 21 日晚上 9 点左右到达的咸阳国际机场,取行李浪费了快一个小时。其实只有一个包和一个旅行箱,后悔没有像其它人那样直接带上飞机,能省好多时间。去西安前看过忠告,在咸阳机场一定要找西安而不是咸阳的出租车,就是车牌号是“陕A”的。其实即使是“陕A”,外地人也难逃被宰的命运,区别只是被宰多少罢了,生活在这片神奇的土地上的人们应该有这种共识。我们从机场到颐和宫大酒店打车花了 110 元左右,通过与其他人的沟通发现,这已经算很便宜的了。

第二天下午就开始改软研职位的笔试考卷,由于这个职位报考的学生比较多,一直改到午夜十二点半。次日上午因与企鹅公司笔试冲突休息一上午,以后的几天就开始进行密集的面试。基本上除中午休息一小时外,要从早上 9 点面到晚上 7 点多。在这样的时间安排下,也就晚上能吃顿像样的饭。其中的一天晚上,我和同事一起去回民街逛了一下,吃了顿贾三灌汤包子。

我老婆在招聘快结束的时候也开始休年假,去到了西安。了解我俩以往旅行经历的都知道,我俩一起出去旅行,往往是待宾馆的时间长,出去逛的时间短。虽然出去不多,但该体验的吃的、玩的大致也体验了。

先说吃。刚到西安时,老余同学招待我到招商大厦附近吃了顿“老米家羊肉泡馍”,我还记成了什么黄记;酒店附近有一家“天下第一面”,英文名特别好玩“First noodle under the sun”,这家的油泼面和 biang-biang 面好吃不贵;还去吃过两次“杨翔豆皮涮牛肚”,都是用团购的券来着,挺实惠的;我和希希还去回民街也吃了顿“老米家羊肉泡馍”,还有“贾三灌汤包子”;回民街的烤柿饼特别难吃,唯一觉得差劲的小吃;希希有个超大的兴趣,就是每到一个城市都要去吃“傣妹火锅”,我们去过两家店,长安南路一家、东大街一家;西安有家凉皮快餐连锁店“魏家凉皮”,凉皮和肉夹馍味道都不错,要是开到北京准能火。

玩儿方面,我俩就比较弱了。说好的陕西历史博物馆懒得起没去;钟鼓楼就在外面看了看,觉得没必要就没进去;古城墙上去了,还租车沿着城墙骑了一圈,花了七八十分钟的样子。看那古城墙貌似大部分也是后来翻修的;去兵马俑最坑爹,先是到西安火车站东侧的广场等车,功课没做好,犯傻没去直接坐 7 块钱直达兵马俑博物馆的 306 (就是游5)。地图上说 914 也到,而且看到 914 排队的人多,就直接排上了。谁知道 914 是趟慢车,坐这车的人大部分是去临潼市区而不是去兵马俑的,售票员还收 10 块大元。结果到了临潼市区,直接把我们转包给一个黑车司机,接着各种转悠,忽悠我们去看假秦陵地宫,买什么玉。在我毫不妥协的坚持下,只好把我们送到兵马俑博物馆门口怏怏而回。

兵马俑博物馆的门票也贵,涨到了 150 元,跟网上有的游记说的不太一样。我买完票出来,希希硬拉着我去退票,说这钱够她吃好多顿傣妹了... 里面呢,实话说没啥意思。就一号坑还规整点儿,有些成排的兵佣,二号、三号坑就是打酱油的,尤其是二号坑,那真是坑,几乎没东西。由于强制买联票,我们心有不甘地也去秦始皇陵转了一圈,但进去没多远就出来了,就一个封土丘啊,除了土和树,毛都看不见。

在兵马俑昂贵的门票教训下,后面希希拒绝去任何要费钱的景点,所以什么大唐芙蓉园啊、大雁塔啊,都没去。倒是在钟鼓楼和火车站广场附近的商业区逛了几遍。

每次出去旅游回来,我都不好意思说去哪里旅游了,往往是说到哪里渡了个假,因为俩懒人实在没游多少地方嘛。下面整理几张照片,好歹纪念一下“这个地方我曾经来过”。

用词典查找代替VLOOKUP

从上一篇《PYTHON操作EXCEL》可以看到,Python 操作 Excel 已非常自如方便。但是 Python 和相关库毕竟是一个额外的依赖,若能从 Excel 自身解决此类问题,自然是更为易用。

1. VBA 中的哈希表

用 Python 的着眼点主要是 VLOOKUP 公式太慢了,所以关键是要找到一种更高效的算法或数据结构定位数据。VLOOKUP 要求对列进行排序,内部应该是对列内数据进行二分查找,算法上不好再优化了,那就只好更换一种数据结构。搜索了一下,VBA 提供了 Scripting.Dictionary 这一词典结构,而且有文章说内部是哈希表实现,那就正是我要的东西了。

这样,VLOOKUP(lookup_value,table_array,col_index_num,range_lookup) 这一公式就转为下面的词典查找方式来实现:

  • 使用要从中进行查找的 table_array 内容构建词典。用 table_array 第一列作为 key,table_array 第 col_index_num 列作为 value,插入 Dictionary 中:Dictionary.Add key, value;
  • 查找时只需直接取 Dictionary 内的值 Dictionary.Item(lookup_value),即可完成查找;

若是仅仅 VLOOKUP 一次,倒也不必费劲先建立起一个词典。但当使用同样 VLOOKUP 公式的单元格很多时(比如几万个),就显得其必要了。因为 Dictionary 只需要建立一次,就可以用 O(1) 的复杂度进行多次查找了。

2. VLOOKUP 慢,主要问题不在算法上

从算法角度,词典查找的确快于二分查找,但优势并不是那么明显。所以在具体执行时,我发现使用词典查找的 VBA 宏运行速度并不比 VLOOKUP 快多少,运行时 Excel 仍然会导致系统假死几个小时。按说如此简单的程序不应该那么慢,问题究竟在哪里呢?

经过一段摸索,我才发现问题的根源所在:

  • VBA 往 Excel 表格中填内容时,会引发表格中已有公式的自动计算,非常耗时;
  • Excel 表格内容更新时,会触发屏幕显示内容的自动刷新,代价也很高;

所以提高 VBA 脚本执行性能的关键点,在于计算时关掉公式自动计算和屏幕刷新,这也是我始料未及的。在 VBA 中实现这两点很容易,但由于 VLOOKUP 本身即是公式,我没能想通直接调用 VLOOKUP 时如何避免这两点带来的性能损失。

3. 示例 VBA 代码

在做了上面提到的两次优化之后,原来 VLOOKUP N 个小时才能完成的任务,只用了 7 秒钟就执行结束了。

下面是我写的一段示例代码。我不熟悉 VBA 语言,只是照葫芦画瓢。代码规范程度相差甚远,但题意应是体现其中了。有心的朋友可以用作参考。

Sub 在机器表上生成一级分中心()
'
' 在机器表上生成一级分中心 Macro
'
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False

t0 = Timer
' 词典
Set map_dict = CreateObject("Scripting.Dictionary")

' 打开分中心映射表
Set map_sheet = Worksheets("分中心映射表")
map_nrows = map_sheet.Range("A300").End(xlUp).Row
Set my_rows = map_sheet.Range("A2:B" & map_nrows).Rows

' 遍历分中心映射表,获得 分中心 对应的一级分中心,插入词典
For Each my_row In my_rows
   center = my_row.Cells(1, 1).Value
   city = my_row.Cells(1, 2).Value
   If Not map_dict.Exists(center) Then
       map_dict.Add center, city
   End If
Next my_row

' 打开机器表
Set dispatch_sheet = Worksheets("机器表")
dispatch_nrows = dispatch_sheet.Range("G99999").End(xlUp).Row
Set my_rows = dispatch_sheet.Range("K2:L" & dispatch_nrows).Rows

' 遍历开通表,通过词典获得 machine_id 对应的一级分中心,插入开通表
For Each o_row In my_rows
   center = o_row.Cells(1, 1).Value
   o_row.Cells(1, 2).Value = map_dict.Item(center)
Next o_row

MsgBox "在机器表上生成一级分中心。共处理 " & dispatch_nrows & " 条记录,总耗时" & Timer - t0 & "秒。"

' 销毁建立的词典
Set map_dict = Nothing

' 打开自动计算和屏幕刷新
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
'
End Sub

最后补充一点:我先实现的词典查找,后发现性能问题根源,所以未能去比较 VLOOKUP 与词典查找两种方式的具体性能差异。我想如果差异可以忍受,那么直接在 VBA 中调用 VLOOKUP 公式或许是一种更为简单的实现。

Python操作Excel

老婆单位有时候有一些很大的 Excel 统计报表需要处理,其中最恶心的是跨表的 JOIN 查询。他们通常采取的做法是,把多个 Excel 工作簿合成一个工作簿的多个表格,然后再跑函数(VLOOKUP之类)去查。因为用的函数效率很低,在 CPU 打满的情况下还要跑几个小时。

然后我就看不过去了,我也不懂 Excel,不知道如何优化,但我想用 Python+SQLite 总归是能够实现的。于是就尝试了一把,效果还不错,一分钟以内完成统计很轻松,其中大部分时间主要花在读 Excel 内容上。

1. Python 操作 Excel 的函数库

我主要尝试了 3 种读写 Excel 的方法:

1> xlrd, xlwt, xlutils: 这三个库的好处是不需要其它支持,在任何操作系统上都可以使用。xlrd 可以读取 .xls, .xlsx 文件,非常好用;但因为 xlwt 不能直接修改 Excel 文档,必须得复制一份然后另存为其它文件,而且据说写复杂格式的 Excel 文件会出现问题,所以我没有选它来写 Excel 文件。

2> openpyxl: 这个库也是不需要其它支持的,而且据说对 Office 2007 格式支持得更好。遗憾地是,我经过测试,发现它加载 Excel 文件的效率比 xlrd 慢 3 倍以上,内存使用在 10 倍以上,于是就放弃了。

3> win32com: Python Win32 扩展,这个库需要运行环境为 Windows+Office 对应版本。由于 Python Win32 扩展只是把 COM 接口包装了一下,可以视为与 VBA 完全相同,不会有读写格式上的问题。尝试了一下用 win32com 读取 Excel 文件,效率还是比 xlrd 慢一些。

由于读取效率上 xlrd > win32com > openpyxl,所以我自然选择了 xlrd 用来读取统计报表;而最终输出的报表格式较复杂,所以选择了 win32com 直接操作 Excel 文件。

2. Python 里的关系型数据库

SQLite 是一个非常轻量级的关系型数据库,很多语言和平台都内置 SQLite 支持,也是 iOS 和 Android 上的默认数据库。Python 的标准库里也包含了 sqlite3 库,用起来非常方便。

3. 用 xlrd 读取 Excel 并插入数据库样例

如果数据量不大,直接用 Python 内部数据结构如 dict, list 就够了。但如果读取的几张表数据量都较大,增加个将数据插入数据库的预处理过程就有很大好处。一是避免每次调试都要进行耗时较长的 Excel 文件载入过程;二是能充分利用数据库的索引和 SQL 语句强大功能进行快速数据分析。

#!/usr/bin/python
# -*- coding: gbk -*-

import xlrd
import sqlite3

# 打开数据库文件
device_city_db = sqlite3.connect('device_city.db')
cursor = device_city_db.cursor()

# 建表
cursor.execute('DROP TABLE IF EXISTS device_city')
cursor.execute('CREATE TABLE device_city (device_id char(16) PRIMARY KEY, city varchar(16))')
 
# 打开 device 相关输入 Excel 文件
device_workbook = xlrd.open_workbook('输入.xlsx')
device_sheet = device_workbook.sheet_by_name('设备表')

# 逐行读取 device-城市 映射文件,并将指定的列插入数据库
for row in range(1, device_sheet.nrows):
   device_id = device_sheet.cell(row, 6).value
   if len(device_id) > 16:
       device_id = device_id[0:16]
   if len(device_id) == 0:
       continue
   city = device_sheet.cell(row, 10).value
   # 避免插入重复记录
   cursor.execute('SELECT * FROM device_city WHERE device_id=?', (device_id,))
   res = cursor.fetchone()
   if res == None:
       cursor.execute('INSERT INTO device_city (device_id, city) VALUES (?, ?)',
                      (device_id, city))
   else:
       if res[1] != city:
           print '%s, %s, %s, %s' % (device_id, city, res[0], res[1])
device_city_db.commit()

4. 将结果写入 Excel 文件样例

使用 win32com 写入 Excel 的时候要注意,一定要记得退出 Excel,否则下次运行会出错。这需要增加异常处理语句,我这里偷了个懒,出了异常后要手动杀死任务管理器中的 excel 进程。至于 win32com 中类的接口,可以从 MSDN 网站查阅。

import win32com.client as win32
import os
excel = win32.gencache.EnsureDispatch('Excel.Application')
excel.Visible = False
# 貌似这里只能接受全路径
workbook = excel.Workbooks.Open(os.path.join(os.getcwd(), '输出.xlsx'))
month_sheet = workbook.Worksheets(1)
# 计算文件中实际有内容的行数
nrows = month_sheet.Range('A65536').End(win32.constants.xlUp).Row
# 操作 Excel 单元格的值
for row in range(5, nrows-4):
   month_sheet.Cells(row, 1).Value += something
# 保存工作簿
workbook.Save()
# 退出 Excel
excel.Application.Quit()

体验海淘-Google Nexus 7

一直想买个安卓 Pad 玩玩,正好 Google 家出了 Nexus 7,很眼馋。但遍览淘宝代购,发现 8G 版最便宜的还要 1670,排队者甚众。仔细算了一下,觉得代购有点儿黑。本来价格比算上关税、运费都要贵,还不是现货,需要预付款排队,不定得等多少天。于是就抱着体验的想法,直接海淘了一把。

海淘肯定得看攻略,Nexus 7 的攻略虽然不多,但也足够了。其实难选的无非是两点:一是在哪里买,除了 Google Play,还有其它网上商店,各有优劣;二是用哪个转运公司,这个也是各有优劣,都得看前人的经验。

我比较懒,转运公司选择了用的比较多的 CUL 中美速递。注册以后会给两个转运地址:一个 CA 加州的,一个 DE 特拉华州的。地址中有个人识别码,在网站下单后送到该地址的商品会被记录到个人的名下,并转运回国内。

我想买 8G 版,只能选择 Google Play 商店并且交 13.99$ 的运费。由于 Google Play 限制销售地域,我就挂了个美国的 SOCKS 代理,在 Google Wallet 顺利用一张 Visa 信用卡配合 CUL DE 的地址下单,没有被砍单。下面就简单用时间记录一下海淘过程,都转换为北京时间:

2012-07-25 21:19:Google Play 下单成功,Gmail 信箱收到订单收据;
2012-07-26 13:23:Gmail 信箱收到发货通知,拿到 UPS 追踪号码,于是去 CUL 页面填写货物预报。
2012-07-26 14:25:填写货物预报时发现收货地址写错(街道地址填成了加州的 914 Ajax Ave),在 Google Play 提交请求修改 UPS 包裹收货地址街道部分;
2012-07-27 02:17:UPS 显示包裹离开 Louisville, KY;
2012-07-27 05:56:收到客服响应,同意修改 UPS 包裹收货地址,但要求提交完整的地址,并说明只能尽力帮忙修改;
2012-07-27 08:03:提交完整地址给 Google 客服;
2012-07-27 08:14:Google 客服帮忙完成了 UPS 包裹收货地址的修改,UPS 追踪显示地址已修改;
2012-07-27 23:05:UPS 显示包裹已完成配送;
2012-07-28 09:00:登陆 CUL 发现包裹已入库,重2磅,于是提交订单,给了5元小费,支付宝付款,然后订单状态就一直显示为处理中;
2012-07-31 09:00:登陆 CUL 发现总算发货了,订单中能找到 EMS 追踪号。但我傻了吧唧地去 EMS 网站上查了两天,都没有查到追踪信息;
2012-08-02 21:00:总算发现早期那个追踪号应该在 UCS 网站上查,查了一下,是7月30日 10:26 发货的,看来包裹在 CUL 仓库里呆了两天。
2012-08-06 11:09:接到快递电话,Nexus 7 到手。

由于中间回了趟家,也没有预计到转运到国内速度会那么快,于是2日到6日之间一直没有关注包裹的追踪情况。到手之后查了一下 EMS 网站,才发现转运的速度还不算慢(EMS的时间貌似都是本地时间):

2012-07-29 22:26:00 UC 纽约 到达处理中心
2012-07-30 21:00:00 UC 纽约 离开处理中心,发往中国 上海
2012-08-04 07:32:00 上海邮政速递物流大宗邮件收寄处 收寄
2012-08-04 10:51:09 上海邮政速递物流大宗邮件收寄处 到达处理中心,来自上海邮政速递物流大宗邮件收寄处
2012-08-04 11:30:00 上海邮政速递物流大宗邮件收寄处 离开处理中心,发往上海市邮政公司邮政速递局
2012-08-04 14:04:11 上海邮政速递物流大宗邮件收寄处 离开处理中心,发往国内出口
2012-08-04 16:34:00 上海市 到达处理中心,来自上海邮政速递物流大宗邮件收寄处
2012-08-04 16:58:00 上海市 离开处理中心,发往北京市
2012-08-05 09:27:41 北京市 到达处理中心,来自上海市
2012-08-05 12:58:42 北京市 离开处理中心,发往北京邮政速递上地分公司上地营运部
2012-08-05 15:10:50 北京邮政速递上地分公司上地营运部 到达处理中心,来自北京市
2012-08-05 15:54:48 北京邮政速递上地分公司上地营运部 安排投递
2012-08-05 16:14:00 北京邮政速递上地分公司上地营运部 未妥投
2012-08-06 07:26:37 北京邮政速递上地分公司上地营运部 安排投递
2012-08-06 11:37:00 北京邮政速递上地分公司上地营运部 投递并签收

最后总结一下几点经验:

  • 全部时间:11天14小时
  • 全部费用:199$(8G版)+13.99$(美国国内运费)+ 75¥(CUL转运费) + 5¥(CUL小费),整体算下来比淘宝代购便宜 200+
  • 信用卡:工商银行信用卡网银支持开通/关闭境外无卡支付功能,支付前开通,扣款完关闭,海淘比较安全
  • CUL 服务:比较差,包裹入库、发出没有邮件通知,只能人肉登陆去看;QQ 客服常年离线;微博官方账号无响应。但从整个转运结果来看,也还算靠谱。更新:最近很多人抱怨CUL不靠谱,所以我这里也不建议大家用了。
  • CUL 羊毛:邀请用户注册,下单付款后,邀请人和被邀请人都能获得 20 积分,以后下单能抵 20 元运费。我的 CUL ID 是 solrex,也可以直接点击这个邀请链接注册,你懂的!

真是大悲大喜的一天

家家有本难念的经,生活中有很多事都是不足与外人道也。但我还是想说一说今天的经历,谈一谈我的懊悔,警示自己和别人。

我妈妈两年前的7月做了乳房切除术以治疗乳腺肿瘤。后来的这两年里,我主要关注她的术后恢复情况。因为术后恢复还算不错,所以去医院复查这样的事情我只是口头上催促催促她。

我的妈妈是一个出身于农村的普通妇女,虽然工作是小学教师,也调到县城工作,但生活经验和圈子都局限在一定范围内。再加上父亲去世后拉扯两个孩子生活比较困难,更是把自己的物质欲望压缩到最小范围。对于她而言,去医院看病是一件费力费钱且让人心生恐惧的事情。也是为了省钱,她的乳腺增生才拖成了肿瘤。

对这样的妈妈,口头上催促去复查总会变成不了了之。最近,我意识到这一点的危险性,于是专门请假回家,除了看望她,有一项重要的计划是带她去医院做术后复查。

不出所料,她依然是各种不愿意。但我还是说服她踏进了医院的大门。上午的检查很顺利,找到了原来的主治医师,前面几项的检查结果也不错,但问题出在了胸透检查项上。胸片显示胸骨左侧第四根肋骨处有个阴影,我看到胸片结果的第一眼忽然觉得胸闷难受,眼睛发红。家里有过肿瘤病人的,都应该知道阴影是一个多么恐怖的兆头!

因为医生中午不在,怀着沉重的心情回到家里,说话、吃饭,有说有笑,但都盖不住心中的担忧。担忧在下午看过医生,被要求再做CT时发展到表面。我跟妈妈说如果检查结果不好,那么明天就开始准备手术,两个月后我的婚礼也不举行了,让她好好养病。等待CT结果期间,我妈妈几分钟就想起来去取片室看一下好了没,那45分钟的等待真是煎熬,我也做好了最坏的准备。

但喜剧的转变发生在CT结果拿到后,CT显示胸部无任何异常。拿照片给主治医生判读后,也说没有任何问题。看到结果的那一刻,我的眼泪差点儿飙出来。虚惊一场,真好!!!

我真后怕,真怕是因为我的不果断导致妈妈拖延复查延误治疗。我也真庆幸,庆幸这一切只是虚惊一场。其实妈妈的胆在我身上,而不是在自己身上。在没见到我时,她不敢动手术,不敢去检查。只有我在她身边陪着,这生活的坎儿才没那么难迈。我一直知道这一点,却才发现自己没能做好。

父母对儿女是最无私的。妈妈退休后到病倒之前,还去私立学校教书,为了几百块钱早起晚睡。虽然她说自己是闲不住,但我知道她是想多攒点儿钱给我买房子。我一遍一遍给她讲,我不需要她的钱,也不希望她辛苦,但直到病倒也是不听。她固执,是出于对我的爱,我却没能固执地让她去做爱护自己的事情。

现在,我会一遍一遍地跟她说,我能挣多少多少钱,我有多少多少存款。我想让她知道,我现在能够支撑起我的小家,这个大家。不要再为钱的事情发愁,我很能挣钱,也不要再怕去医院,我会陪着。我想要的,只是她能开开心心、健健康康!

厦门-上海-苏州闲游图片

接上篇博文《厦门印象-文字版》,这是 6 月份的行程。早就想挑些照片出来,但总缺乏一些动力,导致延了这么久。

继厦门之后,中间途经上海,看望了一下老婆的好朋友。可惜天公仍不作美,在上海待的一天多下了好大的雨。仿佛我们走到哪里,雨就下到哪里。于是收拾一下,第二天就转向苏州了。

随着年龄的增长,我逐渐能领略建筑和园林的美,不再像以前只爱美丽的自然风光。本来计划在苏州主要是逛园林,但老婆忽然想到苏州的婚纱便宜,于是在苏州的婚纱市场花了一天的时间。加之我们都爱睡懒觉,在苏州就只花了一个下午看了苏州博物馆。实话说我感觉非常遗憾,但留点遗憾也好,有个下次再来的理由。

我们两个不算旅行者,倒像是换个城市生活。预先并没有做好计划,还以各种庸俗的原因修改计划:为了躲雨提前离开厦门,为了躲雨提前离开上海,忽然起兴去购物,为了躲端午节人流离开苏州,每天上午都浪费在睡梦中。留给看风景的时间太少,的确有些遗憾。但这样的安排感受到的自由度更大,倒也不算后悔。

照了很多照片,挑出来几张,贴在下面: