2012年5月25日星期五

ConcurrentDictionary的性能问题

最近在写一个小玩意儿,有并发的要求,并且吞吐量巨大,因此需要使用.NET 4出现的并发库,其中最常见的莫过于ConcurrentDictionary这个类了。但是在使用的过程中发现存在一些潜在的并发问题,这类问题会导致系统性能急剧下降。

我们在使用ConcurrentDictionary的时候,可能会有类似下面的代码:

// users的key是用户名(字符串),value是该用户的复杂信息对象。
// 其中用户名如果以"vip:"开头,则是vip用户。
// 于是我们可以用这样的代码获得所有vip用户的用户名。
var vipUserNames = from userName in users.Keys
               where userName.StartsWith("vip:")
               select userName.Substring(4);

在上面的这段代码里面,Keys属性的访问会导致并发性能问题。该属性是通过调用一个GetKeys()函数来获得的,而GetKeys函数则会首先通过AcquireAllLocks来锁定内部的所有锁,然后在这一锁的保护下,取出所有的Key值。正是这一个操作导致了一定程度的互相竞争。要阐明该问题及其原因,需要首先简单的说一下ConcurrentDictionary的并发设计思路。

一般而言,如果我们自己写并发字典,可能会尝试通过简单的使用lock来在读写的时候进行同步锁定。然而这种实施方式会导致在大并发的时候出现互相等待的情况,甚至最终退化成单线程处理的状态。稍微好一点的ReaderWriterLockSlim,在读操作的时候共享读阻塞写,而写操作则阻塞所有操作。这种设计在大量读少量写的时候也不失为一种不错的设计。但是不可避免的是,每一个线程的操作都会导致一次锁请求,而且说不定就会被阻塞。甚至面对大量写操作的场景,则会ReaderWriterLockSlim对写操作的排他性而互相等待,最终退化成单线程处理的状态。那么ConcurrentDictionary又有什么高招呢?

ConcurrentDictionary是这么设计的:通过将桶进行分组,每一组使用一个单独的锁对象。这样,在大量的写操作面前,也会因为写操作对象的不同而进入不同的组,互相之间被锁定的可能性就大为减少了。另一方面,其读操作被设计成为多线程操作下无需锁同步也是安全的,因此读写之间不需要互相阻塞,也不需要获取任何的锁。于是,其吞吐量通常会较一般设计要好得多。

然而,Keys属性的设计却导致并发性能的降低。因为AcquireAllLocks会锁定所有的锁,这个时候所有的写操作都会被阻塞。可是为什么要AcquireAllLocks呢?于是有人会猜测:之所以需要锁定,是因为任何的写操作都可能修改内容,导致最终结果和那一瞬间的情况不一致。然而这类的想法其实是错误的,因为:
1、线程安全问题是指,在多个线程同时操作的时候,程序不能出错(死机死循环或者什么也不干),数据一致性不违反你的期望(比如你的操作是增加,但实际上没有,甚至变成清除所有);
2、在高并发的情况下,“某一瞬间”的定义还真不好说清楚,因此仍然只能够从逻辑上来说是否违反你的预期。

如果你仍然无法接受上面那两句话,那可以继续考察一下ConcurrentDictionary的枚举操作,这一操作依赖于GetEnumerator返回的枚举器。如果你真的去看,就会发现ConcurrentDictionary所返回的枚举器并不会加锁。它实际上和普通的TryGet没有什么两样,是一种不需要锁的线程安全写法。于是,最开始的那一段代码,用下面的方式来写,就不会出问题了:

// users的key是用户名(字符串),value是该用户的复杂信息对象。
// 其中用户名如果以"vip:"开头,则是vip用户。
// 于是我们可以用这样的代码获得所有vip用户的用户名。
var vipUserNames = from userItem in users
               let userName = userItem.Key
               where userName.StartsWith("vip:")
               select userName.Substring(4);

嗯……真的没有问题了?其实还是会有的,比如说,如果我需要按照用户名来进行排序:

// users的key是用户名(字符串),value是该用户的复杂信息对象。
// 其中用户名如果以"vip:"开头,则是vip用户。
// 于是我们可以用这样的代码获得所有vip用户的用户名。
var orderedUserNames = from userItem in users
               orderby userItem.Key
               select userItem.Key;
你猜现在会调用AcquireAllLocks吗?看上去会使用ConcurrentDictionary.GetEnumerator,实际上不然。其实orderby会根据待排序的对象是否为一个ICollection而区别对待,如是,则调用ICollection.CopyTo(array, 0),否,则使用IEnumerable.GetEnumerator()然后MoveNext()。如果你直接orderby,由于ConcurrentDictionary存在ICollection接口,于是会调用CopyTo,而CopyTo也和Keys对象一样,会存在一个AcquireAllLocks的调用,类似的地方还有Values属性。

除了上面的这些问题外,ConcurrentDictionary还有另一个会造成死循环的场景,不过这不是本文的主题了,留待以后再谈。

2012年5月15日星期二

关于南航CZ3525航班急坠失重乘客受伤的猜测

近期南航CZ3525发生一起准事故(不是只有解体坠毁才算是事故,按照这里的解释,需住院48时人数达10人及以上就算一般事故),新闻中并未详细说明受伤人数及程度。仅仅提及有9名乘客受伤,其中2人骨折,其余7人擦伤。另外似乎至少有一名乘务长,和一名刚入职不满一年的乘务员受伤,前者腰部损伤轻重不详,后者貌似轻微擦伤。

在这一起准事故中,据分析称当时高度7000-8000米左右,尚未进入平流层。并且当时天气晴朗,事发突然。根据分析中的说法,这叫做“晴空颠簸”。有旅客说,自由下落数十秒,这基本上不太可能,属于过度夸张。要知道20秒自由下落可以下降近2000米,30秒则可达到近4500米。以本人乘坐飞机时遇到的接近失重2秒经验,部分旅客已经开始尖叫。若失重10秒,估计会各种心脏病,狂哭直至旅途结束。有旅客质疑是否机师操纵不当造成,分析中的专业人士则指出:“飞机在空中飞行的大多数时间都是通过自动驾驶完成,并不是人工操纵。因此,颠簸的感觉和程度跟飞行员自身的技术并没有太大关系。有人举例说,空气和海水一样都是流体运动,飞机遭遇的颠簸与行船中的颠簸道理一致。”要了解这个问题的真实答案,可能需要先了解一些“专业”知识。

首先是“晴空颠簸”,其实这个是一种晴朗天气下的“下击暴流”。所谓下击暴流,就是一种向下(地面)垂至吹出的气流,在遇到地面之后向四周吹去,并形成涡流(像蛋卷一样一层层向内卷),如下图所示:
触地阶段外吹阶段翻起阶段


当飞机从外围超下击暴流中心飞行并穿越的时候,就会出现不平稳甚至引发严重事故的飞行状态。刚开始的时候因为接触的是上下翻滚的乱流,可能会出现一些上下振动颠簸的情况,此时可能并没有明确的单一方向运动。然而在接触到平吹的风时,由于风向从机头吹向机尾,相当于飞机相对于空气的速度增加了,此时获得的升力也会提升。这时候你应该会感觉到一小段时间是像坐电梯上行时那种被往下压的感觉,这时候你应该尽快帮好安全带,尽可能抓好未被固定且可能导致伤害的物体,例如iPad。这时因为很快这股气流就可能开始从水平方向发生向下的偏转,直至垂至向下。在偏转至垂至向下之前也就是以某角度斜向下时,会出现向下的力量和水平逆风引起的升力相抵消的情况,在这一刻风所引起的垂直力(或者说加速度)为零,也就是感觉“平稳”。但这种平稳是瞬间的,因为此后风向会继续偏转直至向下,也就是说此后将会感受到向下的加速度。

根据风阻公式:

F_D\, =\, \tfrac12\, \rho\, v^2\, C_d\, A,
其中
 \mathbf{F}_D  为阻力
 \mathbf{} \rho  为流体密度
 \mathbf{} v  为流体相对于物体的速度
 \mathbf{} C_d  为阻力系数,为一无因次的参数,像汽车的阻力系数约为0.25至0.45
 \mathbf{} A  为参考面积


按照波音777的数据推测,其投影面积 \mathbf{} A 大概为800m2左右,微下击暴流的速度 \mathbf{} v 可以达到270km/h以上(75m/s),而7000米处高空的流体密度(第16页) \mathbf{} \rho 大致为0.59kg/m2。由于飞机投影面积中的一半为近乎平板的机翼,另一半为近乎圆柱体,因此阻力系数 \mathbf{} C_d 取0.95,于是阻力 \mathbf{F}_D 约为1,261,125牛。

而根据升力公式:


L = \tfrac12\rho v^2 A C_L
其中
  • L 为升力,
  • ρ 为流体密度
  • v 是空速,
  • A 为翼面投影面积
  • C_L 是在特定攻角,马赫数以及雷诺数下面的升力系数
这里A大概为400m2,巡航速度v则为905km/h左右(约250m/s)。而C_L则比较难获知,在此高度与巡航速度下,并且假设下降气流没有导致翼面气流分离的问题,则可能的升力系数猜测为0.5左右,因此L=3,687,500牛。但是实际上如果要保持平飞,则不需要如此高的速度。此时所需要的相对空气速度为688km/h(191m/s),若此时吹75m/s的逆风,则此时只需要大约116m/s的速度即可。实际上飞机并不会减速这么多,而是同时通过调整翼面降低升力来保持平衡。


由于不知道该飞机的具体型号,很难算出准确的重量。根据广州飞上海这个信息提示,可以知道不需要携带太多燃油,估计重量约为220吨,也就是大约2,156,000牛。换句话说,当时的极限升力足以应付自身重量和下垂气流的合力。在这种时候为了保持平飞,大约需要886km/h(240m/s)的相对空气速度。由于前面经历了逆风阶段,飞机的自动驾驶系统为了保持飞行平稳,会减少引擎输出推力,并且通过调整飞行面来减速。此时相对地面速度,比如说已经减至160m/s,于是可见离所需速度相差不少。速度并不是瞬间就能够提上去的,因此更多的是通过调整攻角、襟翼等参数来提高升力。


但是不需要太久,就会从中心区穿出到达顺风区(微下击暴流的影响范围直径也就3至4公里,中心区直径也许就几百米的直径,大概就3至4秒就会穿出)。此时如果保持普通平飞参数,则需要266m/s。由于实际速度尚无法提升至足够的水平,即便通过调整攻角等参数来提升升力系数,也不能保持足够的升力,于是就会出现向下加速的情况。


暴雨天气的下击微暴流


正是因为如此,一般飞机遇到不稳定气流,都会尝试绕过。而这种气流的发现,可以通过对雷达反射的水气进行相对运动分析得到,而一般则是直接避开云层。但是对于晴朗天气状态,没有可以反射的水气,于是会无法提前预知,而经历必然的颠簸过程。需要注意的是,一般飞行电脑都会尝试通过调整升力系数和引擎输出来减少这方面的波动,所以一般应该比手动驾驶为舒适。实际上如果您遇到过乱流,一定会听到引擎在这个时候是提高输出而不是减小输出的。换句话说,在进入逆风区遇到乱流的时候,并不会选择降低相对地面速度,而应该是提高,同时通过调整升力系数来减少升力。


而如果是手动驾驶,再遇上经验不足的飞行员,则可能在进入逆风区的时候因为看到空速大幅攀升,而选择减小引擎输出并降低空速来减少升力,就会在后面遇到升力因为空速关系严重不足的情况。比如前面如果真的将相对地面速度减少到116m/s的话,即便在中心区加速至175m/s,在后面进入顺风区时,空速仍然直接下降至不可思议的100m/s,直接就失速了。一旦时速,必须压低机头,通过重力势能重新建立足够的速度。而如果继续勉强拉高机头,不但提升不了升力系数,反而会因为翼面的气流分离导致升力系数骤降,下跌更加严重。同时拉高机头导致的风阻增加,也会阻碍速度增加。最后严重的会进入螺旋或者尾旋,失去挽救的机会。航空界曾经有过不少这样的案例:华航有一架飞机就是机师拉机头导致失速坠毁的,实际上如果松开拉杆由飞行电脑控制,则会自动压低机头提高空速恢复足够的升力。但是压低机头这种方式会导致快速的下降,也就是失重。


微下击暴流实景




至于此航班当时的操作状态,现在并不清楚。但是我们可以假设这几种情况:
飞机自动驾驶,没有限制空速(油门),同时机师不进行任何干预,则如果飞行电脑程序没问题,应该会选择加速减升力,最后顺风区时空速应当足够,不会长时间处于向下加速的状态;
飞机自动驾驶,锁定油门,同时机师不进行任何干预,则可能会颠簸严重一些,顺风区空速相对前一种情况会低一点,顶多是下降时间长一些,也不会太严重;
飞机自动驾驶,在顺风区由于显示空速较低,同时升力不足,机师选择加速拉机头,可能会导致速度增加较慢,恢复平稳所需时间变得更长;
飞机自动驾驶,在逆风区由于显示空速较高,机师选择减少油门并减少空速,最后会在逆风区遇到空速严重不足,此时所需时间就会变得很长,如果再加上拉机头,后果可能会很严重。


当然了,前面的分析基本上可以当我胡诌,因为本人非专业人士。但是有一点可以确认,如果你遇到突然往上升的感觉时,说明已进入乱流甚至微下击暴流得影响区域,赶紧系好安全带是一个保本的选择。毕竟你不知道机师会如何选择,比如降速、拉机头,千万别到了后面抛起来了才后悔。

2012年5月13日星期日

思想的代沟

--- 前传 ---

本来不想这么早聊这个话题的,总觉得不合适。可是今天看一微博,说某市名牌中学择校费过亿,校长控着,还说这是一个没有底线、没有道德、没有责任、没有监管的黑企业,就这么教育着我们的未来下一代。这个说法有点问题,比如说择校费也不完全校长说了算,再比如监管当然有——这么肥的差事,能少了教育部一份?记得以前看过一篇文章(按照国内说法,这是一个谣言,嗯,你懂的),这作者传说是某市重点中学的一财务,控制着择校费的小金库。后来因为和校长不和,黑了这笔钱偷偷移民走了。因为这个小金库建立的时候,官方还没有注意择校费的事情,所以不能报官,当然也更不能让民众知道。后来这“择校”的词语成为非常流行的词汇,官方一算发现好大一块肥肉,于是后来打着监管的旗号,要求到官方那边交。学校肥肉没了怎么行,于是又换个名目叫做择班费,原来多少还是多少。当然,这是个传言,我信不信不重要,关键是会不会有人信。

可是这些问题对于不同年代的人想法会全然不同,比如祖父辈的就曾跟我说:千万不要到外面乱说话!后来等我三十而立的时候,这句话的背后含义才向我揭开:那个年代少有不慎的就会被斗死,更何况像那两位老者,一家是乡下大地主,另一家则是城里大地主,在某某滩还小有名声。就这样的背景,在那个年代没有被斗死,就已然是个奇迹了,而他们居然没有被批斗过。当然,他们什么都不敢跟下一代说,于是有一天要填身份的时候问老者,老者答“干部”,结果被那时候的学生追问什么干部?老者再一次给出了神奇的答案“学生就是干部”。然后是写身家背景材料,照样是写得滴水不漏。下一代这时候这才明白是多么恐怖的事情,而她之前还在向往着革命,向往着保卫领导人,向往着打倒一切。这个红色的小卒想要拱过河,杀将那头想象出来的黑帅。结果这时他才明白,这某国象棋里面还有一个游戏规则是红色的车马还可以吃红色的子儿。简直就是走偏一步就会被自己人杀害,然而之前一点也不知道这盘棋会这么危险。当年说这话的老者,已经走了。

我的另外两个老者显然没有那么的明智,导致的结果是我的上一辈还在娘胎里面的时候,就跟着母亲蹲监狱,一出生就分开。最后折磨十多年后,阴阳两相隔,其后还被乱葬。时隔数十年后,终于挑了个良辰吉日,重迁吉地。当时,我上一辈人描述,有七彩云霞飘然而知,盖过漫天烈日。于是整个过程在清凉爽快的环境下完成,仿佛上天有灵。我的上一辈从未有明显的信教历史,更多时候的言辞似乎是无神论者。然而这一描述却无法超脱中国传统的神怪幻想,但于我仍是可以理解的。这两位老者中,得以苟活的其实是当年某纵队的一不小官员,后当某地重要领导。说其苟活,不是肉体遭罪而是精神摧残。记得本人考取大学前一日,他在梦中乱吼乱叫:你个某某党狗贼……我问为何如此,家人告知他一直觉得有特务在追杀,所以从来不透露其生日,到现在大家都不知道他星座为何,这日梦魇大概类似。另一位早已仙逝的,家为乡绅,从未做恶。与此相反,加入革命的红旗时,亲自说服其长辈,亲自取出家中所有金银财宝全部献给党。后来还将除了祖屋和一亩三分地之外的所有地产,全部分给乡民。他是当地加入红旗的第一人,而后来等待她的后果居然是如此不堪。

最近又和另一位无任何血缘的开店长辈聊天,席间我说到近期看到的一个微博,说国内的教育无底线,因为学校本身就没有底线,并深表认同。然而此长辈则表示不认同,他觉得国内是有底线的,并笑称当年他的想法如我一样,现在则转过弯来了。他说,其实对于当官的,最重要的是不能丢了其工作,否则就没有生活来源了。因此对于政府来说,搞到像他那样的企业完全没有必要。虽然他也承认办个企业也有很多不合理的事情,比如税控机的一群人会来揩油,比如说各种的莫名其妙的监管和认证。但他坚持认为,搞这些事情的人,是为了不要出乱子,影响了他的前程。而且再说了,将所有企业都搞死了,钱哪来?所有人都丢掉就业机会,不都会找其算账么?而我却坚持认为,也许不会大面积的主动把企业搞死,但是这证明不了他不会莫名其妙的搞死个别企业,或者说在这个国度里面,这种灾难会不会降临到自己头上,是完全无法预测的。于是他非要我举一些例子,我只好搬出西南某市的例子。他面带威色的说,那些企业一定和某些势力参合在一起了,不是这边就是那边,言下之意是那是他们活该的。我只好笑称“是是”,然后像个办法圆场过去了事。

显然,这位长辈的思想就是和我有极大的代沟。一个可以和金钱纠缠不清的官府,一个可以为钱分裂两派的官府,一个可以不按法理(也就是规矩)出牌的官府,怎么可能对无钱无权的老百姓,突然就不纠缠金钱了,突然就爱民惜命了,突然就法治了?他的不会搞死企业理论,用糙理来说就是:宰相为了自己爽,会随便乱搞天下所有女人。当然,不会都搞死,不然他到哪里去爽呢?再说了,大家都被搞死了,所有男人都来问罪他也受不了。言下之意就是,宰相他来搞搞你的女人,只要不搞死,你就配合一下,说不定还赏你一点钱。只要你女人不死,你还是能爽的。这样的逻辑我是完全无法认同,即便是在你没有能力保全自己,做出强硬的反抗时,你也应该从道义上保护你的女人,还有别人家的女人不受侵害。你可以退一步,表面配合,暗地里弄得宰相很不舒服但又说不出来。如果智商到不了那个地步,你可以再退一步,携家带口离开这个地方。总之,不能让这样的宰相有爽的日子过,至少不能让他这么容易就爽了。配合宰相?我真心做不到,且不说他是不是我的杀父仇人。

其实在这样一个官府里面,我猜只有两种人存在,一种是官府以外的杀父仇人,另一种是认贼作父的不要脸的小人。

以上为纯小说,描述的是苏联时代的某个人。嗯,你懂的。

---- 后传 ----

和长辈聊天的时候,还提到了许多事情,比如移民,比如食品安全,比如教育。他以过来人的身份说了以下几个论点:

  1. 如果有识之士都走了,谁来救国;
  2. 出去之后发现,那是人家的地盘,为什么只有人家地盘才有;
  3. 出去之后还发现,国内挣钱国外花,几乎没有存下钱;
  4. 国内食品安全也没有想象的那么严重。
论点1立论不错,如果他说,我们就是要在这样的环境中,如何如何改变宰相的行为,那我尚且可以认为算是有胆有识之士,可以佩服之。可惜后来一拐,居然拐到如何配合宰相爽的论述过程中去,显然和论点立意矛盾。这让我无力吐槽;

论点2立论也比较有意思,可惜同样的,后来全然没有讨论为什么,也没有讨论如何让这里有,只是在维护着宰相的爽权,替宰相说话。这也没什么好说的;


论点3立论就已经比较偏颇邪乎了。没错,有很多人出去之后是呆不住的,但也有呆住的。我的亲戚有不少就只身闯国外,移民了。以他们当年的文化水平,能力水平,尚且可以生活,何以今日我们就一定不行?况且他还犯了这么个错误:国内挣钱,让老婆国外花,让后代上那边的私立,这自然是存不下多少钱。除非你能力真的很强,这当属凤毛麟角。可是国外的生活一般都是国外工作国外花,当一个平凡而又尊严的人,而不是家产万贯却以舔脚为生。为何非要私立,为何非要荣归故里?平凡而惬意的度此一生不也是一种生活么,不更应该是绝大多数人应该起码享有的权利么?也许前路会有坎坷,但我真无法面对国内的生存环境,我无法想象让后代生活在这种无时无刻不在的恐惧中,也就是他的论点4。

论点4基本上我完全无视,况且他自己也承认空气确实很差。我不知道一个空气很差,还遮遮掩掩,找茬赖美帝国主义的官家,如何能大大方方承认食品的问题,然后认认真真地整治。基本上国内的所有处理这类问题的思路,都是:先盖着,然后再盖着,实在盖不住了,终于盖着了。从某种典型性疾病,到各种轻度追尾,大头娃娃,某种捐献体液卖钱然后得了某种病而严重威胁整村人生命的事情……我都懒的提起,大概会被选择性的屏蔽。最近又有药壳子时间(包括果冻、某种酸奶……),基本上我们已经基于麻木而“免疫”了。

他说:吃那么一点不会得癌症,自己注意一点到卫生一点的地方就餐,没事的。你看,那么多外国人在中国,他们不也没事么?

啊,这是多么“强大”的论述啊!那咱先说说自来水的问题,自来水不合格都知道吧?那再说说特供食品的问题,如果说一般食品不存在问题,为何要搞昂贵的特供?再说说老外的问题:你难道是说要我们赚老百姓的钱,吃老外价格水平的东西?国内货币如果按照住房价格参照,相当于澳大利亚元的1/2.5(同样工种,国内300个月税前可买一套住房,澳大利亚则是120个月),参考其它非食品因素基本也不相上下。而澳大利亚在外就餐,一顿饭人均约30已是相当好的了,按照前面的购买力平价,大约相当人民币75元。在国内吃个稍好一点的,人均50-60很常见。可我真心不知道,那些50-60的餐厅会否有地沟油,谁可以保证这一点?或者你一顿多少钱可以没有地沟油、重金属、毒农药、染色剂、工业明胶?75?那我还不如去澳大利亚生活呢。

关于癌症,那真是站着说话不腰疼。癌症这玩意儿以目前的科技,根本无法预测,但是肯定和环境有关。据说某市有很多个环,越往里癌症率越高。有一次我参加某饭局,局中一人做保险,谈及这些数据,大家都瞠目结舌。然后还说到认识的某A如何癌症挂掉的,某B又是怎样在怀孕的时候发现得癌症,小孩出生没一个月就走了,以后这个小孩体内有原癌细胞会很高等等。最后这个话题以尴尬的平静,以及突然的转移话题而结束。谁都不愿意提这样的事情,因为大家都知道自己中招几率有多高。本人也曾经腹部不适,虽然目前诊断说是没事,但当时的恐惧可以说是空前的。你不得不思考,这样的饮食和消化道癌症有多大的关系;你不得不思考,这样的空气与呼吸系统的癌症有多大的关系;你不得不思考,这样的工作环境与其它类型的癌症、心脑血管疾病有多大的关系;你不得不思考,我怎样能够降低这样的概率,尤其是你小孩遇到这种问题的概率。

如果说,前传谈到的是无耻者无畏,后传则是无知者无畏,合起来就是无耻无知者大无畏。

当然了,后传说的是英格兰某时期的事情。嗯,你,也是懂的。

2012年5月8日星期二

那都是些什么牛人啊……

终于翻译完狭义相对论发现史了,共计约117kb……

物理学这玩意儿还真不是一件容易的事情,你不仅仅要懂物理,还需要懂数学的一堆乱七八糟的东西:从流形到群论,从平面到立体然后N维,什么标量适量,欧几里德空间,非欧几里德空间……而且你还要有足够的想象力:为毛光速就是恒定的呢?光速恒定,且光速不取决于光源速度,那么时间会发生什么样的变化。

关于相对论的建立,我们来回来的看到的都是一些巨人,他们从我们这个莫名其妙的世界中寻找出各种线索,然后猜测假设以至最后给出了正确的公式。他们是:麦克斯韦、洛仑兹、庞加莱、朗之万、索末菲、普朗克、闵可夫斯基,以及大名鼎鼎的爱因斯坦。

之所以说物理学这个玩意儿不容易,还有一个原因是,我们可以看到有一些已经参与相对论类似理论研究的物理学家,却因为种种原因一次又一次的提出了错误的理论,甚至这些理论最后是自相矛盾的、不完整不一致的,比如说亚伯拉罕。在翻译的过程中,一次又一次的看到他的出现,不过几乎每次都是跟着一句话“……错的”。唉,感情基本上在这里充当了绿叶或者龙套的角色,就是要衬托出前面那些人的强大。

翻译过程中印象最深刻的是这三个人:洛仑兹、庞加莱还有闵可夫斯基。实际上爱因斯坦的广义相对论,包括之前所有人对这一类似理论的所有尝试,都是在讨论如何让洛仑兹发明的洛仑兹变换符合(伽利略提出的)相对性原理,而庞加莱则是在爱因斯坦出现之前,所有提出类似理论的人当中,最为正确的一个。事实上,庞加莱的公式基本上已经和相对论没有多大差别了,只不过有这么两个问题:
太过执着于电磁世界观,导致没有想到这是一个真实世界的运动学问题;
没有放弃以太概念,导致仍然有“真实的”和“表观的”数据量之分,同时静态以太这种概念也导致了特殊参考系的问题,对于解释相对性原理不利;

其实,爱因斯坦只不过在他们的基础上做了很简单却很关键的那么几件事情:1、坚持相对性原理而放弃了以太;2、假定光速对于任何观测者而言都恒定。剩下的部分可以看作把庞加莱的x'、x、t'、t等两组概念,合并成x、t等一组概念。可以说庞加莱和爱因斯坦之间的成果,从数学形式上说差异真的很小,可最后的结论却是由比较大的差异。(我都感觉为庞加莱唏嘘不已。)

至于闵可夫斯基,其实真的很牛,他把这个物理理论彻底的从电磁学的范畴解放出来,转变成一种时空概念,比如世界线之类的。实质上狭义相对论确实是一个时空上面的问题,通过几何的方式来展示确实是更加直观和更加有效的,也更加贴近这一理论背后的真实含义。连爱因斯坦最后都不得不承认其工作的重要性,甚至他后来的广义相对论也是在闵可夫斯基的时空模型,及其相关的数学工具的基础上进行的。可惜闵可夫斯基属于数学空间方面的专才,对于物理意义的理解尽管也算一等一的高手,但和超一流高手之间就还是有那么一点差距。尽管他也提出了一些一统理论,但最后广义相对论的奖牌最终还是被爱因斯坦拿去了。

看那个时代的历史,真是感觉太壮观了,那么多的物理界高手在那么短短的半个世纪内发展出这么强大的理论,以至于所有后续的理论基本上都在这一基础上前行。说到这里,不得不提一个非常出名的照片:


这里面的人都是一群什么样的人呢?这里面的一共有29人,其中的17人获得了诺贝尔奖,17人……或者可以这么说,如果当时正好一个炸弹落在会场,估计我们今天就活在20世纪30年代了。没有网络,没有计算机,没有液晶显示器,没有……

2012年5月4日星期五

狭义相对论发现史翻译有感

最近因为看了一期NGC的Beyond the cosmos,再次对相对论、量子力学等一系列知识产生了兴趣。尤其是想到这些理论中的内容是多么的违反直觉,同时却又有无数的实验证明它们是对的,于是又整天开始想这些无聊的问题了。

关于电磁波

记得高中光学讲电磁波的时候,估计都会听过波粒二相性的说法,当时听课的时候真的不知道怎么去理解。当然,现在算是知道了,其实科学界早已抛弃这种说法,转而支持“几率波”的说法。或者说,光波并不是一种机械波,而是光子到达某处的概率有多少的一种几率波。所谓波粒二象性,只是当时人们无法脱离机械波的条框来看到光波所造成的幻觉:一方面双缝实验证明光具有波的特性,另一方面又存在胶片上面确实可以录得诸如一个光点这样的事实。正是因为这种错误的假定,使得人们开始寻找光“机械波”的载体——以太(当然,还有其它的原因)。事实上,后来发展出来的量子力学便是基于“几率波”这一假设,然而根据该假设所推导出来的众多理论,是非常的违反直觉的,比如量子纠缠。当时这个现象让爱因斯坦一度对量子力学嗤之以鼻,他认为这只是一种数学幻觉。没错,除了相信量子力学这个“邪教”的一小撮“不明真相的群众”之外,大部分人都无法理解这一个学说,或者说无法接受。甚至还有人因为无法接受这种理论,而去设计一些实验来尝试证明量子力学是错的。这些实验最后无一例外的得出了非常让人高兴的结果——量子力学是对的。(操,有什么值得高兴的,应该沮丧才对吧。不,因为目前为止没有比量子力学的能够更加准确预言世界的物理理论了,证明它是失败的才让人沮丧呢。)其中一个实验就是证明量子纠缠的,关于这个实验的解释,回头再补充吧。嗯,重要的是,当时爱因斯坦说过一句话:“上帝不会掷色子”,而波尔回应道“不要再告诉上帝他应该干什么”。而这场纠纷最后居然是波尔赢了。没错,伟大的爱因斯坦,搞出相对论和核弹的爱因斯坦,居然输了!真让我感到悲伤。

关于光速

说到电磁波,就不得不说光速了。这个玩意儿太神奇了——“对于所有观测者而言,不存在任何物体的速度能够超越光速”,基于这一个假设,后来推导出了更多违反只觉得东西:运动物体的时间会发生膨胀(无论方向如何),长度会收缩(在运动的径向上),质量会变大(在运动方向的横截面上)。后面的推论就算了,光是想下面这个问题,我就表示无法想象结论为何:
一个位于地球1光年外的X星球,以近乎光速的速度靠近地球,上面的X星人朝着地球发出一个近乎光速的电子。请问,最后地球人与何时,分别遇到了哪一个时刻的X星球和X星人发出的近乎光速的电子?

发现历史

其实,这些发现也是一步步搞出来的。就拿牛顿的三大定律来说,对于今天的我们来说,这太容易理解了,可对于欧几里德那个时代的人来说,就非常的违反直觉:物体不施加力不久是应该最终会停下来么?所以物体要保持匀速运动,就必须持续施以恒定的力。我们普通人今天对于相对论等问题的困惑,无意于此。甚至对于当时参与这些理论发现过程的神级人物
来说,其懵懂的程度不见得比我们好多少。

最近我在翻译英文维基百科上关于狭义相对论的发现史(传送门在这里,以后翻译完成后的正式地址应该是这里),里面所描述的各种大牛几乎都曾有过错误的认知,作出过错误的假设,得出错误的结论。这些大牛包括庞加莱、洛仑兹、爱因斯坦……当你们看他们这些人认识世界与思考过程的历史资料,你心里面就有这么一个感觉:好大一座山啊!我还想到了另一个词:屎山。

我之所以想到后面那个词,是因为如下的几个事实:

  • 有个经典的高数笑话,高数课可以用如下两句话描述清楚:这他妈是个啥?这他妈又是个啥?
  • 连这些大牛们都弄出很多错误的想法来;
  • 通常搞不懂或者搞砸了的时候,老外用shit,也就是“屎”这个词来表达他们的心情。

有意思的是,爬这座屎山的我,居然感到无比的激动。

最后

中国人对于知识的渴望程度,无论用总量还是人均的口径来衡量,都是个效果。比如在5月2日时,维基百科上面的条目数中文位居12,后面跟着个越南。越南是什么货色,居然有资格紧随中国……如果说日本的人均拥有词条量是中国的20倍可以用人均GDP落后来解释,越南人均拥有维基百科词条量是中国人均数的10倍,我不知道还能怎么解释。功夫网?好吧……我表示不想找这种借口,尽管我也表示对于功夫网及其发明人和接班人的无比鄙视。

(5月2日不知因何原因,中文维基百科疯长了1万个条目,于是乎瞬间超越瑞典语。就像以前有人跟我说,其实我们比阿三同学还是好不少的那样,我很想说咱们能不这么低标准么?不过鉴于我们连越南相比都有点吃力,这句话当时就咽下去了。突然想起来,光程兄如何了?貌似越南现在都不搞这一套了。)

2012年4月27日星期五

喜剧了,D800完胜5DIII

D800一发布,3600万像素就相当震撼了,挑战了135传感器的像素面积极限。当时其实我蛮不爽的,因为我一直认为高感对于抓拍很重要。之前用过一次7D,被那个垃圾级别的高感给震撼了,昏暗的室内拍出来的照片基本上和明亮场景下的卡片机一个级别。这么小的像素面积,高感估计不会很好。以DXOMARK的过往记录,用D700的可用高感推算,如果技术没有改进的话,D800的可用高感估计只有1400左右。当时我就想:啊!废了!

接着5DIII发布了,像素没有质的提高,看来是要拼高感了。当时估计5DIII的高感表现怎么着也会比D800要强,于是相当郁闷。因为考虑到佳能的机器,只要是5D及以下,高感表现完全一样——800左右,这个数字已经维持了至少4年以上了!再加上人家告诉我,佳能低端机器没有色彩曲线,对焦如何烂,测光对焦如何不能联动之类的。未来要是想要双机双镜,佳能简直是……尽管镜头卡口规格有优势,可也经不起这么个糟蹋法。

可后来看的各种测评图片,喜剧效果就开始显现了。先是发现D800比D700的高感还要好,无论是同样的尺寸下面,还是同样的放大比例下面。然后又发现,D800和5DIII比,在ISO1600及以下全面优胜,3200及以上,只要D800有那一档,基本上和5DIII一样的表现。关键是,5DIII涂抹太他妈严重了。我记忆很深刻的是一张对比图,右上角有一个黑色的纸标牌,这个标牌其实不平整,有一些稍微亮一点的折痕。当时是四款机器对比:5DIII vs D800 vs 适马的某型号 vs 845D,其它三个都看到折痕,唯有5DIII是一抹黑。还有很多不那么明显的细节,也是被抹掉了。

再后来,dxomark出来了d800的测评,ISO居然2853!尼玛怪兽啊,怎么可能?这一项有史以来排第三(第一是怪兽级的D3s,第二是后继者D4,D4像素多出33%来,略降一点正常)。而且此机器居然Landscape得分排第一,动态范围高低意思就是亮部暗部的细节会非常好。更变态的是色彩位数,居然在135里面排第一,即使拿120一起比,也拍第三。可以说,这基本上是有史以来最强悍的135电子单反。

即便是这样,我还是以为5DIII出来之后高感会让人吓一跳:毕竟5DIII的像素密度比D800低很多,就算高感没有强太多,也至少要和D800一个等级吧?结果让人失望,居然只有2293。好吧,这也是一个不错的数字,怎么也比5DII的1815好不少。可是……居然败给了像素密度比它还高将近一倍的D800,我这不知道到时候拿什么来宣传这个机器了。是说5DIII的比D800高感好呢,还是像素少呢?我仔细想了一下,比D800有利的地方恐怕只剩下:
  • 不追焦的情况下连拍数稍高一些;
  • 对焦点数稍微多一些;
  • 设想能够连续拍摄超过30分钟(好吧,如果有这么高的要求,估计HDMI直出更有诱惑力吧?;
  • 镜头潜力;
  • 广告和营销;
  • 也许会出现的价格优势,也许;
  • 也许产能会好一些,也许。
这些优势几乎和技术无关,基本上未来4年佳能的套路只剩下:
加大广告和营销的投入;
加大产能,确保在尼康断货期间确保足够的供货;
降价;
出一个升级版;
出一些个变态镜头。

还有吗?没有了。现在要不是大面积断货,估计都要忍不住入手D800了。





2012年4月15日星期日

SqlServer 2005如何改善datalength计算的性能问题


假设数据库中有一个表photo,其中包含2个key组成联合主键,分别名为userId和photoId,以及一个image类型的字段stream。此时有两种不同的思路,一种是在保存的时候进行计算,并存入第三个字段dataSize,另一种是每次都直接使用以下语句进行计算:
select sum(datalength(stream)) from photo where userId=@userid

第一种方法一般来说都挺不错的,但是有那么一个不那么完美的问题:很难确保数据一致性。假如某一次更新中的代码新添加了一种对stream进行修改的特殊代码,但很遗憾忘了修改dataSize字段。更新之后,程序不会表现出明显的异常,有可能会跑了很长一段时间之后才会发现。而发现之后,就需要专门的小程序来重新修复。并且修复之后,以我的经验,一定会有用户不认可发生的变化。也许有人会说,图片不都是只插入不更新的么?那我们就再来想另一个场景:这是一个类似Evernote的应用,用户在里面保存的文字数量也是限量的,同时也是需要修改的。有人还会说,修改stream的代码不应该都在一起吗,不应该修改的时候都会修改dataSize吗?问题是,应该的事情总有不发生的几率,而小概率事件则必然发生。

第二种方法则保证了每一次都是准确的,也不需要代码进行相应的计算和更新,于是就没有了前面的一个问题。这种方法在每个用户的数据量较少的时候问题不大,但是在数据量巨大的时候,会面临非常严重的性能问题。这是由Sql Server的数据存储结构所决定的。

在SqlServer当中,数据库每8K为1页(Page),8页为1片(Extent)。而一行数据中的定长数据通常都存在一个页当中,而不定长数据如varXXX一类,只要这一行数据没有超出8060字节,也会存在于当前页中。上述的数据成为行内数据(In_Row_Data)。而对于一行超出8060字节的情况下,不定长数据会存入超长数据页(Row_Overflow_Data)。而类似Image、text等类型的数据,则会保存在大对象数据页(Large_Object_Blob,简称LOB)当中。关于这些知识的解释,请参见此处的官方说明

可惜的是,上述说明中并没有详细解释超长数据和大对象是如何在行内数据中记录的。对此,我们可以通过下述方式进行分析。

首先,我们需要通过下属语句找出保存数据的某一页:

select first_page from
sys.system_internals_allocation_units as u inner join
sys.partitions as p on u.container_id = p.partition_id
where
p.object_id=object_id('photo') and p.index_id=1 and u.type=1

这一个语句会得到一个类似如下的数据:
0x876543210500
这个16进制数据实际上分为两个部分:0x21436587是页码,而0x0005则是文件号码。注意,这两部分都需要对原始数据进行一个颠倒,有点类似BigEndian和LittleEndian的颠倒过程。

得到这两个数据之后,我们进一步将页码部分转换为十进制558065031,将文件码也转换成十进制,然后输入如下的命令:
dbcc traceon(3604)
dbcc page(databasename, 5, 558065031, 3)
dbcc traceoff(3604)

这一个命令会在消息中输出具体的存储信息,我们在里面首先找到这么一行数据:


stream = [Textpointer] Slot 0 Column 5 Offset 0xab Length 16
                 

请记住这个偏移量0xab,然后在这一行往上找第一个Memory Dump,比如:


Memory Dump @0x0000000024A9C060

0000000000000000:   30008d00 653e0500 02000000 80000000 †0...e>..........
0000000000000010:   00000000 16640000 00ecf496 00749800 †.....d.......t..
0000000000000020:   00000000 00e52001 00000000 00000000 †...... .........
0000000000000030:   00ffffff ffffffff 60000000 00000000 †........`.......
0000000000000040:   00000000 0000f03f d0000000 00ecf496 †.......?........
0000000000000050:   00749800 00000000 00000000 0013f8fe †.t..............
0000000000000060:   f1000000 0000f03f 00000000 0000f03f †.......?.......?
0000000000000070:   cd000000 00030000 00000000 01000000 †................
0000000000000080:   f89e015d 4a49ab3f 00000000 002f0000 †...]JI.?...../..
0000000000000090:   009c0370 7b0600ab 00bb80cb 00cb00cb †...p{...........
00000000000000A0:   00ef0001 4f1a4ecb 4ecd7e00 00f2fef8 †....O.N.N.~.....
00000000000000B0:   13000010 00000005 00010020 20202020 †...........    
00000000000000C0:   20202020 20202020 20202031 64616130 †           1daa0
00000000000000D0:   6230332d 62313866 2d343462 302d6231 †b03-b18f-44b0-b1
00000000000000E0:   62302d35 64353739 38366164 623037††††b0-5d57986adb07


途中高亮部分就是刚才偏移量所指出的16个字节的数据。根据观察,后8个字节10000000 05000100 和堆数据的行定位数据结构(row locator)非常相似。而该结构的含义如下:
页码(4字节) 文件号(2字节) 槽号(2字节)

需要注意的是,这是Little Endian编码格式。经过转换,页码为16,文件号是5,槽号为1。然后我们再次执行dbcc page命令如下:

dbcc traceon(3604)
dbcc page(databasename, 5, 16, 3)
dbcc traceoff(3604)


我们在消息输出中,找到槽号(Slot)为1的位置,就可以看到如下的数据:


Blob row at: Page (5:16) Slot 1 Length: 4900 Type: 3 (DATA)

Blob Id:21960150089728

而上面的二进制大对象块(Blob)编号21960150089728的十六进制值为0x000013f 8fef20000,正好是上面高亮部分十六个字节的前8个字节。换句话说,这个字段的大小并非存在行内数据当中,而是存在于大对象页当中,也就是上面的黑体字所显示的地方。

因此,当我们执行类似select sum(datalength(stream)) from photo where userId=@userid 这样的语句时,数据库系统再怎么优化,也需要读取这些页,而不是只需要读取行内数据即可。由于大对象通常都很大,其数据密度是相当低的,通常一页当中只有3-4行对应的字段值。除此之外,大对象的保存顺序和聚集索引不同,和数据的增删改的顺序有关,而与聚集索引本身的顺序无关。于是,我们基本上可以根据如下的方式估算一次上述查询所需要读取的数据量:
行数= select count(*) from photo where userId=@userid
读取次数 = 行数
读取数据量 = 行数 * 8K

也就是说,当用户数据达到1000行时,读取的数据量可能会达到8M,而且还是随机读取。而用两块普通15krpm的SAS硬盘做镜像,随机读取能达到4MBps就不错了。因此每次查询将会消耗2秒的时间,其它的查询只要内存中没有缓存,都注定需要等待。

那么,是否只能够退化到添加一个静态字段,然后通过代码来更新的境地呢?其实还有另一种方案:添加计算字段并对其做索引。这种方案对于查询远多于修改的系统来说,是一个很不错的选择,方法如下:

首先,在数据库的属性中,修改算术终止已启用(ArithAbort)为是(True)。这一步非常关键,否则当你完成后面两个步骤之后,将会无法对数据进行更改。然后用下列语句添加一个计算列:

alter table photo
  add dataSize as (datalength(stream))

然后再通过下面的语句添加一个索引:

CREATE NONCLUSTERED INDEX [IX_photo_datasize] ON [dbo].[photo] 
(
[userId] ASC,
[photoId] ASC
)
INCLUDE (
[datasize]
) WITH (STATISTICS_NORECOMPUTE  = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = ON, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON)

需要注意的是,上面的ONLINE一定要设置成ON,这样可以让系统不要锁柱整个表(仍然会短暂锁住,但几乎感觉不到)。而datasize一定要设置成包含列,因为这一列基本上不会用于检索,出现相同值的可能性也非常低。而是否应该添加其它的列(包括包含列),例如photoId,则需要根据根据实际情况分析处理。

这三个步骤完成之后,再执行查询select sum(datalength(stream)) from photo where userId=@userid,你会发现执行任务计划已经使用了IX_photo_datasize了!注意,你连sql查询语句都不需要修改,SqlServer会自动识别出datalength(stream)等同于datasize字段,而datasize字段存在于IX_photo_datasize索引。很先进吧!

看到这里,有人就会问了:为啥不直接添加一个持久化的计算字段呢?这是因为一旦设置为持久化,将会导致行内数据的结构发生变化——多了4个字节的数据嘛。于是,这将会强迫数据库系统对已有的数据进行修改。对于数据量较小的库来说还好,而对于数据量巨大的库来说,将会导致系统长时间锁定该表而无法正常提供服务。这在互联网应用上,通常来说会导致用户的强烈反应。当然,你也可以采取复制一个数据库,并且设定为只读的方式,让服务在只读状态下运行,于此同时则修整真实数据库,修整完之后再切换回来的办法。但是,为什么要采取一个这么复杂的步骤,来获得几乎相同的效果呢?

不过,需要注意的是,以下情况会导致索引失效:
  • 查询中的函数不是计算字段中函数的一部分。比如说,查询字段设置成了IsNull(datalength(stream), 0),而查询却为select datalength(stream) ……
  • 查询中包含了某种导致索引失效的情况,比如:
    select sum(streamsize) from
      (
      select row_number() OVER (order by userId, photoId) as rowno,
             datalength(stream) as streamsize
        from photo
        where userid = @userid
      ) t
    where rowno between (1, 1000)
对于第一种情况,你需要修改查询字段,而对于第二种情况,则必须修改sql语句,比如说改成如下所示:
select sum(datalength(stream)) from photo as p inner join
  (
  select row_number() OVER (order by userId, photoId) as rowno,
         userId, photoId
    from photo
    where userid = @userid
  ) as t on p.userId = t.userId, p.photoId = t.photoId
where rowno between (1, 1000)

上述的方法有如下几个巨大的好处:
  • 即便前期规划出现问题导致数据库结构不包含stream大小的静态列,也可以后期修改;
  • 修改时服务不需要下线维护,同时也不会导致明显的服务阻塞感;
  • 几乎不需要修改任何程序,即便需要修改,程序修改和数据库修改也没有严格的先后顺序,在同时修改完成之前,性能几乎没有明显的下降,服务连贯性非常好;
  • 数据一致性非常好,不会受到程序设计、更新等人为失误的影响;
  • 性能提升非常明显,由于数据密集度很高,甚至比静态字段效果还要好。

当然了,硬币的另一面是:
  • 你需要修改数据库的默认行为(ArithAbort要改成True,一般影响不大);
  • 对于select * from photo where ...这样的查询,在执行计划中将会比原来多出一个计算datalength(stream)的步骤。不过由于stream本来就必须从文件中读取出来,因而计算datalength基本上不会增加可感觉到的运算时间;
  • 存储空间和写入次数都比静态字段稍多一些。其实是多出2倍以上(假如索引去掉PhotoId列的话),但是因为每一行的数据量其实很小,因此通常可以忽略不计。比如对于1亿行的数据的表使用上述方法,可能会多出至少400MB的数据量。而对于有着1亿行数据的表来说,本身的数据量就不会少于2.4GB(4字节userId+4字节PhotoId+16字节stream的row locator),这还没有算上stream部分占用的空间。因此,基本上可以忽视其空间上的影响。
怎样,这个方案不错吧?