语言的效率差异2

# 问题 #
为了更深入的测试语言,我做了一个经典问题——24点。
这个问题主要是测试递归,循环效率,还有数组和树的复制性能。
为了简化问题,方便测试,我的问题是这样描述的:
> 有一个数组,里面有多个正整数。有一个操作数组,其中每个都是双目操作符。找出以两者构成算式,其值等于给定值的所有表达式组合。

> 要求不得遗漏,可以有少量重复。例如可交换算符的交换同构暂不做排重。

实际运行的时候,取+-*/和3 4 6 8,运行100次,查看时间消耗。正确的单次输出结果应当是这样的。
> (((8 + 4) / 3) * 6) = 24
> (6 / (3 / (8 + 4))) = 24

> (((8 + 4) * 6) / 3) = 24

> (((8 / 4) + 6) * 3) = 24
> (((8 – 6) * 3) * 4) = 24
> (((8 – 6) * 4) * 3) = 24
> (((3 * 4) – 8) * 6) = 24
> ((8 – (6 / 3)) * 4) = 24
> (((4 + 8) / 3) * 6) = 24
> (6 / (3 / (4 + 8))) = 24
> (((4 + 8) * 6) / 3) = 24
> (((8 / 4) + 6) * 3) = 24
> (((4 * 3) – 8) * 6) = 24
> (((8 – 6) * 3) * 4) = 24
> (((8 – 6) * 4) * 3) = 24
> ((8 – (6 / 3)) * 4) = 24
# python #
python的解很复杂,长达31行,以下是我写的解。当然,还有更简单的版本,我可以用eval来干这个事情,代码只有24行,但是确实给人很evil的感觉。

from itertools import combinations

class opt(object):
   def __init__(self, name, func, ex=True):
       self.name, self.func, self.exchangable = name, func, ex
   def __str__(self): return self.name
   def __call__(self, l, r): return self.func(l, r)
   def fmt(self, l, r):
       return '(%s %s %s)' % (fmt_exp(l), str(self), fmt_exp(r))
def eval_exp(e):
   if not isinstance(e, tuple): return e
   try: return e[0](eval_exp(e[1]), eval_exp(e[2]))
   except: return None
def fmt_exp(e): return e[0].fmt(e[1], e[2]) if isinstance(e, tuple) else str(e)
def print_exp(e): print fmt_exp(e), eval_exp(e)
def chkexp(target):
   def do_exp(e):
       if abs(eval_exp(e) – target) < 0.001: print fmt_exp(e), '=', target
   return do_exp
def iter_all_exp(f, ops, ns, e=None):
   if not ns: return f(e)
   for r in set(ns):
       ns.remove(r)
       if e is None: iter_all_exp(f, ops, ns, r)
       else:
           for op in ops:
               iter_all_exp(f, ops, ns, (op, e, r))
               if not op.exchangable:
                   iter_all_exp(f, ops, ns, (op, r, e))
       ns.append(r)
opts = [
   opt('+', lambda x, y: x+y),
   opt('-', lambda x, y: x-y, False),
   opt('*', lambda x, y: x*y),
   opt('/', lambda x, y: float(x)/y, False),]
if __name__ == '__main__':
   for i in xrange(100): iter_all_exp(chkexp(24), opts, [3, 4, 6, 8])
以下是100次的时间:
> real 0m2.259s
> user 0m2.248s
> sys 0m0.004s
# common lisp #

lisp来解这个问题简直是作弊,难怪被叫做人工智能语言。

(defun chkexp (target)
 (lambda (e)
   (if (equal (ignore-errors (eval e)) target) (print e))))
(defun exchangeable (op)
 (not (member op '(- /))))
(defun iter-all-exp (f ops ns &optional e)
 (cond
   ((not ns) (funcall f e))
   ((not e) (dolist (r (remove-duplicates ns))
      (iter-all-exp f ops (remove r ns :count 1) r)))
   (t (dolist (r (remove-duplicates ns))
(let ((nss (remove r ns :count 1)))
  (dolist (op ops)
    (iter-all-exp f ops nss `(,op ,e ,r))
    (if (not (exchangeable op))
(iter-all-exp f ops nss `(,op ,r ,e)))))))))
(iter-all-exp (chkexp 24) `(+ – * /) `(3 4 6 8))
只有短短17行。原因在于,lisp本身的ast即是用数据结构表示的,因此根本不需要我做eval函数,也不需要画蛇添足的弄自定义算符,直接用系统算符上。显示,打印,都是现成的。需要写的只有主体逻辑。结果也很特别:
> (* (- (* 3 4) 8) 6) 
> (* (- 8 (/ 6 3)) 4) 
> (* (- (* 4 3) 8) 6) 
> (* (/ (+ 4 8) 3) 6) 
> (/ 6 (/ 3 (+ 4 8))) 
> (/ (* (+ 4 8) 6) 3) 
> (* (+ (/ 8 4) 6) 3) 
> (* (- 8 (/ 6 3)) 4) 
> (* (* (- 8 6) 3) 4) 
> (* (* (- 8 6) 4) 3) 
> (* (/ (+ 8 4) 3) 6) 
> (/ 6 (/ 3 (+ 8 4))) 

> (/ (* (+ 8 4) 6) 3) 

> (* (+ (/ 8 4) 6) 3) 
> (* (* (- 8 6) 3) 4) 
> (* (* (- 8 6) 4) 3) 
不但行数只有一半,速度也很让人吐血,比python快了近一倍,这是100次的结果。
Evaluation took:
 1.379 seconds of real time
 1.372086 seconds of total run time (1.372086 user, 0.000000 system)
 [ Run times consist of 0.012 seconds GC time, and 1.361 seconds non-GC time. ]
 99.49% CPU
 3,628,800 forms interpreted
 4,127,047,350 processor cycles
 102,577,080 bytes consed
# go #
# lua #
lua的代码是所有语言中最罗嗦的,足足长达60行,超过python许多。
function find_item(tbl, obj)
  for i, v in pairs(tbl) do
     if v == obj then return i end
  end
  return nil
end
function remove_duplicates (tbl)
  local newtbl = {}
  for i, v in pairs(tbl) do
     if find_item(newtbl, v) == nil then
table.insert(newtbl, v)
     end
  end
  return newtbl
end
function fmt_exp (e)
  if type(e) ~= 'table' then
     return tostring(e)
  else
     return '(' .. fmt_exp(e[3]) .. e[1] .. fmt_exp(e[4]) .. ')'
  end
end
function eval_exp (e)
  if type(e) ~= 'table' then
     return tonumber(e)
  else
     return e[2](eval_exp(e[3]), eval_exp(e[4]))
  end
end
function chkexp (target)
  return function (e)
    if eval_exp(e) == target then
print(fmt_exp(e))
    end
 end
end
function iter_all_exp (f, ops, ns, e)
  if table.maxn(ns) == 0 then return f(e) end
  for i, r in pairs(remove_duplicates(ns)) do
     table.remove(ns, find_item(ns, r))
     if e == nil then
iter_all_exp(f, ops, ns, r)
     else
for op, fp in pairs(ops) do
   iter_all_exp(f, ops, ns, {op, fp, e, r})

   if find_item(exchangable, op) == nil then

      iter_all_exp(f, ops, ns, {op, fp, r, e})
   end
end
     end
     table.insert(ns, r)
  end
end
exchangable = {'+', '*'}
opts = {
  ['+'] = function (a, b) return a + b end,
  ['-'] = function (a, b) return a – b end,
  ['*'] = function (a, b) return a * b end,
  ['/'] = function (a, b) return a / b end,
}
iter_all_exp(chkexp(24), opts, {3, 4, 6, 8})
其实lua的代码很好看,自然语言风格,语言写出来后都能看懂。然而lua秉持了最小化内核的方针,死活不提供一些很常用的函数。我上来近15行全在写常用函数,查找某个值在表中的位置,还有除去表中的重复元素。实现下来,效率也不是特别高,基本和python相当。
> real 0m2.222s
> user 0m2.184s
> sys 0m0.000s

语言的效率差异1

# 问题 #
为了测试语言的效率,做一个正则解析。
预先说好,正则解析的问题是老板正在做的一个实际问题,我把其他和效率无关的部分去了。因此我接受“用法不正确”这样的反驳理由,但是不接受“这不是典型用例”的理由。我欢迎你指正我的用法错误,或者对语言不了解导致的效率低下,但是别来和我吵吵这种例子太特殊。另外,在调整代码和评估速度的时候,顺便注意一下代码行数。我知道用汇编逐行写和优化会很优秀,但是这对实际工作基本没有帮助。
问题是这样的:
> 有一个文本文件,每行两个数,要求解析出来这两个数。
我用python生成了数据,代码是这样的
    with open(sys.argv[1], 'w') as fo:
        for i in xrange(500000):
            fo.write('%d %dn' % (i, random.randint(0, 10000)))
正则分析速率,是个典型的CPU密集操作。对于非编译型语言而言(这里的编译是指正则表达式的解析预编译,实际上除了lisp还真没有编译型的,即使是go也是现场拿到正则进行解析的),这主要是看正则库的实现效率。很多时候,语言的效率问题并不取决于语言本身,还取决于语言的库的实现。大部分情况下我们都不可能砍掉系统的库重新来一个,那还不如换一门语言。
# python #
我首先贴出python语言的解答。
reline = re.compile('(d+) (d+)')
def main():
with open(sys.argv[1], 'r') as fi:
for line in fi: reline.match(line).groups()
这是性能
> real    0m0.466s
> user    0m0.436s
> sys     0m0.012s
# common lisp #
我找了N个正则包,实际能用的只有ppcre。有些包号称很快,实际测试下来还不如ppcre。
(require :cl-ppcre)
(defun grepfile (filename)
 (let* ((cl-ppcre:*use-bmh-matchers* t)
(cl-ppcre:*regex-char-code-limit* 256)
(scanner (cl-ppcre:create-scanner "d+ d+")))
   (with-open-file (in filename)
     (loop for line = (read-line in nil) while line do
  (cl-ppcre:split scanner line)))
 ))
代码在slime里面测试(time (grepfile "data.dat")),下面是结果
CL-USER> (time (main))
Evaluation took:
 0.398 seconds of real time
 0.392025 seconds of total run time (0.384024 user, 0.008001 system)
 [ Run times consist of 0.016 seconds GC time, and 0.377 seconds non-GC time. ]
 98.49% CPU
 1,188,481,425 processor cycles
 72,242,256 bytes consed
# go #
go的代码是现学现卖的,不知道是不是哪里写出问题了。
func main() {
f, _ := os.Open("data.txt")
r := bufio.NewReader(f)
rex, _ := regexp.Compile("(\d+) (\d+)")
for line, isPrefix, err := r.ReadLine();
   err == nil && !isPrefix;
   line, isPrefix, err = r.ReadLine() {
rex.FindSubmatch(line)
}
}
结果居然要差一个数量级!
> real 0m8.699s
> user 0m8.593s
> sys 0m0.036s
这太出乎我的意料了。google的v8引擎赫赫有名,我猜想也应当用到了go上面才是,怎么会性能差成这样?gary说过正则在他那里很快,我希望是我用错了。
# lua #
lua没有使用正则包,更准确的说,lua内置的字符串处理函数可以处理这个情况。以下是我的代码:
for line in io.lines("data.txt") do
  for w in string.gmatch(line, "%d+") do
     – print(w)
  end
end
以下是执行结果:
> real 0m0.796s
> user 0m0.792s
> sys 0m0.000s
lua的代码的却很好看,但是效率上却不见得高。这是当然的,gmatch可是每工作一次就要解析一次阿。
# lua-rex-pcre #
装一个支持pcre的正则包,lua-rex-pcre。
r = require "rex_pcre".new("(\d+) (\d+)", 0)
for line in io.lines("data.txt") do
  r.match(r, line)
end
OK,速度一下就快了不少:
> real 0m0.643s
> user 0m0.632s
> sys 0m0.008s

新闻和八卦和概率聚集

新闻和八卦有个共同点,就是会扭曲我们常规的概率体验,例如:今天摔了一架飞机。我们的结论往往是,飞机好危险。然而,世界上有很多没问题的飞机,这个你是不会从新闻和八卦里面听到的。因此,以这些东西为基础得出的结论,往往是错的。
从这点说开去,其实我们会发现很多东西都不可靠。首先说八卦。八卦之不可靠是全世界皆知的,要是有人神神秘秘和你说个事情,要么是一点不着边的瞎猜,要么就是一枪命中的内幕。可惜的是,到底是哪个,在事先完全无法分辨。香农说,信息就是减少不确定性,从这点来说,这些八卦里面的信息量是负数——本来一个事情挺明白,八卦一传,搞不好当事人都不明白是怎么回事了。
其次我们再说新闻,新闻倒是有信息量,但是新闻的不可靠也是人尽皆知。中国的文艺宣传理论就不去说了,老美也经常骂,大新闻集团只挑符合他们利益的说。不过幸好,人家新闻利益集团有好几个,岳飞打张飞之下,总算还有不少东西给披露出来。
最后,你的亲眼所见一定是真么?我在以前的一篇blog里面说过,我对问题的分析是偏颇的,因为我做不到随机取样。我周围的朋友,一定是具备某个特性/符合某个范式的。例如熟悉电脑,受良好教育的城市青年,中国人,这些总是没办法的事情。如果以此为样本来分析相关问题,相信一定会南辕北辙。例如你以我的gtalk和twitter好友来分析python和emacs用户的比例,那一定是高的异乎寻常。这是当然的,我本来就是以python和emacs作为自己的标签,无论是朋友也好,会fo我的人也好,多半是此道同好。以此类推,若是你不仔细分析,即使以亲眼所见,也未必能得到结论。

全部和谐音程表(泛音表)

>>> for i in [(i, j, 12 * math.log(float(j)/i, 2)) for i, j in itertools.permutations([1,2,3,4,5,6], 2) if i < j]: print i
… 
(1, 2, 12.0)
(1, 3, 19.019550008653876) 
(1, 4, 24.0)
(1, 5, 27.863137138648348)

(1, 6, 31.019550008653873)
(2, 3, 7.019550008653875)
(2, 4, 12.0)
(2, 5, 15.863137138648348)
(2, 6, 19.019550008653876)
(3, 4, 4.980449991346124)
(3, 5, 8.843587129994475)
(3, 6, 12.0)
(4, 5, 3.863137138648348)

(4, 6, 7.019550008653875)
(5, 6, 3.1564128700055254)

论医

# 医生的道德标准 #
要摆正医患关系,首先就必须将医生的道德标准降下来,从“白求恩精神”,降低到一般人标准。
为什么?有一则故事,叫做“子贡赎人”,挺有名的,大家可以自查。跳过故事本身,我直接说其中的观点:
如果将道德标准提高到没有人能够接受的地步,就不会有人照做了。
类似,大家都是人,为什么让别人去遵循“白求恩精神”,或者其他变态的精神,而让自己能够得利呢?拥有这种精神的毕竟是少数。以这个为标准要求多数人的结果,就是没有人能够成为医生。
医生的标准,只要能够诚实的提供服务就好了。动辄就拿奉献说事,实则是用大帽子压人,和文革无异。
        当然,现今最主要的问题,是以白求恩精神要求医生的同时,连诚实服务都贯彻不下去。这种动辄涉及人命的事情上不公开透明是会出人命的。只要有一小撮是贯彻不下去的,那病人就会连剩下的医生一并怀疑,然后杀医生的戏码就会层出不穷。
貌似要立法阻止杀医什么的,其实我说,这也就是对纯粹的医闹管用,真的爆上新闻的,多半不会是医闹。道理很简单,医闹的目的是拿钱,耍赖撒泼软语哀求的目的都是拿钱而不是报仇。就算他再和医生横眉竖眼,也不会往死里下手——真的往死里整的,多半是豁出一条命的家伙。人不畏死,奈何以死惧之。解决这个问题的最好途径不是阻拦,而是有个心平气和能让他解决问题的途径。
# 不实施抢救的道德标准 #
我认为,以下两种情况下,不实施抢救不应当受到责难。
1. 病人死亡。
2. 病人不可避免的向死亡发展,其中会为本人或家属带来极大痛苦。
# 医疗会耗尽社会的剩余资源 #
我觉得这是不言自明的。人类社会从原始社会仅够自己温饱,发展到目前的状态。实则目前一个人的工作供养数人足矣。或者换个表达方式,全球所有人所需的食物/衣物/住房等资源,并不需要所有的人类进行生产。若非如此,贫困救助体系和养老体系都没有发展的前提。多余的人产生的物资,即是社会的剩余资源。
社会目前的剩余资源,必然会有一个出口。一方面,第三产业,娱乐业,都是基于剩余资源的。也可以看作,从事生产的人为他们提供食粮,而他们为从事生产的人带来娱乐。而另一方面,科技发展,医疗和医疗技术发展,也是基于这些资源的。
随着科技发展,人类的剩余资源冗余度会越来越大。人类需要的必需品只需要更少的人进行生产,更多的人会投身非直接产生必需品的行业。在这种情况下,我更希望他们参与医疗和科技发展。并不是说娱乐业不好,或者不需要。只是科技和医疗能够带给人更好的生活——我是这么相信的。