不要过早优化

从入cs这个坑(或者更早一点)的时候,我一直秉承 “代码要精简,高效”这个观点。
所以我写出过:

void inplace_swap(int& a, int& b){  //会溢出
a = a + b;
b = a - b;
a = a - b;
}

void inplace_swap_bit(int& a, int& b){ //没考虑过a ==b 的情况
a = a ^ b;
b = a ^ b;
a = a ^ b;
}

这种想要穿越回几年前狠狠抽自己一巴掌的代码。(而且当时还像掌握了什么武林秘籍似的沾沾自喜)


还有幼年期的时候因为听说“查找虚函数表会在运行期降低性能所以要尽量避免继承”这种鬼话而避免写类与继承,结果一个小项目里面有90多个类,一、个、继、承、关、系、都、没、有。为此又引入了40多个helper 方法,看着都心累。

后来虽然开始讲求起了代码可读性,可复用性。但是依旧忍不住会去为了提升性能(?)而写一些如 const reference 之类的代码(当然更多的时候是在方法里又做了一次拷贝以便修改);或者在PoC阶段就搞一些引用传来传去,结果爆了栈;还有为了避免拷贝结果修改了引用导致数据不一致的bug(当然智商不够,过分naive也是原因)。统计起来看,平均每节约一个拷贝的收益,远小于找bug,掉头发等生理心理和时间上的损失。

在一次一次重新回顾自己的代码(AKA 修以前的bug)之后,我开始反问自己:代码优化(在初期开发阶段)真的重要么?一次拷贝真的会浪费大把大把的资源么?

答案是:真的不重要。

在原型阶段,完成业务流程的优先级远大于代码本身的运行效率。而且此时很多工具尚未定型,过早优化会给自己引来技术债,导致重复修改内部接口,甚至早早出现包装方法,转接器等设计模式,使逻辑跳转更加复杂,可读性大大下降(都是泪)。相反,从个人经验来看:让工具趋向泛型比让工具更具体,更优化来的有意义的多。清理发布阶段再做优化,为时不晚。过早优化,过早背上技术债,得不偿失。

读书笔记: 第一章:低于类级

String 与Byte

python3中,原生的string 从python2 的byte string 变成了unicode,并且是不可变类型(因此string可以被当作dict的key)。并且在python3 中,u” 格式的string等同于 ”。

很多时候我们习惯使用 ”.join((str_a, str_b, … str_n)) 来拼接多个string。这种写法的可读性并不高。而且在实际的工程中,我们很有可能会拿到混合类型的容器(即容器内内部既有字符串,又有数值类型)。对这类容器进行join时会遇到意外的错误,比如:TypeError: sequence item 1: expected str instance, int found

除去 join之外,我们也可以用+来做字符串拼接。

Cython 利用peephole 优化器实现了“常量折叠”的优化,它可以将最多20个字符(为什么最多只有20?)的拼接在编译期压缩优化。在字符串总长小于20的时候,+拼接效率极高,一旦长度超出20,+拼接的效率会迅速下降。

最佳实践建议我们用str.format() 或者’%’ 来拼接字符串(当然这前提是我们知道有多少字符串要进行拼接)。

集合 list, tuple, dictionary, set

list与tuple 的区别是前者是可更变的类型,后者是不可变类型: 所以不能用list作为dict的key,同样不能用list做set的key。

如果同样的方法既能用list实现,也能用tuple 实现,那么就无脑用tuple——因为tuple创建和拷贝的效率高。

很多人不知道,在Cython的世界中,list其实是数组而非链表。具体实现时,list初始化时会预留一段空间给append。因此list 的append和pop的性能接近于O(1)。 但是对于insert 和delete, list的时间复杂度是O(n)。

如果你真的需要linked list, python 提供了deque类型(在collections包里)。

列表推导

列表推导式在python中应用极广。如:

#列表推导
>>> [i for i in range(10)]
>>> print (i)
... [0,1,2,3,4,5,6,7,8,9]
# for循环
>>> res = []
>>> for i in range(10):
>>>     res.append(i)
>>> print (res)
... [0,1,2,3,4,5,6,7,8,9]

与for 循环表达式相比,列表推导式更简洁易懂。

有一个著名谣言说,用列表推导式比用for循环的性能更好是因为解释器会先推导出最终列表的长度,所以可以避免多次reallocate list。(这是骗人的!)

尽管列表推导式在性能上并没有显著优于for 循环,但是从易读性和可维护性上来说,还是推荐用列表推导式来生成列表。

enumerate,zip

enumerate方法在遍历有 key value容器时会同时返回key 与value,在遍历无key容器时会给它加上一个索引。

zip方法可以将两个容器压缩成一个key value容器,长度与较短容器长度一致。

dict

dict是python中最常用的key value 容器。它的推导式和列表推导式比较类似,仅多了一个key : value 格式。


>>> squares = {number: number **2 for number in range(5)}
>>> print (squares)
... {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

在python3中,keys(), values(), items() 的返回类型是view 而不是python2 中的list。

相比于list,view会随着字典内容的更新而更新,不需要额外的存储空间,并且也支持len() 与in 操作。

python3 中, dict的 iterkeys(), itervalues() 和 iteritems() 方法也被移除。

一些实现细节:

在Cython中,字典是由hash table实现的。只有不可变类型可以作为key。如果你想要自己创建可以作为hash key的不可变类型,切记实现它的 __hash__ (计算hash 值)与__eq__ (比较)这两个魔术方法。

Cython使用开放寻址法来解决hash 冲突。

在字典中,key的存放并没有顺序一说(想象下所有的key都被保存在一个set中)。如果需要key值有序,用OrderedDict。本质上来所OrderedDict是一个红黑树。

set

set是一个可变的容器(不可变的集合是frozenset)。从数学定义上来说,set中的元素具有无序性,唯一性,确定性。所以set中的每一个元素必须是不可变类型,而且set常被用来去重复值。

一些实现细节:

同样,在Cython中,set也是由hash table 实现的(事实上,set更类似一个value为bool的dict)。因此,set和dict相比,操作的时间复杂度十分类似。

迭代器:

实现__next__ 和 __iter__ 方法即可。

class CountDown:
def __init__(self, step):
self.step = step
def __next__(self):
“””Return the next element.”””
if self.step <= 0:
raise StopIteration
self.step -= 1
return self.step
def __iter__(self):
“””Return the iterator itself.”””
return self

真码农从不用pascal

译自:http://web.mit.edu/humor/Computers/real.programmers
在编程的黄金年代,码农和非码农是很容易区分的: 懂计算机编程语言的就是码农,反之就是非码农。真·码农总爱说`DO 10I=1,10′ 或者`ABEND’ (你懂的,他们永远只用大写字母)。 其他人都在说“计算机对太难了”或者“我不喜欢和电脑打交道,它们太冷冰冰,没有一点人情味”。

但是,时至今日,我们已经来到了一个计算机大爆炸的时代了 – 你家里随便一台家电里都运行着定制的系统,小孩子玩的ipad里面运行着世界上最先进的几个操作系统之一。任何人都可以花个几千块从商店里买一台属于自己的电脑。人人都可以接触电脑,人人都可以学习编程使得真·码农这一物种濒临灭绝,取而代之的是80后的codecademy毕业生。

所以,找到一个方法来快速准确的判断一个码农是codecademy毕业生还是真·码农迫在眉睫。 如果两者直接区别很明显,那么对于codecademy玩家来说,真·码农就是他们人生的下一个目标;对于HR来说,这些差异也可以告诉他们为什么用codecademy毕业生来替换真·码农是一件石乐智的事。

语言

判断一个码农是真还是假,最简单直观的方式是看他擅长的语言。 真·码农用FORTRAN,80后用PASCAL。曾经有人问PASCAL的设计者Nicklaus Wirth 如何念自己的名字。他说,如果你按照名字(name)念,那应该念“Veert”, 如果按照值(value)念,那应该念(Worth). 从这个回答里我们可以立刻判断Nicklaus Wirth是个codecademy 选手:真·码农只认同返回值这一种参数传递机制。真·码农们从不关注过多的无用的概念,在他们的日常生活中,键盘,啤酒和FORTRAN VI的编译器就完美满足他们的需求。

  • 真·码农用FORTRAN处理列表
  • 真·码农用FORTRAN处理字符串
  • 真·码农用FORTRAN报税/炒股
  • 真·码农用FORTRAN实现AI
  • 。。。

如果一个需求不能用FORTRAN解决,真·码农会用汇编来解决。如果汇编也解决不了 – 那这个需求就不应该存在。

 

结构化编程

计算机科学在过去几年里引进了“结构化编程”。很多学者都认为使用结构化的方式来编程,程序会更容易被理解。但是他们给出来的例子并不具有普适性。这使得很多刚毕业的学生认为自己是世界上最好的码农:他们可以用五种不同的语言来写一个五子棋游戏!可以用20行代码翻转二叉树!但是大多数时候,现实世界的任务是:读一个20万行代码的文件,理解并尝试去优化它。

真·码农不会被各种“结构化”定义所约束,他们一般都:

  • 敢于用GOTO
  • 可以写一个5屏的for 循环,并且能快速的找到每一个大括号对应的另一部分
  • 喜欢在if中计算表达式 —— 使代码具有加密性并自动筛掉读不懂表达式的
  • 真码农喜欢写自修改代码 —— 为了节省哪怕1纳秒的运算时间
  • 从不写注释:代码自身的逻辑很“浅显易懂”
  • 由于FORTRAN没有结构化的IF,REPEAT,UNTIL 和CASE,真·码农会用GOTO来模拟它们

数据结构,接口,抽象类,指针等等结构化数据也变的越来越流行。Wirth(PASCAL设计者) 写了一整本书来教你如何使用这些结构化数据来编程。但是在真·码农的世界里,只有一样数据是结构化的:数组。字符串,链表,集合 —— 他们都是用数组来实现的。

 

操作系统

真·码农用什么操作系统?windows?在真·码农眼中,windows就是玩具——小学生都知道该怎么操作它。

Linux看起来复杂很多。 但是大多数时候人们不会用Linux来做真正的编程,他们大多用Linux来写论文,玩MUD游戏和上telnet bbs。

真·码农向来只用OS\370(注1)  。一个好的码农可以在JCL手册中找到如何解决IJK305I错误,一个伟大的码农可以不用任何参考手册来编写JCL代码。一个真·码农可以用心算来发现在6MB核心中的数据转存错误。

同时,OS\370 是一个真正卓越的操作系统:一个bit的数据错位就可以毁掉你几周的工作数据。所以开发者必(bei)须(po)保持警觉。

 

编程工具

真·码农用什么开发工具?理论上来说,只要你把代码敲进电脑的终端,它就可以运行。在计算机有终端前码农如何编程? 真·码农熟知16进制的内存引导数据。每当他写好代码后,都会加上引导代码强制计算机来运行它。(在那个时候,内存还是有记忆的——断电并不会抹去其中的数据。现在的内存变得非常健忘,只要断电就会忘了所有的数据)。

我认识一个真正的码农Jim,他在德州仪器上班。有一天他接到一个客户的电话,声称他的系统在保存一个关键数据时崩溃了。Jim 让他把电话线接口接到计算机上(那时候的计算机有电话接口),并用0/1两个按钮远程重写了部分代码。一个真·码农的工具箱里总会有一个键盘和一个打卡器,有了这两样工具,他就可以写出各种代码。(当然,真正的码农从来不用鼠标)

大多数系统提供了几种文本编辑器供码农使用, 真·码农会小心的挑选一个能反映他风格的编辑器。很多人认为Emacs/Vi是世界上最好的编辑器。这些编辑器的问题是,真·码农认为它们宣扬的“所见即所得”的概念很糟糕 —— 他们更需要的是一个“你要我有”的编辑器。TECO就是这样的编辑器。TECO的命令类似另一种序列化编程语言。(一个著名的娱乐游戏就是,你在TECO命令行里输入你的名字,猜猜它会解析成什么命令组合)。

基于以上的编辑器弊端,很多真· 码农认为直接输入二进制代码效率更高。 下面是真·码农绝对不会用的工具:

  • FORTRAN预处理工具:如MORTRAN, RATFOR等。
  • 调试器——真·码农只会阅读core dump
  • 检测数组边界的编译器
  • 版本控制

 

娱乐活动

真·码农的娱乐生活和他的工作方式很别致。下面是一些能帮助你快速判断一个人是真·码农的秘诀:

  • 爬梯中,躲在墙角讨论操作系统安全性的那撮人
  • 足球场上,拿真人和ps2游戏里的模拟选手比较的那撮人
  • 沙滩上,在沙子上画流程图的那撮人
  • 在超市里,执意自己用激光扫码器结帐的人:他们完全无法相信收银员(尽管他们都用键盘)。

 

日常生活

真·码农的生活环境与其他人不同,为了让他们能发挥最大的工作效率,真·码农应该坐在终端前面,四周环绕着:

  • 一个列着所有真·码农工作过的项目列表。
  • 几杯没喝完的冷咖啡,有没抽完的烟屁股更佳
  • 《操作系统》《编译原理》《JS the good part》之类图书基本。有折印更佳。
  • 一份1969年的日历(方便计算时间戳)
  • 在办公室某个抽屉里藏了一盒奥利奥饼干
  • 饼干下面是一个流程图模板

真·码农可以连续待机长达50小时,他们只需要在编译的时候打个盹就足购休息。如果日程安排的没有那么紧张,真·码农可能会先用90% 的时间做一些与项目无关的玩具,然后在最后一周干满50小时结束项目。当然,这会给项目经理造成极大的心理阴影。所以要了解, 一个真·码农的日常是:

  • 从不朝九晚五
  • 从不打领结
  • 从不穿高跟鞋(不论男女)
  • 通常在午饭时间到公司
  • 真·码农可能知道也可能不知道他老婆的名字,但他一定能背下来整个ASCII表
  • 真·码农从不下厨。

并且,真码农从不用PASCAL

 

情人节书单推荐

最近沉迷于分布式系统无法自拔。

恰逢小王同学要我推荐一个书单,关于CS小伙伴自我提升的。(当然算法导论这种太大路货的都不用我推荐吧)

我觉得,学习这种事情,藏拙不如献丑,知道自己哪里走了弯路,尽快走回正路才是好事。所以推荐一个书单与大家分享。

书单如下,编号不代表推荐顺序:


《编程珠玑》

  “编程珠玑”的图片搜索结果

这本书我是2016年看的。看后觉得相见恨晚。这本书越早读越好,越多读越好。语言只是工具,思想才是最无可替代的。

《大型网站技术架构:核心原理与案例分析》

 “大型网站技术架构:核心原理与案例分析”的图片搜索结果

可以说是大型网站架构入门书。从一个小网站生长成为大型网站,里面遇到的每一个无法回避的问题,都会衍生出一个相应的技术。读完此书,基本对网站架构里的术语有一个了解。

《大规模分布式系统架构与设计实战》

“大规模分布式系统架构与设计实战”的图片搜索结果

深入讲解了不同的分布式模型,也是从一个例子开始,分析不同架构之间的瓶颈,适用场景,技术难点等。读后会对分布式架构有一个比较直观的认知。

《Zookeeper:分布式过程协同技术详解》

“Zookeeper:分布式过程协同技术详解”的图片搜索结果

对于Zookeeper,这个业界知名分布式工具,它的设计理念与准则,还有具体的实现,都值得思考与借鉴。这本书如同一个地图,引导你一步一步深入下去。

《微服务设计》

“微服务设计”的图片搜索结果

微服务越来越流行,真应了那句话:“天下大势,分久必合,合久必分”。架构上也是这样。微服务的优势在哪里,如何平稳过渡,如何切分现有服务,当你读完这本书就会有一个大体的概念。

《MacTalk:人生元编程》

“mactalk·人生元编程”的图片搜索结果

池老师这个系列博客我从11年就在追了。后来他把当年的博客整理成了这个MacTalk。读完这本书,借鉴下池老师的经历,给自己的职业有一个早期规划。

《纳兰词》

“纳兰词 平装”的图片搜索结果

情人节嘛,总要推荐个应景的书才好。P.S:此书切忌看译文,写的乱七八糟的。

情人节大概就推荐这么多书吧,推荐多了总觉的会耽误正事 : D。

[教程] ch0 一个简单的web框架

Web是何如工作的

在讨论框架之前,我们需要先了解一下web是如何工作的。在浏览器里输入”https://andysim.us”并回车后,你会看到我的个人博客主页。在这一个简单的 “回车——页面”的背后,就是一个Web工作循环(这里不考虑DNS等)

Request

在你敲下回车的一瞬间,你通过浏览器,向我的服务器(andysim.us)发送了一个请求。

Web App

在我的博客里,我部署了Apache作为服务器,其上运行着Wordpress。Wordpress会接受到你的请求,处理请求后返回响应。

Response

响应会由我的服务器发回到你的电脑,里面包含整个页面的信息。你的浏览器拿到Response信息后,就可以解析,渲染成整个页面了。

Web框架浅谈

通过上面的例子,我想你已经对Web工作有一些了解了。

那么什么是Web框架呢?

我们先来简化一下模型。

根据刚才的例子,整个web应用就是一个接受Request信息,并返回Response的程序。

如下图,蓝色为request,红色为response。

Web 应用模型

如果 Web 应用什么都不干,只返回一个200作为status code(意为正常),这就是一个简单的Web 应用模型。

(这里涉及到一些网络的概念,比如request一般是由Http协议传输,而http大多又基于TCP协议通信等,本文暂不涉及到过于底层的内容)

说了这么多,我们还没有说到什么是web框架:简单来说,如果有一个程序,它负责处理接收Request,返回Response,而你只需要添加处理resquest与生成response的逻辑,这就可以当作一个web框架。

Web框架解决两大问题

围绕web应用的所有问题中,两个问题尤其突出:

  1. 我们如何将动态的URL映射到对应的逻辑?(路由)
  2. 我们如何将静态HTML与动态消息结合?(模板)

而这些就是主流的Web框架需要解决的。

本Web框架简介

本次系列教程将会带领读者从0开始做一个web框架,基于主流的MVC设计模式。

微服务应该有多“微”

https://www.innoq.com/blog/st/2014/11/how-small-should-your-microservice-be/

随着微服务概念的普及,关于“微” 的大小有了很多不同的定义。一个广受开发者支持的定义是,一个微服务应该小到只做一件事。对我个人来说,我并不觉得这是一个有意义的解释—— “一件事”这个概念本身就有不同的理解,它并不能起到有效约束微服务大小的作用。因此,我同样反对那些认为每个独立的服务应该能且仅能实现单一功能的观点。假设,一个方法根据三个输入值来计算输出——你真的认为我们有必要将这个功能抽离成一个微服务并独立部署么?

相反,我认为从另一个角度看待这个问题比较容易获得我们想要的答案。我们举一个例子:一个电子邮件系统。为了尽量简单化这个模型,我们假设它是传统的邮件系统,只有基本特性,如登录、登出,保持用户设置,创建、删除邮件,查看收件箱,创建/修改文件夹,记录通讯录,搜索邮件等。从单体模式的设计角度出发,我们可以用一个应用来实现所有功能。接下来,我们可以用模块化设计的方式将它的功能拆分成模块,比如我们可以用DDD的方式来拆分。当然,我们需要一些其他的依赖来实现某些功能——UI,数据存储,外部搜索系统等。最后,我们可能得到一个六边形或多层结构的单体应用。

所有与这个邮件系统相关的团队都必须紧密联系在一起,一旦应用有任何改变,(几乎)所有人的代码都会受到影响。这种应用被我们成为 All-or-Nothing。并且,我们只能选择运行/不运行整个系统,没法灵活的开启/关闭某些功能。对于某些团队,这样的结构就很好。但,我们假设,你的团队并不满足于这种架构,你们想要将整个应用切分成相对独立,有着自己生命周期的子应用/服务/库。

如何切分?首先,登陆/登出(或者我们成为授权系统)和用户资料这两块可以拆分成单独的服务。并且因为它们在安全上的特殊性,它们也应该被单独考量。邮件和文件夹两者联系很紧密,所以我将它们划分到一个服务(你也可以尝试拆分它们,尽管我个人并不建议)。接下来,如果我们有不同的通讯协议,如web interface, POP3, IMAP, SMAP等, 我会将每个协议的实现部分拆成对应的服务。同样,信息存储,可以被抽离成独立的服务。我们可以将附件与邮件同样作为信息文件存储,这样信息存储就变成了存储服务。带有UI和API的通讯录可以被抽离成一个独立的服务。

最后,我可能将整个邮件系统拆离成15~20个独立服务。对于任意的请求/操作,我们都可以将它映射到若干微服务之上——比如,用户在表格里输入数据之后,点击了一个按钮,数据应该被保存在这个表格里。我们会用3-5个服务组合来处理整个逻辑。

这就是我拆分微服务的思路 —— 按照业务逻辑与迭代频率区分,而非单纯的追求KISS(Keep It Small and Simple).

换言之,我认为将“服务拆分到尽可能小”并不是你的目的。如果你这样做,只能说明你将拆分服务当作了你的首要业务,而忽视了业务之间的交互性。选择更适合自己的模块方式,才是微服务的最佳实践。