IT牛人博客聚合网站 发现IT技术最优秀的内容, 寻找IT技术的价值 http://www.udpwork.com/ zh_CN http://www.udpwork.com/about hourly 1 Mon, 11 Dec 2017 14:03:01 +0800 <![CDATA[ECS 的 entity 集合维护]]> http://www.udpwork.com/item/16533.html http://www.udpwork.com/item/16533.html#reviews Sun, 10 Dec 2017 12:17:51 +0800 云风 http://www.udpwork.com/item/16533.html 最近在基于 ECS 模型做一些基础工作。实际操作时有一个问题不太明白,那就是涉及对象 (entity) 集合本身的 System 到底应该怎样处理才合适。

仔细阅读了能找到的关于 ECS 的资料,网上能找到的大多是几年前甚至 10 年前的。关于 ECS 的资料都不断地强调一些基本原则:C 里面不可以有方法(纯数据结构),S 里面不可以有状态(纯函数)。从这个角度看,Unity 其实只是一个 EC 系统,而不是 ECS 系统。从 Unity 中寻找关于 System 的设计模式恐怕并不合适。

重看了一遍暴雪在今年 GDC 上的演讲 Overwatch Gameplay Architecture and Netcode —— 这可能是最新公开的采用 ECS 模式的成功(守望先锋)实践了—— 我想我碰到的基础问题应该在里面都有答案。

从绝大多数资料看来,Entity-Component 是对 C++ 中对象模型的一个反思:基于组合,甚至是运行期组合,而不是继承,去合成对象;System 是对 OO 面向对象设计方法的反思:把方法和数据结构分离,不要把一组方法绑定在对象上,即面向对象所主张的,由对象来处理针对它的不同行为;而是由 System 来处理不同的对象聚合。

采用 ECS 模型是因为过去的 OOP 模型耦合度太高,EC/System 的方式可以用来解耦。

把对象 Entity 拆分为更基础的数据结构单元(Component),让 System 直接作用于 Component 集合而不是对象,的确可以对大部分问题解耦。正如 Wikipedia 页面上的举例:假设有一个绘图 System将迭代所有有物理组件和可视组件的 Entity ,它从可视组件中了解 Entity 的怎样绘制,再从物理组件中了解 Entity 该在哪里绘制。而另一个 System 专门处理碰撞检测,它迭代出所有有物理组件的 Entity ,处理他们的碰撞关系,负责产生碰撞事件,但这个 System 不用关心这些 Entity 是怎么绘制的,也不用知道 Entity 具体是什么东西,碰撞本身会有什么后果。再会有另一个 System 负责 Entity 的血量,血量组件记录了 Entity 的 HP 数据,这个 System 处理碰撞事件,知道当子弹击中怪物后,怪物需要扣血。

这样看都很美好,只是,游戏引擎中,往往还有一个性能相关的需求:剔除。一个 System 需要处理的对象往往是全体对象的一个子集。如果子集远小于全体的话,每帧按 Component 类型去迭代整体就有很大的性能开销。

比如,渲染 System 通常会根据摄像机在场景中的位置,剔除掉场景中的大部分物件。如果我们编写了这么一个剔除的 System ,那么在这个 System 运作之后,后续的其它 System 就不应该在整个世界中迭代可视的 Component ,而应该针对的是剔除后的集合了。

如果忽略 EC 这种基于组合的对象模型和传统 C++ 中基于继承的对象模型的实现上的差异,我们可以把 Component 仅看成是 Entity 身上的一种筛选标签 Label ,ECS 模型其实是为 System 提供了按 Label 筛选出 Entity 集合的能力。那么,我们是不是应该提供一种不太影响处理效率的,动态贴标签和撕标签的能力?空间剔除器可以给 Entity 打上需要渲染的标签,后续的渲染器可以迭代“需渲染”的组件集合。

我在 Overwatch Gameplay Architecture and Netcode 中看不到类似的设计,在我自己的实践中,Label 的想法也有不少问题。所以想了另一种方案。

据说,在守望先锋的引擎中,存在着大量的单件(singleton)组件。相关的 System 只会处理这一个组件,从里面读取数据,或把数据放在其中。我认为、用于 System 间交换数据的事件队列、剔除器的结果、这些都应该是存放在某个单件中。像渲染器这种 System 不必从 Entity 全集中迭代需要渲染的集合,而应该转而从剔除器单件里迭代一个子集。

而剔除器的工作依赖对象本身的状态变化。游戏场景中会有大量的物件,它们的状态几乎不会变化,每帧都迭代一遍是很低效的。最好是只在位置变化时才更新剔除器中的集合。

Wikipedia 页面中也谈到 system 间的通讯问题。某些对象状态改变并不频繁,所以处理需要利用观察者模式来被动触发:

The normal way to send data between systems is to store the data in components. For example, the position of an object can be updated regularly. This position is then used by other systems.

If there are a lot of different infrequent events, a lot of flags will be needed in one or more components. Systems will then have to monitor these flags every iteration, which can become inefficient. A solution could be to use the ovserver pattern. All systems that depend on an event subscribe to it. The action from the event will thus only be executed once, when it happens, and no polling is needed.

我自己在实现的时候给 ECS 框架增加这么一个设施:你可以让一个 System 关注一类 Component 的变化事件。只有这类 Component 变化后,System 才运行 —— 而普通 System 是每帧都运行的。而 Component 的新建和删除都会触发这种变更事件,我还给框架增加了一个方法,可以主动设置一个 Component 变更。同一个 Component 的变更事件在同一帧内只会触发一次。

btw, 如果你有留意 Overwatch Gameplay Architecture and Netcode 演讲,大约在第 4 分钟的时候,他展示了一张系统的结构图,其中 System 有两个方法,一个是 Update ,第二个叫 NotifyComponent ,参数是一个 Component 指针。演讲中并未谈及这个 NotifyComponent 是做什么用的,但我猜想就是做的类似工作。

在我的(基于 Lua 的)实现中,System 每帧都会收到一个变更集合,而不是每个 Component 调用一次 System 的对应函数。这是因为 Lua 中维护集合相对廉价,而函数调用相对昂贵。

这个集合每帧更新。一旦有新建 Component ,或是别的 System 主动触发变更消息,都会添加。一开始,我打算在最后将同帧删除了的 Entity 以及从 entity 中移除了 Component 的部分从集合中去掉,保证 System 遍历集合中的 entity 都是有效的。

后来发现,这种做其实是多余的。因为 System 不仅要关心 component 的变化,更要关心 Component 的消失。比如对于空间管理器来说,一个对象从空间中移除也是重要事件。好在 Entity 我们都用唯一 Id 来引用,即使删除,id 也永不复用。如果只需要在 Entity 删除时也把 id 记在这个集合里,System 自己迭代时,发现一个 Entity id 已经无效了,就说明触发了删除事件。这比单独再设计一个删除事件要简洁的多。

总结:

当 System 需要迭代一组 Entity 对他们中的 Component 做特定操作时,这个集合可以从 EntityAdmin 中用 Component 类别做筛选,也可以从 EntityAdmin 获取一个单件,由单件维护这样的集合。

一个单件中的 Entity 集合由特定的 System 来维护,这个 System 可以订阅指定的 Component 的变更事件。

变更事件包含了 Component 的创建、移除和状态变化,创建和移除事件会随着 Entity 的构建和移除自动产生,状态变化由专有 API 产生;同一帧内,一个 Component 最多只会产生一个变更事件。事件并不区分类别(创建、移除等),由 System 在迭代时自行检查 Entity id 的有效性来判别。

]]>
最近在基于 ECS 模型做一些基础工作。实际操作时有一个问题不太明白,那就是涉及对象 (entity) 集合本身的 System 到底应该怎样处理才合适。

仔细阅读了能找到的关于 ECS 的资料,网上能找到的大多是几年前甚至 10 年前的。关于 ECS 的资料都不断地强调一些基本原则:C 里面不可以有方法(纯数据结构),S 里面不可以有状态(纯函数)。从这个角度看,Unity 其实只是一个 EC 系统,而不是 ECS 系统。从 Unity 中寻找关于 System 的设计模式恐怕并不合适。

重看了一遍暴雪在今年 GDC 上的演讲 Overwatch Gameplay Architecture and Netcode —— 这可能是最新公开的采用 ECS 模式的成功(守望先锋)实践了—— 我想我碰到的基础问题应该在里面都有答案。

从绝大多数资料看来,Entity-Component 是对 C++ 中对象模型的一个反思:基于组合,甚至是运行期组合,而不是继承,去合成对象;System 是对 OO 面向对象设计方法的反思:把方法和数据结构分离,不要把一组方法绑定在对象上,即面向对象所主张的,由对象来处理针对它的不同行为;而是由 System 来处理不同的对象聚合。

采用 ECS 模型是因为过去的 OOP 模型耦合度太高,EC/System 的方式可以用来解耦。

把对象 Entity 拆分为更基础的数据结构单元(Component),让 System 直接作用于 Component 集合而不是对象,的确可以对大部分问题解耦。正如 Wikipedia 页面上的举例:假设有一个绘图 System将迭代所有有物理组件和可视组件的 Entity ,它从可视组件中了解 Entity 的怎样绘制,再从物理组件中了解 Entity 该在哪里绘制。而另一个 System 专门处理碰撞检测,它迭代出所有有物理组件的 Entity ,处理他们的碰撞关系,负责产生碰撞事件,但这个 System 不用关心这些 Entity 是怎么绘制的,也不用知道 Entity 具体是什么东西,碰撞本身会有什么后果。再会有另一个 System 负责 Entity 的血量,血量组件记录了 Entity 的 HP 数据,这个 System 处理碰撞事件,知道当子弹击中怪物后,怪物需要扣血。

这样看都很美好,只是,游戏引擎中,往往还有一个性能相关的需求:剔除。一个 System 需要处理的对象往往是全体对象的一个子集。如果子集远小于全体的话,每帧按 Component 类型去迭代整体就有很大的性能开销。

比如,渲染 System 通常会根据摄像机在场景中的位置,剔除掉场景中的大部分物件。如果我们编写了这么一个剔除的 System ,那么在这个 System 运作之后,后续的其它 System 就不应该在整个世界中迭代可视的 Component ,而应该针对的是剔除后的集合了。

如果忽略 EC 这种基于组合的对象模型和传统 C++ 中基于继承的对象模型的实现上的差异,我们可以把 Component 仅看成是 Entity 身上的一种筛选标签 Label ,ECS 模型其实是为 System 提供了按 Label 筛选出 Entity 集合的能力。那么,我们是不是应该提供一种不太影响处理效率的,动态贴标签和撕标签的能力?空间剔除器可以给 Entity 打上需要渲染的标签,后续的渲染器可以迭代“需渲染”的组件集合。

我在 Overwatch Gameplay Architecture and Netcode 中看不到类似的设计,在我自己的实践中,Label 的想法也有不少问题。所以想了另一种方案。

据说,在守望先锋的引擎中,存在着大量的单件(singleton)组件。相关的 System 只会处理这一个组件,从里面读取数据,或把数据放在其中。我认为、用于 System 间交换数据的事件队列、剔除器的结果、这些都应该是存放在某个单件中。像渲染器这种 System 不必从 Entity 全集中迭代需要渲染的集合,而应该转而从剔除器单件里迭代一个子集。

而剔除器的工作依赖对象本身的状态变化。游戏场景中会有大量的物件,它们的状态几乎不会变化,每帧都迭代一遍是很低效的。最好是只在位置变化时才更新剔除器中的集合。

Wikipedia 页面中也谈到 system 间的通讯问题。某些对象状态改变并不频繁,所以处理需要利用观察者模式来被动触发:

The normal way to send data between systems is to store the data in components. For example, the position of an object can be updated regularly. This position is then used by other systems.

If there are a lot of different infrequent events, a lot of flags will be needed in one or more components. Systems will then have to monitor these flags every iteration, which can become inefficient. A solution could be to use the ovserver pattern. All systems that depend on an event subscribe to it. The action from the event will thus only be executed once, when it happens, and no polling is needed.

我自己在实现的时候给 ECS 框架增加这么一个设施:你可以让一个 System 关注一类 Component 的变化事件。只有这类 Component 变化后,System 才运行 —— 而普通 System 是每帧都运行的。而 Component 的新建和删除都会触发这种变更事件,我还给框架增加了一个方法,可以主动设置一个 Component 变更。同一个 Component 的变更事件在同一帧内只会触发一次。

btw, 如果你有留意 Overwatch Gameplay Architecture and Netcode 演讲,大约在第 4 分钟的时候,他展示了一张系统的结构图,其中 System 有两个方法,一个是 Update ,第二个叫 NotifyComponent ,参数是一个 Component 指针。演讲中并未谈及这个 NotifyComponent 是做什么用的,但我猜想就是做的类似工作。

在我的(基于 Lua 的)实现中,System 每帧都会收到一个变更集合,而不是每个 Component 调用一次 System 的对应函数。这是因为 Lua 中维护集合相对廉价,而函数调用相对昂贵。

这个集合每帧更新。一旦有新建 Component ,或是别的 System 主动触发变更消息,都会添加。一开始,我打算在最后将同帧删除了的 Entity 以及从 entity 中移除了 Component 的部分从集合中去掉,保证 System 遍历集合中的 entity 都是有效的。

后来发现,这种做其实是多余的。因为 System 不仅要关心 component 的变化,更要关心 Component 的消失。比如对于空间管理器来说,一个对象从空间中移除也是重要事件。好在 Entity 我们都用唯一 Id 来引用,即使删除,id 也永不复用。如果只需要在 Entity 删除时也把 id 记在这个集合里,System 自己迭代时,发现一个 Entity id 已经无效了,就说明触发了删除事件。这比单独再设计一个删除事件要简洁的多。

总结:

当 System 需要迭代一组 Entity 对他们中的 Component 做特定操作时,这个集合可以从 EntityAdmin 中用 Component 类别做筛选,也可以从 EntityAdmin 获取一个单件,由单件维护这样的集合。

一个单件中的 Entity 集合由特定的 System 来维护,这个 System 可以订阅指定的 Component 的变更事件。

变更事件包含了 Component 的创建、移除和状态变化,创建和移除事件会随着 Entity 的构建和移除自动产生,状态变化由专有 API 产生;同一帧内,一个 Component 最多只会产生一个变更事件。事件并不区分类别(创建、移除等),由 System 在迭代时自行检查 Entity id 的有效性来判别。

]]>
0
<![CDATA[知识付费时代的来临]]> http://www.udpwork.com/item/16532.html http://www.udpwork.com/item/16532.html#reviews Sat, 09 Dec 2017 19:36:52 +0800 唐巧 http://www.udpwork.com/item/16532.html

就在前几天,喜马拉雅对外宣布其知识狂欢节3 天卖了 1.96 亿。我想,知识付费的时代看起来真的来了。

其实我一直在思考知识付费这个类型的产品,这里面有一些产品基本上死掉的,比如分答,在行。有一些产品发展得还行,比如「得到」,喜马拉雅,极客时间,知乎 Live,网易云课堂,微博问答,知识星球,混沌大学,GitChat。还有一些新产品不断出现,比如小专栏,有书共读,一块听听。

我们拿最成功的「得到」来说吧。我之前一直看不起「得到」,说实话在行业内也有不少人黑罗胖,我自己也认为,罗胖的罗辑思维虽然给人有一些帮助,但也有一些不太客观和理性的结论,另外罗胖的个人能力天花板会极大地限制他输出的内容数量和质量。所以我自己之前一直对「得到」是一种中立偏负面的看法。

后来我发现我想错了。为什么呢?因为「得到」里面,已经基本上不是罗胖一人给大家分享知识了,它成为了一个平台,这个平台上,由各行业的专家和精英来贡献知识。这就像什么呢?我觉得这就像一个互联网时代的出版社。

「得到」的非专栏内容

「得到」的非专栏内容包括「罗辑思维」和「李翔知识内参」以及「每天听本书」。这部分内容都非常轻,主要是为了吸引大家留下来。随便消费一些内容,让大家感觉到学到了一些知识,解决知识的焦虑感。

但「得到」核心的、能够让用户有可能系统性获取知识的课程,应该是「得到」的专栏。

「得到」的专栏课程

「得到」的专栏课程,本质上就是一个脱离了出版行业的多媒体「图书」而已,外加多了一个时间纬度让订阅的读者有一些「服务感」。

以前我们去书店买书,现在我们去「得到」买同步出版的「图书」。颠覆掉出版社,以及通过互联网的聚光效应,会使得优质内容的收益无比大,也同时使得内容无比好。

当你能够理解「得到」本质上做的是传统图书出版行业做的事情后,你就能看出它的牛逼之处了。

  • 首先,「得到」颠覆掉了传统图书出版行业的传播成本。在图书行业,一般的作者只能拿到 8% 的版税。而在「得到」,作者可以拿到 50% 的版税。翻翻得到的专栏,凭一个专栏一年挣上百万轻轻松松。
  • 然后,传统的图书出版行业太过于陈旧和体制内,造成内容的生产得经过层层审核和发行。我当年的《iOS 开发进阶》从印出来到京东上架就要 3 个月。而「得到」可以用卖期货的方式卖书。一本书还没写完,给个大纲就可以开卖了。大家还能有像追美剧一样的追更新的参与感。
  • 最后,「得到」的内容以音频为主,文本为辅,加上每期在篇幅上的限制,极大地降低了知识获取的门槛。你基本上只需要每天花几分钟,就可以跟上一本书的内容。最终,不知不觉,你就看完了一本书。

「得到」的逻辑

一个出版平台刚开始最重要的是什么?当然是流量!所以在刚开始,「得到」的逻辑是吸引大 V 在平台上开专栏。大 V 其实就是自带流量的网红,这些人可以很好的让自己的流量变现的同时,也增强了「得到」的平台影响力。

但是,大家必须意识到,大 V 其实没有那么多货一直写,虽然大 V 们以订阅的形式在产出内容,但是就像罗胖自己无法一直输出更多的罗辑思维那样,大 V 们的知识输出也是有一定极限的。所以,订阅模式其实和直播一样,只是给内容增加了「时间」这个维度,让它增加了一定的实时性和参与感。

那么「得到」怎么办呢?这恰恰是「得到」作为平台的价值。它作为平台,可以推那些水平高但是本身不是大 V 的人,这些人就像当年上北京卫视的郭德纲,上最强大脑的 Doctor 魏一样,自己水平很厉害,但是需要平台来捧。

「得到」只需要派出一群有着星探一样眼光的人,找出这些黑马,然后就可以让平台的价值体现出来。而且事实上,找出这样的人比持续依赖仅有的那么些大 V 要容易得多。

知识付费产品的困境

知识付费产品的困境是人性的懒。相比于娱乐来说,学习看起是一个极其反人性的事情。所以,虽然得到一直强调它是一所「终身大学」,但是真正有觉悟在这里面坚持学习的人还是偏少。

所以,如果你仔细分析「得到」的产品设计,你就会发现它一直在努力让学习这件事情不那么反人性:从罗胖最开始做的罗辑思维节目开始,得到就一直试图让学知识就像听「评书」一样让人觉得轻松又觉得有收获。包括每天听本书,也是一种快餐式的知识消费方式。得到还做了一个发现栏目,将很多泛知识也放到里面,我上次居然在里面还发现了「如何科学地预防和治疗感冒」的文章。

以上这些内容其实没有专栏内容那么深入和系统,但是却可以让人们稍微坚持下来一些。毕竟相对来说肯花 199 订阅专栏内容的人偏少。

知识付费产品的垂直化

如果你仔细观察,就会发现知识付费产品的垂直化非常明显,各个领域都尝试做知识付费的平台。比如极客时间就希望做成 IT 类知识付费平台。这一类平台能成功吗?我想了很久,当前的结论如下,不一定对。

我觉得这一类的平台都可以成功。因为本质上还是看这一类的平台能否做到「平台」这个级别。要做到平台这个级别,它需要搞定两方面:一方面是高质量的内容;另一方面是愿意消费内容的用户。在初期,这两者的结合体就是大 V。大 V 通常在自己的领域本来就很有建树,同时大 V 又自带粉丝。所以,一个平台如果在初期能够搞定几个大 V,基本上就可以暂时立足了。

「得到」的立足,初期就是依赖通过「罗辑思维」节目给罗胖带来的粉丝和影响力,然后又叠加上李翔这个大 V 的影响力。罗胖明白这个是初期平台的立足之本,所以把这部分内容完全免费,以增加平台的吸引力。然后,「得到」就开始一个一个死磕大 V 了,前几个大 V 立下高收入的 Flag 之后,后面的大 V 就好谈多了。雪球滚起来之后,「得到」就可以依赖自己平台的影响力自己推网红,慢慢去掉对大 V 的依赖以及大 V 在内容持续性输出上的匮乏。

「得到」是这样,极客时间也是这样。不过极客时间的问题就是他免费的内容还不够有吸引力,付费的内容依赖的大 V 粉丝数还不够大。不可否认,朱赟和邱岳都是非常厉害的人物,但是其实极客时间更需要像李开复、Fenng、池建强这样的超级网红,通过超级网红把流量带过来,才可以放大其他人的影响力。

如果说极客时间还在努力向「得到」学习的话,那么像混沌大学这类知识付费平台,还始终停留在内容吸引人这个层面。一个视频内容几个小时,其实是非常反人性的。不过混沌大学的定位人群是那些想读商学院但是又没钱的人,所以应该还是会有用户,但是这个用户量级,很难成为一个有影响力的平台。

而知识星球则完全是一个大 V 的变现平台,我个人认为平台本身的价值还是比较小的,所以未来注定平台不可能抽取过高的分成。而像「得到」这类平台,却可以做到和作者五五分成。

未来

渐渐地,人们可能会发现,知识付费其实最大的问题不是价格和内容,而是时间。每个人每天就只有那么点时间学习,而这么多海量优质内容在网上,他们需要理由说服自己,放弃一部分付费内容,购买其中非常少量的内容。因为他只要稍微多买一点,就会发现根本消费不过来。

从这个角度思考,未来知识付费的天花板还挺低的,因为知识付费平台不但得做到足够精良的内容,还需要有足够的流量来将内容推出去,还需要抢夺用户非常宝贵的休息时间。相比而言,学习又是一个比娱乐难坚持 10000 倍的事情。

不过即使这样,我还是很感谢这些知识付费 App,他们让这个世界的知识获得变得异常容易,也让那些愿意成长的人们不再受制于信息获取的障碍。

我希望他们每家都能够挣到足够多的钱,活得好好的。

推荐

最后,推荐「得到」的 刘润《5 分钟商学院》课程,极客时间的《邱岳的产品手记》、《朱赟的技术管理课》,都是不错的专栏内容。

]]>

就在前几天,喜马拉雅对外宣布其知识狂欢节3 天卖了 1.96 亿。我想,知识付费的时代看起来真的来了。

其实我一直在思考知识付费这个类型的产品,这里面有一些产品基本上死掉的,比如分答,在行。有一些产品发展得还行,比如「得到」,喜马拉雅,极客时间,知乎 Live,网易云课堂,微博问答,知识星球,混沌大学,GitChat。还有一些新产品不断出现,比如小专栏,有书共读,一块听听。

我们拿最成功的「得到」来说吧。我之前一直看不起「得到」,说实话在行业内也有不少人黑罗胖,我自己也认为,罗胖的罗辑思维虽然给人有一些帮助,但也有一些不太客观和理性的结论,另外罗胖的个人能力天花板会极大地限制他输出的内容数量和质量。所以我自己之前一直对「得到」是一种中立偏负面的看法。

后来我发现我想错了。为什么呢?因为「得到」里面,已经基本上不是罗胖一人给大家分享知识了,它成为了一个平台,这个平台上,由各行业的专家和精英来贡献知识。这就像什么呢?我觉得这就像一个互联网时代的出版社。

「得到」的非专栏内容

「得到」的非专栏内容包括「罗辑思维」和「李翔知识内参」以及「每天听本书」。这部分内容都非常轻,主要是为了吸引大家留下来。随便消费一些内容,让大家感觉到学到了一些知识,解决知识的焦虑感。

但「得到」核心的、能够让用户有可能系统性获取知识的课程,应该是「得到」的专栏。

「得到」的专栏课程

「得到」的专栏课程,本质上就是一个脱离了出版行业的多媒体「图书」而已,外加多了一个时间纬度让订阅的读者有一些「服务感」。

以前我们去书店买书,现在我们去「得到」买同步出版的「图书」。颠覆掉出版社,以及通过互联网的聚光效应,会使得优质内容的收益无比大,也同时使得内容无比好。

当你能够理解「得到」本质上做的是传统图书出版行业做的事情后,你就能看出它的牛逼之处了。

  • 首先,「得到」颠覆掉了传统图书出版行业的传播成本。在图书行业,一般的作者只能拿到 8% 的版税。而在「得到」,作者可以拿到 50% 的版税。翻翻得到的专栏,凭一个专栏一年挣上百万轻轻松松。
  • 然后,传统的图书出版行业太过于陈旧和体制内,造成内容的生产得经过层层审核和发行。我当年的《iOS 开发进阶》从印出来到京东上架就要 3 个月。而「得到」可以用卖期货的方式卖书。一本书还没写完,给个大纲就可以开卖了。大家还能有像追美剧一样的追更新的参与感。
  • 最后,「得到」的内容以音频为主,文本为辅,加上每期在篇幅上的限制,极大地降低了知识获取的门槛。你基本上只需要每天花几分钟,就可以跟上一本书的内容。最终,不知不觉,你就看完了一本书。

「得到」的逻辑

一个出版平台刚开始最重要的是什么?当然是流量!所以在刚开始,「得到」的逻辑是吸引大 V 在平台上开专栏。大 V 其实就是自带流量的网红,这些人可以很好的让自己的流量变现的同时,也增强了「得到」的平台影响力。

但是,大家必须意识到,大 V 其实没有那么多货一直写,虽然大 V 们以订阅的形式在产出内容,但是就像罗胖自己无法一直输出更多的罗辑思维那样,大 V 们的知识输出也是有一定极限的。所以,订阅模式其实和直播一样,只是给内容增加了「时间」这个维度,让它增加了一定的实时性和参与感。

那么「得到」怎么办呢?这恰恰是「得到」作为平台的价值。它作为平台,可以推那些水平高但是本身不是大 V 的人,这些人就像当年上北京卫视的郭德纲,上最强大脑的 Doctor 魏一样,自己水平很厉害,但是需要平台来捧。

「得到」只需要派出一群有着星探一样眼光的人,找出这些黑马,然后就可以让平台的价值体现出来。而且事实上,找出这样的人比持续依赖仅有的那么些大 V 要容易得多。

知识付费产品的困境

知识付费产品的困境是人性的懒。相比于娱乐来说,学习看起是一个极其反人性的事情。所以,虽然得到一直强调它是一所「终身大学」,但是真正有觉悟在这里面坚持学习的人还是偏少。

所以,如果你仔细分析「得到」的产品设计,你就会发现它一直在努力让学习这件事情不那么反人性:从罗胖最开始做的罗辑思维节目开始,得到就一直试图让学知识就像听「评书」一样让人觉得轻松又觉得有收获。包括每天听本书,也是一种快餐式的知识消费方式。得到还做了一个发现栏目,将很多泛知识也放到里面,我上次居然在里面还发现了「如何科学地预防和治疗感冒」的文章。

以上这些内容其实没有专栏内容那么深入和系统,但是却可以让人们稍微坚持下来一些。毕竟相对来说肯花 199 订阅专栏内容的人偏少。

知识付费产品的垂直化

如果你仔细观察,就会发现知识付费产品的垂直化非常明显,各个领域都尝试做知识付费的平台。比如极客时间就希望做成 IT 类知识付费平台。这一类平台能成功吗?我想了很久,当前的结论如下,不一定对。

我觉得这一类的平台都可以成功。因为本质上还是看这一类的平台能否做到「平台」这个级别。要做到平台这个级别,它需要搞定两方面:一方面是高质量的内容;另一方面是愿意消费内容的用户。在初期,这两者的结合体就是大 V。大 V 通常在自己的领域本来就很有建树,同时大 V 又自带粉丝。所以,一个平台如果在初期能够搞定几个大 V,基本上就可以暂时立足了。

「得到」的立足,初期就是依赖通过「罗辑思维」节目给罗胖带来的粉丝和影响力,然后又叠加上李翔这个大 V 的影响力。罗胖明白这个是初期平台的立足之本,所以把这部分内容完全免费,以增加平台的吸引力。然后,「得到」就开始一个一个死磕大 V 了,前几个大 V 立下高收入的 Flag 之后,后面的大 V 就好谈多了。雪球滚起来之后,「得到」就可以依赖自己平台的影响力自己推网红,慢慢去掉对大 V 的依赖以及大 V 在内容持续性输出上的匮乏。

「得到」是这样,极客时间也是这样。不过极客时间的问题就是他免费的内容还不够有吸引力,付费的内容依赖的大 V 粉丝数还不够大。不可否认,朱赟和邱岳都是非常厉害的人物,但是其实极客时间更需要像李开复、Fenng、池建强这样的超级网红,通过超级网红把流量带过来,才可以放大其他人的影响力。

如果说极客时间还在努力向「得到」学习的话,那么像混沌大学这类知识付费平台,还始终停留在内容吸引人这个层面。一个视频内容几个小时,其实是非常反人性的。不过混沌大学的定位人群是那些想读商学院但是又没钱的人,所以应该还是会有用户,但是这个用户量级,很难成为一个有影响力的平台。

而知识星球则完全是一个大 V 的变现平台,我个人认为平台本身的价值还是比较小的,所以未来注定平台不可能抽取过高的分成。而像「得到」这类平台,却可以做到和作者五五分成。

未来

渐渐地,人们可能会发现,知识付费其实最大的问题不是价格和内容,而是时间。每个人每天就只有那么点时间学习,而这么多海量优质内容在网上,他们需要理由说服自己,放弃一部分付费内容,购买其中非常少量的内容。因为他只要稍微多买一点,就会发现根本消费不过来。

从这个角度思考,未来知识付费的天花板还挺低的,因为知识付费平台不但得做到足够精良的内容,还需要有足够的流量来将内容推出去,还需要抢夺用户非常宝贵的休息时间。相比而言,学习又是一个比娱乐难坚持 10000 倍的事情。

不过即使这样,我还是很感谢这些知识付费 App,他们让这个世界的知识获得变得异常容易,也让那些愿意成长的人们不再受制于信息获取的障碍。

我希望他们每家都能够挣到足够多的钱,活得好好的。

推荐

最后,推荐「得到」的 刘润《5 分钟商学院》课程,极客时间的《邱岳的产品手记》、《朱赟的技术管理课》,都是不错的专栏内容。

]]>
0
<![CDATA[宝塔面板使用腾讯云COS备份]]> http://www.udpwork.com/item/16531.html http://www.udpwork.com/item/16531.html#reviews Fri, 08 Dec 2017 20:20:49 +0800 s5s5 http://www.udpwork.com/item/16531.html 腾讯云每月大约提供免费对象存储 COS有 50 GB,用他来做网站数据的定时备份(特别是主机放在腾讯云上)再好不过了,但宝塔面板还没有提供一键工具来备份。没关系,自已动手丰衣足食,在学习COS 文档后,发现在后台定时执行COSCMD 工具即可。

首先在COS控制台建立一个和自己主机同地域的 Bucket ,同地域才能发挥机房内网高速上传的优势

相同地域内腾讯云产品访问,将会自动使用内网连接,不产生流量费用。因此选购腾讯云不同产品时,建议尽量选择相同地域,减少您的费用。

以腾讯 CVM 访问 COS 为例,判断是否使用内网访问 COS 的方法: 在 CVM 上 ping COS 域名,若返回内网 IP,则表明 CVM 和 COS 之间是内网访问,否则为外网访问。 内网 IP 地址一般形如 10...、100...等。

记得勾选:私有读写

然后,pip 安装 COSCMD

pip install coscmd

升级一下

pip install coscmd -U

配置参数

COSCMD 工具在使用前需要进行参数配置。用户可以直接编辑~/.cos.conf文件,也可以通过如下命令来配置:

coscmd config -a <access_id> -s <secret_key> -u <appid> -b <bucketname> -r <region> [-m <max_thread>] [-p <parts_size>]

上述示例中使用”<>”的字段为必选参数,使用”[]”的字段为可选参数。其中:

名称 描述 有效值
secret_id 必选参数,APPID 对应的密钥 ID,可从控制台获取,参考基本概念 字符串
secret_key 必选参数,APPID 对应的密钥 Key,可从控制台获取,参考基本概念 字符串
appid 必选参数,需要进行操作的 APPID,可从控制台获取,参考基本概念 数字
bucketname 必选参数,指定的存储桶名称, 需要提前在控制台建立,参考创建存储桶 字符串
region 必选参数,存储桶所在地域。参考可用地域 字符串
max_thread 可选参数,多线程上传时的最大线程数(默认为 5),有效值:1~10 数字
parts_size 可选参数,分块上传的单块大小(单位为 M,默认为 1M),有效值:1~10 数字

配置完成之后的.cos.conf文件内容示例如下所示:

secret_id = AChT4ThiXAbpBDEFGhT4ThiXAbpHIJK
secret_key = WE54wreefvds3462refgwewerewr
appid = 1234567890
bucket = ABC
region = cn-south
max_thread = 5
part_size = 1

然后在宝塔面板的计划任务中先竟添加两个定时任务(比如每周一早三点)

  • 备份数据库
  • 备份网站

这两个任务执行后会生成两个大压缩包,我们上传到 COS 即可,所在再添加Shell脚本 任务,使用 COSCMD 命令输入

coscmd upload -r /www/backup/ /

这个命令会把/www/backup/目录(宝塔默认备份目录),上传到 COS 的/根目录(可按自己情况调整)

然后执行一下,试试,然后去 COS 控制台一看,挖,网站备份好啦~~~

要说明一下的是,这个命令,上传 COS 时同名文件会覆盖。

特别注意:本方法只能在腾讯云 CVM 和 COS 同地域情况下用,不同地域太慢,上传不了所有文件。

扫码关注米随随

]]>
腾讯云每月大约提供免费对象存储 COS有 50 GB,用他来做网站数据的定时备份(特别是主机放在腾讯云上)再好不过了,但宝塔面板还没有提供一键工具来备份。没关系,自已动手丰衣足食,在学习COS 文档后,发现在后台定时执行COSCMD 工具即可。

首先在COS控制台建立一个和自己主机同地域的 Bucket ,同地域才能发挥机房内网高速上传的优势

相同地域内腾讯云产品访问,将会自动使用内网连接,不产生流量费用。因此选购腾讯云不同产品时,建议尽量选择相同地域,减少您的费用。

以腾讯 CVM 访问 COS 为例,判断是否使用内网访问 COS 的方法: 在 CVM 上 ping COS 域名,若返回内网 IP,则表明 CVM 和 COS 之间是内网访问,否则为外网访问。 内网 IP 地址一般形如 10...、100...等。

记得勾选:私有读写

然后,pip 安装 COSCMD

pip install coscmd

升级一下

pip install coscmd -U

配置参数

COSCMD 工具在使用前需要进行参数配置。用户可以直接编辑~/.cos.conf文件,也可以通过如下命令来配置:

coscmd config -a <access_id> -s <secret_key> -u <appid> -b <bucketname> -r <region> [-m <max_thread>] [-p <parts_size>]

上述示例中使用”<>”的字段为必选参数,使用”[]”的字段为可选参数。其中:

名称 描述 有效值
secret_id 必选参数,APPID 对应的密钥 ID,可从控制台获取,参考基本概念 字符串
secret_key 必选参数,APPID 对应的密钥 Key,可从控制台获取,参考基本概念 字符串
appid 必选参数,需要进行操作的 APPID,可从控制台获取,参考基本概念 数字
bucketname 必选参数,指定的存储桶名称, 需要提前在控制台建立,参考创建存储桶 字符串
region 必选参数,存储桶所在地域。参考可用地域 字符串
max_thread 可选参数,多线程上传时的最大线程数(默认为 5),有效值:1~10 数字
parts_size 可选参数,分块上传的单块大小(单位为 M,默认为 1M),有效值:1~10 数字

配置完成之后的.cos.conf文件内容示例如下所示:

secret_id = AChT4ThiXAbpBDEFGhT4ThiXAbpHIJK
secret_key = WE54wreefvds3462refgwewerewr
appid = 1234567890
bucket = ABC
region = cn-south
max_thread = 5
part_size = 1

然后在宝塔面板的计划任务中先竟添加两个定时任务(比如每周一早三点)

  • 备份数据库
  • 备份网站

这两个任务执行后会生成两个大压缩包,我们上传到 COS 即可,所在再添加Shell脚本 任务,使用 COSCMD 命令输入

coscmd upload -r /www/backup/ /

这个命令会把/www/backup/目录(宝塔默认备份目录),上传到 COS 的/根目录(可按自己情况调整)

然后执行一下,试试,然后去 COS 控制台一看,挖,网站备份好啦~~~

要说明一下的是,这个命令,上传 COS 时同名文件会覆盖。

特别注意:本方法只能在腾讯云 CVM 和 COS 同地域情况下用,不同地域太慢,上传不了所有文件。

扫码关注米随随

]]>
0
<![CDATA[基础优化-最不坏的哈希表]]> http://www.udpwork.com/item/16530.html http://www.udpwork.com/item/16530.html#reviews Fri, 08 Dec 2017 19:14:03 +0800 skywind http://www.udpwork.com/item/16530.html 哈希表性能优化的方法有很多,比如:

  • 使用双 hash 检索冲突
  • 使用开放+封闭混合寻址法组织哈希表
  • 使用跳表快速定位冲突
  • 使用 LRU 缓存最近访问过的键值,不管表内数据多大,短时内访问的总是那么几个
  • 使用更好的分配器来管理 keyvaluepair 这个节点对象

上面只要随便选两条你都能得到一个比 unordered_map 快不少的哈希表,类似的方法还有很多,比如使用除以质数来归一化哈希值(x86下性能最好,整数除法非常快,但非x86就不行了,arm还没有整数除法指令,要靠软件模拟,代价很大)。

哈希表最大的问题就是过分依赖哈希函数得到一个正态分布的哈希值,特别是开放寻址法(内存更小,速度更快,但是更怕哈希冲突),一旦冲突多了,或者 load factor 上去了,性能就急剧下降。

Python 的哈希表就是开放寻址的,速度是快了,但是面对哈希碰撞攻击时,挂的也是最惨的,早先爆出的哈希碰撞漏洞,攻击者可以通过哈希碰撞来计算成千上万的键值,导致 Python / Php / Java / V8 等一大批语言写成的服务完全瘫痪。

后续 Python 推出了修正版本,解决方案是增加一个哈希种子,用随机数来初始化它,这都不彻底,开放寻址法对hash函数的好坏仍然高度敏感,碰到特殊的数据,性能下降很厉害。

经过最近几年的各种事件,让人们不得不把目光从“如何实现个更快的哈希表 ”转移到 “如何实现一个最不坏的哈希表 ”来,以这个新思路重新思考 hash 表的设计。

哈希表定位主要靠下面一个操作:

index_pos = hash(key) % index_size;

来决定一个键值到底存储在什么地方,虽然 hash(key) 返回的数值在 0-0xffffffff 之前,但是索引表是有大小限制的,在 hash 值用 index_size 取模以后,大量不同哈希值取模后可能得到相同的索引位置,所以即使哈希值不一样最终取模后还是会碰撞

第一种思路是尽量避免冲突,比如双哈希,比如让索引大小 index_size 保持质数式增长,但是他们都太过依赖于哈希函数本身;所以第二种思路是把注意力放在碰撞发生了该怎么处理上,比如多层哈希,开放+封闭混合寻址法,跳表,封闭寻址+平衡二叉树。

优化方向

今天我们就来实现一下最后一种,也是最彻底的做法:封闭寻址+平衡二叉树,看看最终性能如何?能否达到我们的要求?实现起来有哪些坑?其原理简单说起来就是,将原来封闭寻址的链表,改为平衡二叉树:

传统的封闭寻址哈希表,也是 Linux / STL 等大部分哈希表的实现,碰到碰撞时链表一长就挂掉,所谓哈希表+平衡二叉树就是:

将原来的链表(有序或者无序)换成平衡二叉树,这是复杂度最高的做法,同时也是最可靠的做法。发生碰撞时能将时间复杂度由 O(N) 降低到 O(logN),10个节点,链表的复杂度就是 10,而使用平衡二叉树的复杂度是 3;100个节点前者的时间是100,后者只有6.6 越往后差距约明显。

面临问题

树表混合结构(哈希表+平衡二叉树)的方法,Hash table – Wikipedia 上面早有说明,之所以一直没有进入各大语言/SDK的主流实现主要有四个问题:

  • 比起封闭寻址(STL,Java)来讲,节点少时,维持平衡(旋转等)会比有序链表更慢。
  • 比起开放寻址(python,v8实现)来讲,内存不紧凑,缓存不够友好。
  • 占用更多内存:一个平衡二叉树节点需要更多指针。
  • 设计比其他任何哈希表都要复杂。

所以虽然早就有人提出,但是一直都是一个边缘方法,并未进入主流实现。而最近两年随着各大语言暴露出来的各种哈希碰撞攻击,和原有设计基本无力应对坏一些的情况,于是又开始寻求这种树表混合结构是否还有优化的空间。

先来解决第一个问题,如果二叉树节点只有3-5个,那还不如使用有序链表,这是公认的事实,Java 8 最新实现的树表混合结构里,引入了一个 TREEIFY_THRESHOLD = 8 的常量,同一个索引内(或者叫同一个桶/slot/bucket内),冲突键值小于 8 的,使用链表,大于这个阈值时当前 index 内所有节点进行树化操作(treeify)。

Java 8 靠这个方法有效的解决了第一个问题和第三个问题,最终代替了原有 java4-7 一直在使用的 HashMap 老实现,那么我们要使用 Java 8 的方法么?

不用,今天我们换种实现。

java 8 经过了反复测试选择了 TREEIFY_THRESHOLD=8 这个阈值,因为它 treeify 的过程的代价是非常大的:

  • 要为每个节点重新分配一个 TreeNode ,分配内存过本身就是很大的一个开销。
  • 构造二叉平衡树,其维护平衡即便不需要再平衡,进去也是要走一堆逻辑判断。
  • 当节点降低到 UNTREEIFY_THRESHOLD=6这个阈值以内,又要把树节点全部删除掉,换回链表去,那如果 index 内节点就在 5-9 之间波动,这就傻了,这是第三项成本。
  • 每次插入节点时要判断这是链表还是树,到阈值没有,大家知道这些额外增加的分支都是对 CPU的流水线十分不友好的。

如果我们能想办法不多分配内存(开销最大),那基本上这个阈值可以降低到 4,再此基础上使用更紧凑的搜索代码,在 gcc 那里可以获得更好的优化,现代CPU对小型分支预测的处理都比较完善,x86下还有 cmov 指令解决跳转,树搜索时多用紧凑的三元式:

node = (key < node->key)? node->left : node->right;

最终生成 cmov 的代码是没有分支的,实测新老 gcc,新版本 gcc 生成 cmov指令性能是没有用 cmov 的一倍以上,诸如此类的代码层的优化技巧我们尽量用起来,让这个阈值降低到 2-3 的时候,也就是说传统的有序链表操作已经没有优化余地了,但是通过降低 treeify 过程中再次分配内存的消耗,树维护的消耗,以及提高树的性能,可以把阈值从 8 降低到 4,再通过代码层的优化降低到 2-3,也就是说,最终链表只有在 2-3个节点以内才比树更快,如果做到这一点,那好,我们的故事就变了

再链表/树的性能对比阈值降低到 2-3个节点后,接下来我们就可以完全抛弃链表,index里面只保留树结构,然后给平衡二叉树增加几条 fast path,让其性能尽量接近链表,比如大部分时候(hash值较为平均),每个 index只有 0-2个节点的时候,我们的这些快速路径就生效了:

  • 当某 index 下面节点数为 0 时,要增加一个节点,我们直接更改树的根节点即可,完全跳过后面的各种再平衡逻辑,以及比较逻辑。
  • 如果本来有 1 个节点,要增加一个新节点,那么也完全不需要再平衡,一次比较后直接设置成左或者右儿子同样跳过再平衡逻辑。
  • 如果是2个节点变3个节点,那同样很简单,把最大和最小节点找出来,放在剩下节点的左右子树即可,同样跳过再平衡。

这样 3个节点以内我们采取一次性直接构造树的方法免去再平衡逻辑,基本上保证了3个节点以内的树操作和链表代价相同,又因为自始至终只有一种节点,避免了 java8 中同节点可能需要两次内存分配的问题,又大大缩小了链表和树的性能差距,只使用树结构,免除了 treeify, untreeify 两道逻辑和开头的判断,最终达到和链表性能差不多的树结构。

指针优化

再看 TreeNode 内容,大家猜猜一个 java.util.HashMap.TreeNode 对象有多少个成员变量?不说不知道,一说吓一跳,整整 9个变量(HashMap.Node 本身有4个对象,其派生类 HashMap.TreeNode 又增加了5个新变量)。还有每次分配一断内存其实都有一个 overhead ,表明这个内存是从哪里分配出来的,释放的时候好找到归还的位置,全部加在一起有 10个变量,变量越多缓存越不集中,每次操作的成本也越大,我没研究过 java 的对象内存布局,就以 C语言为例 10个变量 32位下要占用 40字节的空间,64位下占用更多。

其中关键的一处冗余是 Java 8 下面为每个节点维护了 next, prev 指针,这两个指针何用?遍历哈希表的时候用。大家知道如果节点不维护 next, prev 而遍历哈希表的话,传统遍历法要遍历所有 index,即便一个 index 为空也要走一圈,python就是这么干的,那么在 load factor 比较高的时候,这么做没问题;而 load factor 很低时,也就是说 index 表很大,但是整个 hash表里的节点却很少时,遍历就会跑很多空的 index,效率很低。

所以 java8 里一个 treeify 过后的索引里,所有节点不当放在树里,还同时放到了链表里了,虽然搜索不搜索链表而是直接搜索树,但是每次插入和删除需要同时操作树和链表,来保证遍历的时候可以不跑空节点,这又是一件肯爹的事情。

那么我们可以把 next, prev 省掉么?当然可以,既然我们已经全部都使用平衡二叉树了,本身平衡二叉树就可以方便的遍历而不需要额外的存储,只是这个 index 里的二叉树遍历完了怎么寻找下一个有效节点?很简单,我们把 next, prev 指针加到 index上,让有数据的 index 连在一起,这个 index 内的所有树节点遍历完了后直接跳到下一个有数据的 index 里的节点上去。这样我们把每个节点都有的 next/prev 拿了出来,放到了 index 上面,插入和删除节点都不需要再同时维护树和链表两个结构,仅仅再节点数 0->1, 1->0时需要把 index 移进/移除链表,保证了遍历时不会跑空的 index,同时为每个节点都省掉了两个指针。

内存优化

开放寻址的 hash 表的优势是:省内存+不需要额外分配内存+缓存友好,使用树表混合结构后,内存显然我们是省不了多少了,但是后两个目的却可以想办法达到,在哈希表内按照 load factor 和初始索引大小计算出 rehash 前最多会有多少个节点,然后一次性分配一整块内存,按照 node 的大小切割后放入哈希表内部的 freelist 中,rehash的时候再重新计算分配另外一块整块内存,这样做有三个明显的好处:

  • 内存分配/释放,基本是O(1)极速了,消耗基本可以忽略。
  • 表内所有节点再内存上都是连续的,缓存十分友好。
  • 免去了为每个节点单独分配内存的头部占用(overhead),相当于又省掉了一个指针。

使用该方法进行内存优化过的树表结构,性能得以再次提升。

其他优化

设计一个数据结构,本身就是再做选择,做权衡,选择了树表混合结构,看重的是它的终极平衡特性,而上面所有工作,其实都是在死磕它的短处,各处可以优化的细节还很多:

比如 rehash 删除原有二叉平衡树时,不需要用标准 rbtree/avl 删除一个节点每次再平衡的传统方法,可以使用一种类似“摘叶子”的方法解构二叉树,每次选择最近的叶子节点删除,没有就往上回溯父节点,比标准方法老老实实删除快很多。

比如传统二叉树的增删改查都是要比较键值的,而比较键值是个很费时间的操作(比如用字符串做键值时),但是既然我们有哈希值,节点又是 unordered 的,那么在操作二叉树时,我们线比较哈希值,先用哈希值的大小进行比较,哈希值相同了再实际比较键值,这样可以大大避免频繁的键值比较,最终每个节点大部分只有1次键值比较,少部分有2次以上比较。

。。。。

基准测试

引入一个新技术再大部分情况下如果比老技术更慢就别玩了,不能光说不练嘛,我们测试一下上面说的这些方法最终效果如何。首先看看正常情况下,即 hash 值均匀分布的情况下它的表现,对比的是几个主流编译器自带的 STL 里的 unordered_map,为了避免额外因素干扰,两边都用 int 值做主键,使用了相同的哈希函数:hash(x) = x,这也是大部分 STL 实现默认的 hash函数:

上面的 avl-hash 是我们上面实现的树表混合结构,二叉平衡树我用的是 AVL,这是个人习惯问题了,关于 avl / rbtree 的性能其实是差不多的,我以前讨论过,这里不展开了:

韦易笑:AVL树,红黑树,B树,B+树,Trie树都分别应用在哪些现实场景中?

你也可以用 rbtree 代替,这里纯粹一个个人口味问题,下面比较了 Windows / Linux 下面vc和 gcc 自带的 std::unorderedmap ,从查询时间看,两个差不多,avl-hash 略微领先一些,后面插入时间和删除时间 std::unorderedmap 很吃亏,因为我们的树表混合结构里自己管理了节点内存分配,而 unordered_map 却使用了默认内存分配器(会用系统默认的内存分配),所以排除内存分配开销,二者差距应该没上面那么大,比如 ubuntu 下面 libc 内存分配器对于 128 字节以内的对象分配直接使用 fastbin (一种freelist),性能比windows下的好很多,接近 avl-hash 内部自己管理内存的速度,因此性能差距会比 windows 小不少。

冲突测试

大部分哈希表都让用户 “选择一个合适的哈希函数”,而什么哈希函数才最合适?才分布最好?谁他妈都说不清,特别时对于未知键值到底会是怎样的情况下,你完全没根据去选择一个“好”的哈希函数,比如你给 python 重新设计一遍字典,你能预知成千上万用户会用它存什么吗?比如你实现一个 redis / mq 之类的服务,你知道用户会发送什么键值上来?而这类服务,同一个哈希表里 hold 住百万级别的键值是很常见的事情。

未来键值不确定时,必须把希望寄托在一个“先验”的好的 hash函数上么?不需要了,这回我们可以靠哈希表自己了。前面实现了那么多,究竟我们实现的 “最不坏的哈希表”,不坏到什么程度?当发生碰撞时,或者 load-factor 比较高的情况下,性能是否能达到预期?

激动人心的时刻终于到了,第一项,搜索测试:

可以看出搜索对比,横轴表示冲突节点数量,纵轴表示测试耗时,可以看出随着碰撞的增加,树表混合结构的查询时间基本只是从 0毫秒增加到了 1-2毫秒,而 unordered_map 的搜索时间却是抛物线上升到1.4秒了。

插入和删除耗时类似

删除操作都比较费时,unordered_map 在三万个节点时基本接近1.6秒,而我们的树表混合结构耗时只有少许增加。

相关代码:https://github.com/skywind3000/avlmini

通过上面的工作,我们得到了这个最不坏的哈希表,我们用它做一个类似 redis / mq 的服务,存储百万级别的键值不用太过在意数据哈希值分布不均匀所带来的问题了,也不用担心碰撞攻击会让其性能跌落到深渊。

我们没法完全依赖哈希函数,当哈希函数靠不住时,还得靠哈希表本身,这叫打铁还需自身硬嘛,最终测试基本符合我们的初衷和预期。

(完)

]]>
哈希表性能优化的方法有很多,比如:

  • 使用双 hash 检索冲突
  • 使用开放+封闭混合寻址法组织哈希表
  • 使用跳表快速定位冲突
  • 使用 LRU 缓存最近访问过的键值,不管表内数据多大,短时内访问的总是那么几个
  • 使用更好的分配器来管理 keyvaluepair 这个节点对象

上面只要随便选两条你都能得到一个比 unordered_map 快不少的哈希表,类似的方法还有很多,比如使用除以质数来归一化哈希值(x86下性能最好,整数除法非常快,但非x86就不行了,arm还没有整数除法指令,要靠软件模拟,代价很大)。

哈希表最大的问题就是过分依赖哈希函数得到一个正态分布的哈希值,特别是开放寻址法(内存更小,速度更快,但是更怕哈希冲突),一旦冲突多了,或者 load factor 上去了,性能就急剧下降。

Python 的哈希表就是开放寻址的,速度是快了,但是面对哈希碰撞攻击时,挂的也是最惨的,早先爆出的哈希碰撞漏洞,攻击者可以通过哈希碰撞来计算成千上万的键值,导致 Python / Php / Java / V8 等一大批语言写成的服务完全瘫痪。

后续 Python 推出了修正版本,解决方案是增加一个哈希种子,用随机数来初始化它,这都不彻底,开放寻址法对hash函数的好坏仍然高度敏感,碰到特殊的数据,性能下降很厉害。

经过最近几年的各种事件,让人们不得不把目光从“如何实现个更快的哈希表 ”转移到 “如何实现一个最不坏的哈希表 ”来,以这个新思路重新思考 hash 表的设计。

哈希表定位主要靠下面一个操作:

index_pos = hash(key) % index_size;

来决定一个键值到底存储在什么地方,虽然 hash(key) 返回的数值在 0-0xffffffff 之前,但是索引表是有大小限制的,在 hash 值用 index_size 取模以后,大量不同哈希值取模后可能得到相同的索引位置,所以即使哈希值不一样最终取模后还是会碰撞

第一种思路是尽量避免冲突,比如双哈希,比如让索引大小 index_size 保持质数式增长,但是他们都太过依赖于哈希函数本身;所以第二种思路是把注意力放在碰撞发生了该怎么处理上,比如多层哈希,开放+封闭混合寻址法,跳表,封闭寻址+平衡二叉树。

优化方向

今天我们就来实现一下最后一种,也是最彻底的做法:封闭寻址+平衡二叉树,看看最终性能如何?能否达到我们的要求?实现起来有哪些坑?其原理简单说起来就是,将原来封闭寻址的链表,改为平衡二叉树:

传统的封闭寻址哈希表,也是 Linux / STL 等大部分哈希表的实现,碰到碰撞时链表一长就挂掉,所谓哈希表+平衡二叉树就是:

将原来的链表(有序或者无序)换成平衡二叉树,这是复杂度最高的做法,同时也是最可靠的做法。发生碰撞时能将时间复杂度由 O(N) 降低到 O(logN),10个节点,链表的复杂度就是 10,而使用平衡二叉树的复杂度是 3;100个节点前者的时间是100,后者只有6.6 越往后差距约明显。

面临问题

树表混合结构(哈希表+平衡二叉树)的方法,Hash table – Wikipedia 上面早有说明,之所以一直没有进入各大语言/SDK的主流实现主要有四个问题:

  • 比起封闭寻址(STL,Java)来讲,节点少时,维持平衡(旋转等)会比有序链表更慢。
  • 比起开放寻址(python,v8实现)来讲,内存不紧凑,缓存不够友好。
  • 占用更多内存:一个平衡二叉树节点需要更多指针。
  • 设计比其他任何哈希表都要复杂。

所以虽然早就有人提出,但是一直都是一个边缘方法,并未进入主流实现。而最近两年随着各大语言暴露出来的各种哈希碰撞攻击,和原有设计基本无力应对坏一些的情况,于是又开始寻求这种树表混合结构是否还有优化的空间。

先来解决第一个问题,如果二叉树节点只有3-5个,那还不如使用有序链表,这是公认的事实,Java 8 最新实现的树表混合结构里,引入了一个 TREEIFY_THRESHOLD = 8 的常量,同一个索引内(或者叫同一个桶/slot/bucket内),冲突键值小于 8 的,使用链表,大于这个阈值时当前 index 内所有节点进行树化操作(treeify)。

Java 8 靠这个方法有效的解决了第一个问题和第三个问题,最终代替了原有 java4-7 一直在使用的 HashMap 老实现,那么我们要使用 Java 8 的方法么?

不用,今天我们换种实现。

java 8 经过了反复测试选择了 TREEIFY_THRESHOLD=8 这个阈值,因为它 treeify 的过程的代价是非常大的:

  • 要为每个节点重新分配一个 TreeNode ,分配内存过本身就是很大的一个开销。
  • 构造二叉平衡树,其维护平衡即便不需要再平衡,进去也是要走一堆逻辑判断。
  • 当节点降低到 UNTREEIFY_THRESHOLD=6这个阈值以内,又要把树节点全部删除掉,换回链表去,那如果 index 内节点就在 5-9 之间波动,这就傻了,这是第三项成本。
  • 每次插入节点时要判断这是链表还是树,到阈值没有,大家知道这些额外增加的分支都是对 CPU的流水线十分不友好的。

如果我们能想办法不多分配内存(开销最大),那基本上这个阈值可以降低到 4,再此基础上使用更紧凑的搜索代码,在 gcc 那里可以获得更好的优化,现代CPU对小型分支预测的处理都比较完善,x86下还有 cmov 指令解决跳转,树搜索时多用紧凑的三元式:

node = (key < node->key)? node->left : node->right;

最终生成 cmov 的代码是没有分支的,实测新老 gcc,新版本 gcc 生成 cmov指令性能是没有用 cmov 的一倍以上,诸如此类的代码层的优化技巧我们尽量用起来,让这个阈值降低到 2-3 的时候,也就是说传统的有序链表操作已经没有优化余地了,但是通过降低 treeify 过程中再次分配内存的消耗,树维护的消耗,以及提高树的性能,可以把阈值从 8 降低到 4,再通过代码层的优化降低到 2-3,也就是说,最终链表只有在 2-3个节点以内才比树更快,如果做到这一点,那好,我们的故事就变了

再链表/树的性能对比阈值降低到 2-3个节点后,接下来我们就可以完全抛弃链表,index里面只保留树结构,然后给平衡二叉树增加几条 fast path,让其性能尽量接近链表,比如大部分时候(hash值较为平均),每个 index只有 0-2个节点的时候,我们的这些快速路径就生效了:

  • 当某 index 下面节点数为 0 时,要增加一个节点,我们直接更改树的根节点即可,完全跳过后面的各种再平衡逻辑,以及比较逻辑。
  • 如果本来有 1 个节点,要增加一个新节点,那么也完全不需要再平衡,一次比较后直接设置成左或者右儿子同样跳过再平衡逻辑。
  • 如果是2个节点变3个节点,那同样很简单,把最大和最小节点找出来,放在剩下节点的左右子树即可,同样跳过再平衡。

这样 3个节点以内我们采取一次性直接构造树的方法免去再平衡逻辑,基本上保证了3个节点以内的树操作和链表代价相同,又因为自始至终只有一种节点,避免了 java8 中同节点可能需要两次内存分配的问题,又大大缩小了链表和树的性能差距,只使用树结构,免除了 treeify, untreeify 两道逻辑和开头的判断,最终达到和链表性能差不多的树结构。

指针优化

再看 TreeNode 内容,大家猜猜一个 java.util.HashMap.TreeNode 对象有多少个成员变量?不说不知道,一说吓一跳,整整 9个变量(HashMap.Node 本身有4个对象,其派生类 HashMap.TreeNode 又增加了5个新变量)。还有每次分配一断内存其实都有一个 overhead ,表明这个内存是从哪里分配出来的,释放的时候好找到归还的位置,全部加在一起有 10个变量,变量越多缓存越不集中,每次操作的成本也越大,我没研究过 java 的对象内存布局,就以 C语言为例 10个变量 32位下要占用 40字节的空间,64位下占用更多。

其中关键的一处冗余是 Java 8 下面为每个节点维护了 next, prev 指针,这两个指针何用?遍历哈希表的时候用。大家知道如果节点不维护 next, prev 而遍历哈希表的话,传统遍历法要遍历所有 index,即便一个 index 为空也要走一圈,python就是这么干的,那么在 load factor 比较高的时候,这么做没问题;而 load factor 很低时,也就是说 index 表很大,但是整个 hash表里的节点却很少时,遍历就会跑很多空的 index,效率很低。

所以 java8 里一个 treeify 过后的索引里,所有节点不当放在树里,还同时放到了链表里了,虽然搜索不搜索链表而是直接搜索树,但是每次插入和删除需要同时操作树和链表,来保证遍历的时候可以不跑空节点,这又是一件肯爹的事情。

那么我们可以把 next, prev 省掉么?当然可以,既然我们已经全部都使用平衡二叉树了,本身平衡二叉树就可以方便的遍历而不需要额外的存储,只是这个 index 里的二叉树遍历完了怎么寻找下一个有效节点?很简单,我们把 next, prev 指针加到 index上,让有数据的 index 连在一起,这个 index 内的所有树节点遍历完了后直接跳到下一个有数据的 index 里的节点上去。这样我们把每个节点都有的 next/prev 拿了出来,放到了 index 上面,插入和删除节点都不需要再同时维护树和链表两个结构,仅仅再节点数 0->1, 1->0时需要把 index 移进/移除链表,保证了遍历时不会跑空的 index,同时为每个节点都省掉了两个指针。

内存优化

开放寻址的 hash 表的优势是:省内存+不需要额外分配内存+缓存友好,使用树表混合结构后,内存显然我们是省不了多少了,但是后两个目的却可以想办法达到,在哈希表内按照 load factor 和初始索引大小计算出 rehash 前最多会有多少个节点,然后一次性分配一整块内存,按照 node 的大小切割后放入哈希表内部的 freelist 中,rehash的时候再重新计算分配另外一块整块内存,这样做有三个明显的好处:

  • 内存分配/释放,基本是O(1)极速了,消耗基本可以忽略。
  • 表内所有节点再内存上都是连续的,缓存十分友好。
  • 免去了为每个节点单独分配内存的头部占用(overhead),相当于又省掉了一个指针。

使用该方法进行内存优化过的树表结构,性能得以再次提升。

其他优化

设计一个数据结构,本身就是再做选择,做权衡,选择了树表混合结构,看重的是它的终极平衡特性,而上面所有工作,其实都是在死磕它的短处,各处可以优化的细节还很多:

比如 rehash 删除原有二叉平衡树时,不需要用标准 rbtree/avl 删除一个节点每次再平衡的传统方法,可以使用一种类似“摘叶子”的方法解构二叉树,每次选择最近的叶子节点删除,没有就往上回溯父节点,比标准方法老老实实删除快很多。

比如传统二叉树的增删改查都是要比较键值的,而比较键值是个很费时间的操作(比如用字符串做键值时),但是既然我们有哈希值,节点又是 unordered 的,那么在操作二叉树时,我们线比较哈希值,先用哈希值的大小进行比较,哈希值相同了再实际比较键值,这样可以大大避免频繁的键值比较,最终每个节点大部分只有1次键值比较,少部分有2次以上比较。

。。。。

基准测试

引入一个新技术再大部分情况下如果比老技术更慢就别玩了,不能光说不练嘛,我们测试一下上面说的这些方法最终效果如何。首先看看正常情况下,即 hash 值均匀分布的情况下它的表现,对比的是几个主流编译器自带的 STL 里的 unordered_map,为了避免额外因素干扰,两边都用 int 值做主键,使用了相同的哈希函数:hash(x) = x,这也是大部分 STL 实现默认的 hash函数:

上面的 avl-hash 是我们上面实现的树表混合结构,二叉平衡树我用的是 AVL,这是个人习惯问题了,关于 avl / rbtree 的性能其实是差不多的,我以前讨论过,这里不展开了:

韦易笑:AVL树,红黑树,B树,B+树,Trie树都分别应用在哪些现实场景中?

你也可以用 rbtree 代替,这里纯粹一个个人口味问题,下面比较了 Windows / Linux 下面vc和 gcc 自带的 std::unorderedmap ,从查询时间看,两个差不多,avl-hash 略微领先一些,后面插入时间和删除时间 std::unorderedmap 很吃亏,因为我们的树表混合结构里自己管理了节点内存分配,而 unordered_map 却使用了默认内存分配器(会用系统默认的内存分配),所以排除内存分配开销,二者差距应该没上面那么大,比如 ubuntu 下面 libc 内存分配器对于 128 字节以内的对象分配直接使用 fastbin (一种freelist),性能比windows下的好很多,接近 avl-hash 内部自己管理内存的速度,因此性能差距会比 windows 小不少。

冲突测试

大部分哈希表都让用户 “选择一个合适的哈希函数”,而什么哈希函数才最合适?才分布最好?谁他妈都说不清,特别时对于未知键值到底会是怎样的情况下,你完全没根据去选择一个“好”的哈希函数,比如你给 python 重新设计一遍字典,你能预知成千上万用户会用它存什么吗?比如你实现一个 redis / mq 之类的服务,你知道用户会发送什么键值上来?而这类服务,同一个哈希表里 hold 住百万级别的键值是很常见的事情。

未来键值不确定时,必须把希望寄托在一个“先验”的好的 hash函数上么?不需要了,这回我们可以靠哈希表自己了。前面实现了那么多,究竟我们实现的 “最不坏的哈希表”,不坏到什么程度?当发生碰撞时,或者 load-factor 比较高的情况下,性能是否能达到预期?

激动人心的时刻终于到了,第一项,搜索测试:

可以看出搜索对比,横轴表示冲突节点数量,纵轴表示测试耗时,可以看出随着碰撞的增加,树表混合结构的查询时间基本只是从 0毫秒增加到了 1-2毫秒,而 unordered_map 的搜索时间却是抛物线上升到1.4秒了。

插入和删除耗时类似

删除操作都比较费时,unordered_map 在三万个节点时基本接近1.6秒,而我们的树表混合结构耗时只有少许增加。

相关代码:https://github.com/skywind3000/avlmini

通过上面的工作,我们得到了这个最不坏的哈希表,我们用它做一个类似 redis / mq 的服务,存储百万级别的键值不用太过在意数据哈希值分布不均匀所带来的问题了,也不用担心碰撞攻击会让其性能跌落到深渊。

我们没法完全依赖哈希函数,当哈希函数靠不住时,还得靠哈希表本身,这叫打铁还需自身硬嘛,最终测试基本符合我们的初衷和预期。

(完)

]]>
0
<![CDATA[AVL/RBTREE 实际比较]]> http://www.udpwork.com/item/16529.html http://www.udpwork.com/item/16529.html#reviews Fri, 08 Dec 2017 18:37:04 +0800 skywind http://www.udpwork.com/item/16529.html 网上对 AVL被批的很惨,认为性能不如 rbtree,这里给 AVL 树平反昭雪。最近优化了一下我之前的 AVL 树,总体跑的和 linux 的 rbtree 一样快了:

他们都比 std::map 快很多(即便使用动态内存分配,为每个新插入节点临时分配个新内存)。

项目代码在:skywind3000/avlmini
其他 AVL/RBTREE 评测也有类似的结论,见:STL AVL Map

谣言1:RBTREE的平均统计性能比 AVL 好

统计下来一千万个节点插入 AVL 共旋转 7053316 次(先左后右算两次),RBTREE共旋转 5887217 次,RBTREE看起来少是吧?应该很快?但是别忘了 RBTREE 再平衡的操作除了旋转外还有再着色,每次再平衡噼里啪啦的改一片颜色,父亲节点,叔叔,祖父,兄弟节点都要访问一圈,这些都是代价,再者平均树高比 AVL 高也成为各项操作的成本。

谣言2:RBTREE 一般情况只比 AVL 高一两层,这个代价忽略不计

纯粹谣言,随便随机一下,一百万个节点的 RBTREE 树高27,和一千万个节点的 AVL树相同,而一千万个节点的 RBTREE 树高 33,比 AVL 多了 6 层,这还不是最坏情况,最坏情况 AVL 只有 1.440 * log(n + 2) – 0.328, 而 RBTREE 是 2 * log(n + 1),也就是说同样100万个节点,AVL最坏情况是 28 层,rbtree 最坏可以到 39 层。

谣言3:AVL树删除节点是需要回溯到根节点

我以前也是这么写 AVL 树的,后来发现根据 AVL 的定义,可以做出两个推论,再平衡向上回溯时:

插入更新时:如当前节点的高度没有改变,则上面所有父节点的高度和平衡也不会改变。
删除更新时:如当前节点的高度没有改变且平衡值在 [-1, 1] 区间,则所有父节点的高度和平衡都不会改变。

根据这两个推论,AVL的插入和删除大部分时候只需要向上回溯一两个节点即可,范围十分紧凑。

谣言4:虽然二者插入一万个节点总时间类似,但是rbtree树更平均,avl有时很快,有时慢很多,rbtree 只需要旋转两次重新染色就行了,比 avl 平均

完全说反了,avl是公认的比rbtree平均的数据结构,插入时间更为平均,rbtree才是不均衡,有时候直接插入就返回了(上面是黑色节点),有时候插入要染色几个节点但不旋转,有时候还要两次旋转再染色然后递归到父节点。该说法最大的问题是以为 rbtree 插入节点最坏情况是两次旋转加染色,可是忘记了一条,需要向父节点递归,比如:当前节点需要旋转两次重染色,然后递归到父节点再旋转两次重染色,再递归到父节点的父节点,直到满足 rbtree 的5个条件。这种说法直接把递归给搞忘记了,翻翻看 linux 的 rbtree 代码看看,再平衡时那一堆的 while 循环是在干嘛?不就是向父节点递归么?avl和rbtree 插入和删除的最坏情况都需要递归到根节点,都可能需要一路旋转上去,否则你设想下,假设你一直再树的最左边插入1000个新节点,每次都想再局部转两次染染色,而不去调整整棵树,不动根节点,可能么?只是说整个过程avl更加平均而已。

比较结论

AVL / RBTREE 真的差不多,AVL被早期各种乱七八糟的实现和数学上的“统计”给害了,别盯着 linux 用了 rbtree 就觉得 rbtree 一定好过 avl了,solaris 里面大范围的使用 avltree ,完全没有 rbtree 的痕迹那。

补充测试

更新:有人比较疑惑 std::map 那么容易被超越么?无图无真相,给一下我测试的编译器和标准库版本吧,否则疑惑我在和 vc6 的 STL 做比较呢。

主要开发环境:mingw gcc 5.2.0

linux rbtree with 10000000 nodes:
insert time: 4451ms, height=32
search time: 2037ms error=0
delete time: 548ms
total: 7036ms

avlmini with 10000000 nodes:
insert time: 4563ms, height=27
search time: 2018ms error=0
delete time: 598ms
total: 7179ms

std::map with 10000000 nodes:
insert time: 4281ms
search time: 4124ms error=0
delete time: 767ms
total: 9172ms

linux rbtree with 1000000 nodes:
insert time: 355ms, height=26
search time: 171ms error=0
delete time: 46ms
total: 572ms

avlmini with 1000000 nodes:
insert time: 438ms, height=24
search time: 141ms error=0
delete time: 47ms
total: 626ms

std::map with 1000000 nodes:
insert time: 345ms
search time: 360ms error=0
delete time: 62ms
total: 767ms
又测试了一下 vs2017,结论类似:

linux rbtree with 10000000 nodes:
insert time: 4201ms, height=32
search time: 3411ms error=0
delete time: 567ms
total: 8179ms

avlmini with 10000000 nodes:
insert time: 4250ms, height=27
search time: 3233ms error=0
delete time: 658ms
total: 8141ms

std::map with 10000000 nodes:
insert time: 4658ms
search time: 4275ms error=0
delete time: 815ms
total: 9748ms

linux rbtree with 1000000 nodes:
insert time: 330ms, height=26
search time: 316ms error=0
delete time: 62ms
total: 708ms

avlmini with 1000000 nodes:
insert time: 409ms, height=24
search time: 266ms error=0
delete time: 53ms
total: 728ms

std::map with 1000000 nodes:
insert time: 426ms
search time: 375ms error=0
delete time: 78ms
total: 879ms

注意,avlmini 和 linux rbtree 都是使用结构体内嵌的形式,这样和 std::map 这种需要 overhead 的容器比较起来 std::map 太吃亏了,所以我测试时,每次插入 avlmini 和 linux rbtree 之前都会模拟 std::map 为每对新的 (key, value) 分配一个结构体(包含node信息和 key, value),再插入,这样加入了内存分配的开销,才和 std::map 进行比较。

参考:别人做的更多树和哈希表的评测rcarbone/kudb

]]>
网上对 AVL被批的很惨,认为性能不如 rbtree,这里给 AVL 树平反昭雪。最近优化了一下我之前的 AVL 树,总体跑的和 linux 的 rbtree 一样快了:

他们都比 std::map 快很多(即便使用动态内存分配,为每个新插入节点临时分配个新内存)。

项目代码在:skywind3000/avlmini
其他 AVL/RBTREE 评测也有类似的结论,见:STL AVL Map

谣言1:RBTREE的平均统计性能比 AVL 好

统计下来一千万个节点插入 AVL 共旋转 7053316 次(先左后右算两次),RBTREE共旋转 5887217 次,RBTREE看起来少是吧?应该很快?但是别忘了 RBTREE 再平衡的操作除了旋转外还有再着色,每次再平衡噼里啪啦的改一片颜色,父亲节点,叔叔,祖父,兄弟节点都要访问一圈,这些都是代价,再者平均树高比 AVL 高也成为各项操作的成本。

谣言2:RBTREE 一般情况只比 AVL 高一两层,这个代价忽略不计

纯粹谣言,随便随机一下,一百万个节点的 RBTREE 树高27,和一千万个节点的 AVL树相同,而一千万个节点的 RBTREE 树高 33,比 AVL 多了 6 层,这还不是最坏情况,最坏情况 AVL 只有 1.440 * log(n + 2) – 0.328, 而 RBTREE 是 2 * log(n + 1),也就是说同样100万个节点,AVL最坏情况是 28 层,rbtree 最坏可以到 39 层。

谣言3:AVL树删除节点是需要回溯到根节点

我以前也是这么写 AVL 树的,后来发现根据 AVL 的定义,可以做出两个推论,再平衡向上回溯时:

插入更新时:如当前节点的高度没有改变,则上面所有父节点的高度和平衡也不会改变。
删除更新时:如当前节点的高度没有改变且平衡值在 [-1, 1] 区间,则所有父节点的高度和平衡都不会改变。

根据这两个推论,AVL的插入和删除大部分时候只需要向上回溯一两个节点即可,范围十分紧凑。

谣言4:虽然二者插入一万个节点总时间类似,但是rbtree树更平均,avl有时很快,有时慢很多,rbtree 只需要旋转两次重新染色就行了,比 avl 平均

完全说反了,avl是公认的比rbtree平均的数据结构,插入时间更为平均,rbtree才是不均衡,有时候直接插入就返回了(上面是黑色节点),有时候插入要染色几个节点但不旋转,有时候还要两次旋转再染色然后递归到父节点。该说法最大的问题是以为 rbtree 插入节点最坏情况是两次旋转加染色,可是忘记了一条,需要向父节点递归,比如:当前节点需要旋转两次重染色,然后递归到父节点再旋转两次重染色,再递归到父节点的父节点,直到满足 rbtree 的5个条件。这种说法直接把递归给搞忘记了,翻翻看 linux 的 rbtree 代码看看,再平衡时那一堆的 while 循环是在干嘛?不就是向父节点递归么?avl和rbtree 插入和删除的最坏情况都需要递归到根节点,都可能需要一路旋转上去,否则你设想下,假设你一直再树的最左边插入1000个新节点,每次都想再局部转两次染染色,而不去调整整棵树,不动根节点,可能么?只是说整个过程avl更加平均而已。

比较结论

AVL / RBTREE 真的差不多,AVL被早期各种乱七八糟的实现和数学上的“统计”给害了,别盯着 linux 用了 rbtree 就觉得 rbtree 一定好过 avl了,solaris 里面大范围的使用 avltree ,完全没有 rbtree 的痕迹那。

补充测试

更新:有人比较疑惑 std::map 那么容易被超越么?无图无真相,给一下我测试的编译器和标准库版本吧,否则疑惑我在和 vc6 的 STL 做比较呢。

主要开发环境:mingw gcc 5.2.0

linux rbtree with 10000000 nodes:
insert time: 4451ms, height=32
search time: 2037ms error=0
delete time: 548ms
total: 7036ms

avlmini with 10000000 nodes:
insert time: 4563ms, height=27
search time: 2018ms error=0
delete time: 598ms
total: 7179ms

std::map with 10000000 nodes:
insert time: 4281ms
search time: 4124ms error=0
delete time: 767ms
total: 9172ms

linux rbtree with 1000000 nodes:
insert time: 355ms, height=26
search time: 171ms error=0
delete time: 46ms
total: 572ms

avlmini with 1000000 nodes:
insert time: 438ms, height=24
search time: 141ms error=0
delete time: 47ms
total: 626ms

std::map with 1000000 nodes:
insert time: 345ms
search time: 360ms error=0
delete time: 62ms
total: 767ms
又测试了一下 vs2017,结论类似:

linux rbtree with 10000000 nodes:
insert time: 4201ms, height=32
search time: 3411ms error=0
delete time: 567ms
total: 8179ms

avlmini with 10000000 nodes:
insert time: 4250ms, height=27
search time: 3233ms error=0
delete time: 658ms
total: 8141ms

std::map with 10000000 nodes:
insert time: 4658ms
search time: 4275ms error=0
delete time: 815ms
total: 9748ms

linux rbtree with 1000000 nodes:
insert time: 330ms, height=26
search time: 316ms error=0
delete time: 62ms
total: 708ms

avlmini with 1000000 nodes:
insert time: 409ms, height=24
search time: 266ms error=0
delete time: 53ms
total: 728ms

std::map with 1000000 nodes:
insert time: 426ms
search time: 375ms error=0
delete time: 78ms
total: 879ms

注意,avlmini 和 linux rbtree 都是使用结构体内嵌的形式,这样和 std::map 这种需要 overhead 的容器比较起来 std::map 太吃亏了,所以我测试时,每次插入 avlmini 和 linux rbtree 之前都会模拟 std::map 为每对新的 (key, value) 分配一个结构体(包含node信息和 key, value),再插入,这样加入了内存分配的开销,才和 std::map 进行比较。

参考:别人做的更多树和哈希表的评测rcarbone/kudb

]]>
0
<![CDATA[使用Go 机器学习库来进行数据分析 3 (平均感知器)]]> http://www.udpwork.com/item/16527.html http://www.udpwork.com/item/16527.html#reviews Thu, 07 Dec 2017 19:36:31 +0800 鸟窝 http://www.udpwork.com/item/16527.html 这一次,我们使用平均感知器(Average Perceptron)算法来预测美国国会的投票。

1984美国国会投票记录数据集

这一次,我们使用1984美国国会的投票记录来预测一下投票结果。

数据集针对不同的投票议题分为了16类, 记录了民主党和共和党议员们得投票结果。

格式如下

123456789101112131415
v16,v1,v2,v3,v4,v5,v6,v7,v8,v9,v10,v11,v12,v13,v14,v15,party1,-1,1,-1,1,1,1,-1,-1,-1,1,-1,1,1,1,-1,republican-1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,1,1,1,-1,republican-1,-1,1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,democrat1,-1,1,1,-1,-1,1,-1,-1,-1,-1,1,-1,1,-1,-1,democrat1,1,1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,1,democrat1,-1,1,1,-1,1,1,-1,-1,-1,-1,-1,-1,1,1,1,democrat1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,-1,-1,1,1,democrat1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,-1,1,1,-1,republican1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,1,1,1,-1,republican-1,1,1,1,-1,-1,-1,1,1,1,-1,-1,-1,-1,-1,-1,democrat-1,-1,1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,1,1,-1,republican-1,-1,1,-1,1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,republican......

这一次,我们还是将数据集分为训练数据和测试数据,以评估算法的预测结果的准确性。

平均感知器

感知器算法一个监督学习的二分分类器, 是线性分类器的一种。

感知机算法是非常好的二分类算法,该算法求取一个分离超平面,超平面由w参数化并用来预测,对于一个样本x,感知机算法通过计算y = [w,x]预测样本的标签,最终的预测标签通过计算sign(y)来实现。算法仅在预测错误时修正权值w。
平均感知机和感知机算法的训练方法一样,不同的是每次训练样本xi后,保留先前训练的权值,训练结束后平均所有权值。最终用平均权值作为最终判别准则的权值。参数平均化可以克服由于学习速率过大所引起的训练过程中出现的震荡现象。

代码

12345678910111213141516171819202122232425262728293031323334
package mainimport (	"fmt"	base "github.com/sjwhitworth/golearn/base"	evaluation "github.com/sjwhitworth/golearn/evaluation"	perceptron "github.com/sjwhitworth/golearn/perceptron"	"math/rand")func main() {	rand.Seed(4402201)	rawData, err := base.ParseCSVToInstances("../datasets/house-votes-84.csv", true)	if err != nil {		panic(err)	}	//Initialises a new AveragePerceptron classifier	cls := perceptron.NewAveragePerceptron(10, 1.2, 0.5, 0.3)	//Do a training-test split	trainData, testData := base.InstancesTrainTestSplit(rawData, 0.50)	fmt.Println(trainData)	fmt.Println(testData)	cls.Fit(trainData)	predictions := cls.Predict(testData)	// Prints precision/recall metrics	confusionMat, _ := evaluation.GetConfusionMatrix(testData, predictions)	fmt.Println(evaluation.GetSummary(confusionMat))}

首先读入国会投票数据集。

然后创建平均感知器算法实例。

之后将数据集分为两份,一份训练数据,一份用来预测和评估。

最后将评估结果打印。

评估结果

12345
Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------democrat	98		24		70		0.8033		0.6806	0.7368republican	70		46		98		0.6034		0.7447	0.6667Overall accuracy: 0.7059
]]>
这一次,我们使用平均感知器(Average Perceptron)算法来预测美国国会的投票。

1984美国国会投票记录数据集

这一次,我们使用1984美国国会的投票记录来预测一下投票结果。

数据集针对不同的投票议题分为了16类, 记录了民主党和共和党议员们得投票结果。

格式如下

123456789101112131415
v16,v1,v2,v3,v4,v5,v6,v7,v8,v9,v10,v11,v12,v13,v14,v15,party1,-1,1,-1,1,1,1,-1,-1,-1,1,-1,1,1,1,-1,republican-1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,1,1,1,-1,republican-1,-1,1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,democrat1,-1,1,1,-1,-1,1,-1,-1,-1,-1,1,-1,1,-1,-1,democrat1,1,1,1,-1,1,1,-1,-1,-1,-1,1,-1,1,1,1,democrat1,-1,1,1,-1,1,1,-1,-1,-1,-1,-1,-1,1,1,1,democrat1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,-1,-1,1,1,democrat1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,-1,1,1,-1,republican1,-1,1,-1,1,1,1,-1,-1,-1,-1,-1,1,1,1,-1,republican-1,1,1,1,-1,-1,-1,1,1,1,-1,-1,-1,-1,-1,-1,democrat-1,-1,1,-1,1,1,-1,-1,-1,-1,-1,-1,-1,1,1,-1,republican-1,-1,1,-1,1,1,1,-1,-1,-1,-1,1,-1,1,1,-1,republican......

这一次,我们还是将数据集分为训练数据和测试数据,以评估算法的预测结果的准确性。

平均感知器

感知器算法一个监督学习的二分分类器, 是线性分类器的一种。

感知机算法是非常好的二分类算法,该算法求取一个分离超平面,超平面由w参数化并用来预测,对于一个样本x,感知机算法通过计算y = [w,x]预测样本的标签,最终的预测标签通过计算sign(y)来实现。算法仅在预测错误时修正权值w。
平均感知机和感知机算法的训练方法一样,不同的是每次训练样本xi后,保留先前训练的权值,训练结束后平均所有权值。最终用平均权值作为最终判别准则的权值。参数平均化可以克服由于学习速率过大所引起的训练过程中出现的震荡现象。

代码

12345678910111213141516171819202122232425262728293031323334
package mainimport (	"fmt"	base "github.com/sjwhitworth/golearn/base"	evaluation "github.com/sjwhitworth/golearn/evaluation"	perceptron "github.com/sjwhitworth/golearn/perceptron"	"math/rand")func main() {	rand.Seed(4402201)	rawData, err := base.ParseCSVToInstances("../datasets/house-votes-84.csv", true)	if err != nil {		panic(err)	}	//Initialises a new AveragePerceptron classifier	cls := perceptron.NewAveragePerceptron(10, 1.2, 0.5, 0.3)	//Do a training-test split	trainData, testData := base.InstancesTrainTestSplit(rawData, 0.50)	fmt.Println(trainData)	fmt.Println(testData)	cls.Fit(trainData)	predictions := cls.Predict(testData)	// Prints precision/recall metrics	confusionMat, _ := evaluation.GetConfusionMatrix(testData, predictions)	fmt.Println(evaluation.GetSummary(confusionMat))}

首先读入国会投票数据集。

然后创建平均感知器算法实例。

之后将数据集分为两份,一份训练数据,一份用来预测和评估。

最后将评估结果打印。

评估结果

12345
Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------democrat	98		24		70		0.8033		0.6806	0.7368republican	70		46		98		0.6034		0.7447	0.6667Overall accuracy: 0.7059
]]>
0
<![CDATA[使用Go 机器学习库来进行数据分析 2 (决策树)]]> http://www.udpwork.com/item/16528.html http://www.udpwork.com/item/16528.html#reviews Thu, 07 Dec 2017 19:07:30 +0800 鸟窝 http://www.udpwork.com/item/16528.html 这篇文章, 继续使用golearn库分析鸢尾花的数据集。 这一次,我们会使用决策树和随机森林来分析。

决策树和随机森林

决策树是机器学习中最接近人类思考问题的过程的一种算法,通过若干个节点,对特征进行提问并分类(可以是二分类也可以使多分类),直至最后生成叶节点(也就是只剩下一种属性)。

每个决策树都表述了一种树型结构,它由它的分支来对该类型的对象依靠属性进行分类。每个决策树可以依靠对源数据库的分割进行数据测试。这个过程可以递归式的对树进行修剪。 当不能再进行分割或一个单独的类可以被应用于某一分支时,递归过程就完成了。另外,随机森林分类器将许多决策树结合起来以提升分类的正确率。

golearn支持两种决策树算法。ID3和RandomTree。

  • ID3 : 以信息增益为准则选择信息增益最大的属性。

    ID3 is a decision tree induction algorithm which splits on the Attribute which gives the greatest Information Gain (entropy gradient). It performs well on categorical data. Numeric datasets will need to be discretised before using ID3

  • RandomTree : 与ID3类似,但是选择的属性的时候随机选择。

    Random Trees are structurally identical to those generated by ID3, but the split Attribute is chosen randomly. Golearn's implementation allows you to choose up to k nodes for consideration at each split.

可以参考 ChongmingLiu的介绍:决策树(ID3 & C4.5 & CART)

维基百科中对随机森林的介绍:

在机器学习中,随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。 Leo Breiman和Adele Cutler发展出推论出随机森林的算法。而"Random Forests"是他们的商标。这个术语是1995年由贝尔实验室的Tin Kam Ho所提出的随机决策森林(random decision forests)而来的。这个方法则是结合Breimans的"Bootstrap aggregating"想法和Ho的"random subspace method" 以建造决策树的集合。

在机器学习中,随机森林由许多的决策树组成,因为这些决策树的形成采用了随机的方法,因此也叫做随机决策树。随机森林中的树之间是没有关联的。当测试数据进入随机森林时,其实就是让每一颗决策树进行分类,最后取所有决策树中分类结果最多的那类为最终的结果。因此随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。

代码

下面是使用决策树和随机森林预测鸢尾花分类的代码,来自golearn:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
// Demonstrates decision tree classificationpackage mainimport (	"fmt"	"github.com/sjwhitworth/golearn/base"	"github.com/sjwhitworth/golearn/ensemble"	"github.com/sjwhitworth/golearn/evaluation"	"github.com/sjwhitworth/golearn/filters"	"github.com/sjwhitworth/golearn/trees"	"math/rand")func main() {	var tree base.Classifier	rand.Seed(44111342)	// Load in the iris dataset	iris, err := base.ParseCSVToInstances("../datasets/iris_headers.csv", true)	if err != nil {		panic(err)	}	// Discretise the iris dataset with Chi-Merge	filt := filters.NewChiMergeFilter(iris, 0.999)	for _, a := range base.NonClassFloatAttributes(iris) {		filt.AddAttribute(a)	}	filt.Train()	irisf := base.NewLazilyFilteredInstances(iris, filt)	// Create a 60-40 training-test split	trainData, testData := base.InstancesTrainTestSplit(irisf, 0.60)	//	// First up, use ID3	//	tree = trees.NewID3DecisionTree(0.6)	// (Parameter controls train-prune split.)	// Train the ID3 tree	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	// Generate predictions	predictions, err := tree.Predict(testData)	if err != nil {		panic(err)	}	// Evaluate	fmt.Println("ID3 Performance (information gain)")	cf, err := evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	tree = trees.NewID3DecisionTreeFromRule(0.6, new(trees.InformationGainRatioRuleGenerator))	// (Parameter controls train-prune split.)	// Train the ID3 tree	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	// Generate predictions	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	// Evaluate	fmt.Println("ID3 Performance (information gain ratio)")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	tree = trees.NewID3DecisionTreeFromRule(0.6, new(trees.GiniCoefficientRuleGenerator))	// (Parameter controls train-prune split.)	// Train the ID3 tree	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	// Generate predictions	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	// Evaluate	fmt.Println("ID3 Performance (gini index generator)")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	//	// Next up, Random Trees	//	// Consider two randomly-chosen attributes	tree = trees.NewRandomTree(2)	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	fmt.Println("RandomTree Performance")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	//	// Finally, Random Forests	//	tree = ensemble.NewRandomForest(70, 3)	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	fmt.Println("RandomForest Performance")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))}

首选使用ChiMerge方法进行数据离散,ChiMerge 是监督的、自底向上的(即基于合并的)数据离散化方法。它依赖于卡方分析:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。
基本思想:对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。可以参考"Principles of Data Mining"第二版中的第105页--第115页。

接着调用base.NewLazilyFilteredInstances应用filter得到FixedDataGrid。

之后将数据集分成训练数据和测试数据两部分。

接下来就是训练数据、预测与评估了。

分别使用ID3、ID3 with InformationGainRatioRuleGenerator、ID3 with GiniCoefficientRuleGenerator、RandomTree、RandomForest算法进行处理。

评估结果

以下是各种算法的评估结果,可以和 kNN进行比较,看起来比不过kNN的预测。

123456789101112131415161718192021222324252627282930313233343536373839
ID3 Performance (information gain)Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	32		5		46		0.8649		0.9697	0.9143Iris-versicolor	4		1		61		0.8000		0.1818	0.2963Iris-setosa	29		13		42		0.6905		1.0000	0.8169Overall accuracy: 0.7738ID3 Performance (information gain ratio)Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	29		3		48		0.9062		0.8788	0.8923Iris-versicolor	5		3		59		0.6250		0.2273	0.3333Iris-setosa	29		15		40		0.6591		1.0000	0.7945Overall accuracy: 0.7500ID3 Performance (gini index generator)Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	26		5		46		0.8387		0.7879	0.8125Iris-versicolor	17		36		26		0.3208		0.7727	0.4533Iris-setosa	0		0		55		NaN		0.0000	NaNOverall accuracy: 0.5119RandomTree PerformanceReference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	30		3		48		0.9091		0.9091	0.9091Iris-versicolor	9		3		59		0.7500		0.4091	0.5294Iris-setosa	29		10		45		0.7436		1.0000	0.8529Overall accuracy: 0.8095RandomForest PerformanceReference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	31		8		43		0.7949		0.9394	0.8611Iris-versicolor	0		0		62		NaN		0.0000	NaNIris-setosa	29		16		39		0.6444		1.0000	0.7838Overall accuracy: 0.7143
]]>
这篇文章, 继续使用golearn库分析鸢尾花的数据集。 这一次,我们会使用决策树和随机森林来分析。

决策树和随机森林

决策树是机器学习中最接近人类思考问题的过程的一种算法,通过若干个节点,对特征进行提问并分类(可以是二分类也可以使多分类),直至最后生成叶节点(也就是只剩下一种属性)。

每个决策树都表述了一种树型结构,它由它的分支来对该类型的对象依靠属性进行分类。每个决策树可以依靠对源数据库的分割进行数据测试。这个过程可以递归式的对树进行修剪。 当不能再进行分割或一个单独的类可以被应用于某一分支时,递归过程就完成了。另外,随机森林分类器将许多决策树结合起来以提升分类的正确率。

golearn支持两种决策树算法。ID3和RandomTree。

  • ID3 : 以信息增益为准则选择信息增益最大的属性。

    ID3 is a decision tree induction algorithm which splits on the Attribute which gives the greatest Information Gain (entropy gradient). It performs well on categorical data. Numeric datasets will need to be discretised before using ID3

  • RandomTree : 与ID3类似,但是选择的属性的时候随机选择。

    Random Trees are structurally identical to those generated by ID3, but the split Attribute is chosen randomly. Golearn's implementation allows you to choose up to k nodes for consideration at each split.

可以参考 ChongmingLiu的介绍:决策树(ID3 & C4.5 & CART)

维基百科中对随机森林的介绍:

在机器学习中,随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。 Leo Breiman和Adele Cutler发展出推论出随机森林的算法。而"Random Forests"是他们的商标。这个术语是1995年由贝尔实验室的Tin Kam Ho所提出的随机决策森林(random decision forests)而来的。这个方法则是结合Breimans的"Bootstrap aggregating"想法和Ho的"random subspace method" 以建造决策树的集合。

在机器学习中,随机森林由许多的决策树组成,因为这些决策树的形成采用了随机的方法,因此也叫做随机决策树。随机森林中的树之间是没有关联的。当测试数据进入随机森林时,其实就是让每一颗决策树进行分类,最后取所有决策树中分类结果最多的那类为最终的结果。因此随机森林是一个包含多个决策树的分类器,并且其输出的类别是由个别树输出的类别的众数而定。

代码

下面是使用决策树和随机森林预测鸢尾花分类的代码,来自golearn:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
// Demonstrates decision tree classificationpackage mainimport (	"fmt"	"github.com/sjwhitworth/golearn/base"	"github.com/sjwhitworth/golearn/ensemble"	"github.com/sjwhitworth/golearn/evaluation"	"github.com/sjwhitworth/golearn/filters"	"github.com/sjwhitworth/golearn/trees"	"math/rand")func main() {	var tree base.Classifier	rand.Seed(44111342)	// Load in the iris dataset	iris, err := base.ParseCSVToInstances("../datasets/iris_headers.csv", true)	if err != nil {		panic(err)	}	// Discretise the iris dataset with Chi-Merge	filt := filters.NewChiMergeFilter(iris, 0.999)	for _, a := range base.NonClassFloatAttributes(iris) {		filt.AddAttribute(a)	}	filt.Train()	irisf := base.NewLazilyFilteredInstances(iris, filt)	// Create a 60-40 training-test split	trainData, testData := base.InstancesTrainTestSplit(irisf, 0.60)	//	// First up, use ID3	//	tree = trees.NewID3DecisionTree(0.6)	// (Parameter controls train-prune split.)	// Train the ID3 tree	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	// Generate predictions	predictions, err := tree.Predict(testData)	if err != nil {		panic(err)	}	// Evaluate	fmt.Println("ID3 Performance (information gain)")	cf, err := evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	tree = trees.NewID3DecisionTreeFromRule(0.6, new(trees.InformationGainRatioRuleGenerator))	// (Parameter controls train-prune split.)	// Train the ID3 tree	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	// Generate predictions	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	// Evaluate	fmt.Println("ID3 Performance (information gain ratio)")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	tree = trees.NewID3DecisionTreeFromRule(0.6, new(trees.GiniCoefficientRuleGenerator))	// (Parameter controls train-prune split.)	// Train the ID3 tree	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	// Generate predictions	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	// Evaluate	fmt.Println("ID3 Performance (gini index generator)")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	//	// Next up, Random Trees	//	// Consider two randomly-chosen attributes	tree = trees.NewRandomTree(2)	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	fmt.Println("RandomTree Performance")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))	//	// Finally, Random Forests	//	tree = ensemble.NewRandomForest(70, 3)	err = tree.Fit(trainData)	if err != nil {		panic(err)	}	predictions, err = tree.Predict(testData)	if err != nil {		panic(err)	}	fmt.Println("RandomForest Performance")	cf, err = evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(cf))}

首选使用ChiMerge方法进行数据离散,ChiMerge 是监督的、自底向上的(即基于合并的)数据离散化方法。它依赖于卡方分析:具有最小卡方值的相邻区间合并在一起,直到满足确定的停止准则。
基本思想:对于精确的离散化,相对类频率在一个区间内应当完全一致。因此,如果两个相邻的区间具有非常类似的类分布,则这两个区间可以合并;否则,它们应当保持分开。而低卡方值表明它们具有相似的类分布。可以参考"Principles of Data Mining"第二版中的第105页--第115页。

接着调用base.NewLazilyFilteredInstances应用filter得到FixedDataGrid。

之后将数据集分成训练数据和测试数据两部分。

接下来就是训练数据、预测与评估了。

分别使用ID3、ID3 with InformationGainRatioRuleGenerator、ID3 with GiniCoefficientRuleGenerator、RandomTree、RandomForest算法进行处理。

评估结果

以下是各种算法的评估结果,可以和 kNN进行比较,看起来比不过kNN的预测。

123456789101112131415161718192021222324252627282930313233343536373839
ID3 Performance (information gain)Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	32		5		46		0.8649		0.9697	0.9143Iris-versicolor	4		1		61		0.8000		0.1818	0.2963Iris-setosa	29		13		42		0.6905		1.0000	0.8169Overall accuracy: 0.7738ID3 Performance (information gain ratio)Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	29		3		48		0.9062		0.8788	0.8923Iris-versicolor	5		3		59		0.6250		0.2273	0.3333Iris-setosa	29		15		40		0.6591		1.0000	0.7945Overall accuracy: 0.7500ID3 Performance (gini index generator)Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	26		5		46		0.8387		0.7879	0.8125Iris-versicolor	17		36		26		0.3208		0.7727	0.4533Iris-setosa	0		0		55		NaN		0.0000	NaNOverall accuracy: 0.5119RandomTree PerformanceReference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	30		3		48		0.9091		0.9091	0.9091Iris-versicolor	9		3		59		0.7500		0.4091	0.5294Iris-setosa	29		10		45		0.7436		1.0000	0.8529Overall accuracy: 0.8095RandomForest PerformanceReference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-virginica	31		8		43		0.7949		0.9394	0.8611Iris-versicolor	0		0		62		NaN		0.0000	NaNIris-setosa	29		16		39		0.6444		1.0000	0.7838Overall accuracy: 0.7143
]]>
0
<![CDATA[使用Go 机器学习库来进行数据分析 1 (kNN)]]> http://www.udpwork.com/item/16526.html http://www.udpwork.com/item/16526.html#reviews Thu, 07 Dec 2017 18:25:24 +0800 鸟窝 http://www.udpwork.com/item/16526.html 这个系列的文章是介绍如何使用Go语言来进行数据分析和机器学习。

Go机器学习的库目前还不是很多,功能海没有Python的丰富,希望在未来的几年里能有更多的功能丰富库面试。

这篇文章利用golearn库, 使用kNN方法来对Iris数据集进行分析。

Iris数据集

Iris数据集也称为鸢尾花数据集,或者叫做费雪鸢尾花卉数据集或者安德森鸢尾花卉数据集。是一类多重变量分析的数据集。它最初是埃德加·安德森从加拿大加斯帕半岛上的鸢尾属花朵中提取的地理变异数据,后由罗纳德·费雪作为判别分析的一个例子,运用到统计学中。

其它比较流行的数据集还有Adult,Wine,Car Evaluation等(1)。

Iris数据集包含了150个样本,都属于鸢尾属下的三个亚属,分别是山鸢尾(setosa)、变色鸢尾(versicolor)和维吉尼亚鸢尾(virginica)。四个特征被用作样本的定量分析,它们分别是花萼花瓣长度宽度 。基于这四个特征的集合,费雪发展了一个线性判别分析以确定其属种。

下面是这三种鸢尾的花,非常的漂亮:







下图是鸢尾花数据集的散布图, 第一个种类与另外两个种类是线性可分离的,后两个种类是非线性可分离的:

以上内容主要参考维基百科和百度百科关于Iris数据集的介绍。

这个数据集在网上很容易搜到,也可以在 golearn 项目中下载。

kNN K近邻算法

分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。

kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 kNN方法在类别决策时,只与极少量的相邻样本有关。由于kNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,kNN方法较其他方法更为适合。

简单说, 如果你住在高档小区,周围都是"高端人口", 那么可以判定你就是"高端人口", 然后就不会被......

k代表最近的k个邻居。

算法更详细的介绍可以参考:百度百科维基百科

训练数据和预测

下面就让我们看看 golearn使用kNN算法分析鸢尾花数据集的例子。

12345678910111213141516171819202122232425262728293031323334353637
package mainimport (	"fmt"	"github.com/sjwhitworth/golearn/base"	"github.com/sjwhitworth/golearn/evaluation"	"github.com/sjwhitworth/golearn/knn")func main() {	rawData, err := base.ParseCSVToInstances("../datasets/iris_headers.csv", true)	if err != nil {		panic(err)	}	//Initialises a new KNN classifier	cls := knn.NewKnnClassifier("euclidean", "linear", 2)	//Do a training-test split	trainData, testData := base.InstancesTrainTestSplit(rawData, 0.50)	cls.Fit(trainData)	//Calculates the Euclidean distance and returns the most popular label	predictions, err := cls.Predict(testData)	if err != nil {		panic(err)	}	fmt.Println(predictions)	// Prints precision/recall metrics	confusionMat, err := evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(confusionMat))}

#12 行加载鸢尾花数据集, base提供了读取CSV文本文件的方法。

#18 行创建一个kNN分类器, 距离的计算使用欧几里德方法,此外还支持manhattan、cosine距离计算方法。第二个参数支持linear和kdtree。

#18 还指定了K为2。

#21 将鸢尾花数据集按照参数分成两份,它使用随机数和这个参数比较,所以分成的数据的结果大致是这个比例。一部分用于训练数据,一部分用于测试。接着#22 开始训练数据。

#25 使用测试预测数据,并将预测结果打印出来。

#32 ~ #36 是评估预测模型,并将评估结果输出。

评估

首先看一下评估结果

12345
Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-setosa	30		0		58		1.0000		1.0000	1.0000Iris-virginica	27		1		58		0.9643		0.9310	0.9474Iris-versicolor	28		2		57		0.9333		0.9655	0.9492

这里有几个概念需要说明一下。

  • ConfusionMatrix: 混淆矩阵,它描绘样本数据的真实属性与识别结果类型之间的关系,是评价分类器性能的一种常用方法,用于有监督学习。
  • True Positives: 真正,TP, 被模型预测为正的正样本;可以称作判断为真的正确率
  • False Positives: 假正,FP,误报, 被模型预测为正的负样本;可以称作误报率
  • True Negatives: 真负,TN, 被模型预测为负的负样本 ;可以称作判断为假的正确率
  • False Negatives: 假负,FN,漏报, 被模型预测为负的正样本;可以称作漏报率
  • Precision: 精确率, 正确预测正负的个数/总个数,它表示的是预测为正的样本中有多少是真正的正样本。 $$P = \frac{TP}{TP+FP}$$,
  • Recall: 召回率,它表示的是样本中的正例有多少被预测正确了, $$R = TPR = \frac{TP}{TP+FN}$$
  • F1 Score: 为了能够评价不同算法的优劣,在Precision和Recall的基础上提出了F1值的概念,来对Precision和Recall进行整体评价。F1的定义如下:F1值 = 正确率 * 召回率 * 2 / (正确率 + 召回率)

Python代码实现

使用sklearn很容易实现上面的逻辑。

123456789101112131415161718192021
from sklearn import neighbors, datasets, metricsfrom sklearn.model_selection import train_test_split # import some data to play withIRIS = datasets.load_iris() # prepare dataX_train, X_test, Y_train, Y_test = train_test_split(IRIS.data, IRIS.target, test_size=0.5, random_state=0)# we create an instance of Neighbours Classifier and fit the data.knn = neighbors.KNeighborsClassifier(n_neighbors=2, weights='distance')knn.fit(X_train, Y_train)# make predictionpredicted = knn.predict(X_test) # evaluateprint(metrics.classification_report(Y_test, predicted))print(metrics.confusion_matrix(Y_test, predicted))
]]>
这个系列的文章是介绍如何使用Go语言来进行数据分析和机器学习。

Go机器学习的库目前还不是很多,功能海没有Python的丰富,希望在未来的几年里能有更多的功能丰富库面试。

这篇文章利用golearn库, 使用kNN方法来对Iris数据集进行分析。

Iris数据集

Iris数据集也称为鸢尾花数据集,或者叫做费雪鸢尾花卉数据集或者安德森鸢尾花卉数据集。是一类多重变量分析的数据集。它最初是埃德加·安德森从加拿大加斯帕半岛上的鸢尾属花朵中提取的地理变异数据,后由罗纳德·费雪作为判别分析的一个例子,运用到统计学中。

其它比较流行的数据集还有Adult,Wine,Car Evaluation等(1)。

Iris数据集包含了150个样本,都属于鸢尾属下的三个亚属,分别是山鸢尾(setosa)、变色鸢尾(versicolor)和维吉尼亚鸢尾(virginica)。四个特征被用作样本的定量分析,它们分别是花萼花瓣长度宽度 。基于这四个特征的集合,费雪发展了一个线性判别分析以确定其属种。

下面是这三种鸢尾的花,非常的漂亮:







下图是鸢尾花数据集的散布图, 第一个种类与另外两个种类是线性可分离的,后两个种类是非线性可分离的:

以上内容主要参考维基百科和百度百科关于Iris数据集的介绍。

这个数据集在网上很容易搜到,也可以在 golearn 项目中下载。

kNN K近邻算法

分类算法是数据挖掘分类技术中最简单的方法之一。所谓K最近邻,就是k个最近的邻居的意思,说的是每个样本都可以用它最接近的k个邻居来代表。

kNN算法的核心思想是如果一个样本在特征空间中的k个最相邻的样本中的大多数属于某一个类别,则该样本也属于这个类别,并具有这个类别上样本的特性。该方法在确定分类决策上只依据最邻近的一个或者几个样本的类别来决定待分样本所属的类别。 kNN方法在类别决策时,只与极少量的相邻样本有关。由于kNN方法主要靠周围有限的邻近的样本,而不是靠判别类域的方法来确定所属类别的,因此对于类域的交叉或重叠较多的待分样本集来说,kNN方法较其他方法更为适合。

简单说, 如果你住在高档小区,周围都是"高端人口", 那么可以判定你就是"高端人口", 然后就不会被......

k代表最近的k个邻居。

算法更详细的介绍可以参考:百度百科维基百科

训练数据和预测

下面就让我们看看 golearn使用kNN算法分析鸢尾花数据集的例子。

12345678910111213141516171819202122232425262728293031323334353637
package mainimport (	"fmt"	"github.com/sjwhitworth/golearn/base"	"github.com/sjwhitworth/golearn/evaluation"	"github.com/sjwhitworth/golearn/knn")func main() {	rawData, err := base.ParseCSVToInstances("../datasets/iris_headers.csv", true)	if err != nil {		panic(err)	}	//Initialises a new KNN classifier	cls := knn.NewKnnClassifier("euclidean", "linear", 2)	//Do a training-test split	trainData, testData := base.InstancesTrainTestSplit(rawData, 0.50)	cls.Fit(trainData)	//Calculates the Euclidean distance and returns the most popular label	predictions, err := cls.Predict(testData)	if err != nil {		panic(err)	}	fmt.Println(predictions)	// Prints precision/recall metrics	confusionMat, err := evaluation.GetConfusionMatrix(testData, predictions)	if err != nil {		panic(fmt.Sprintf("Unable to get confusion matrix: %s", err.Error()))	}	fmt.Println(evaluation.GetSummary(confusionMat))}

#12 行加载鸢尾花数据集, base提供了读取CSV文本文件的方法。

#18 行创建一个kNN分类器, 距离的计算使用欧几里德方法,此外还支持manhattan、cosine距离计算方法。第二个参数支持linear和kdtree。

#18 还指定了K为2。

#21 将鸢尾花数据集按照参数分成两份,它使用随机数和这个参数比较,所以分成的数据的结果大致是这个比例。一部分用于训练数据,一部分用于测试。接着#22 开始训练数据。

#25 使用测试预测数据,并将预测结果打印出来。

#32 ~ #36 是评估预测模型,并将评估结果输出。

评估

首先看一下评估结果

12345
Reference Class	True Positives	False Positives	True Negatives	Precision	Recall	F1 Score---------------	--------------	---------------	--------------	---------	------	--------Iris-setosa	30		0		58		1.0000		1.0000	1.0000Iris-virginica	27		1		58		0.9643		0.9310	0.9474Iris-versicolor	28		2		57		0.9333		0.9655	0.9492

这里有几个概念需要说明一下。

  • ConfusionMatrix: 混淆矩阵,它描绘样本数据的真实属性与识别结果类型之间的关系,是评价分类器性能的一种常用方法,用于有监督学习。
  • True Positives: 真正,TP, 被模型预测为正的正样本;可以称作判断为真的正确率
  • False Positives: 假正,FP,误报, 被模型预测为正的负样本;可以称作误报率
  • True Negatives: 真负,TN, 被模型预测为负的负样本 ;可以称作判断为假的正确率
  • False Negatives: 假负,FN,漏报, 被模型预测为负的正样本;可以称作漏报率
  • Precision: 精确率, 正确预测正负的个数/总个数,它表示的是预测为正的样本中有多少是真正的正样本。 $$P = \frac{TP}{TP+FP}$$,
  • Recall: 召回率,它表示的是样本中的正例有多少被预测正确了, $$R = TPR = \frac{TP}{TP+FN}$$
  • F1 Score: 为了能够评价不同算法的优劣,在Precision和Recall的基础上提出了F1值的概念,来对Precision和Recall进行整体评价。F1的定义如下:F1值 = 正确率 * 召回率 * 2 / (正确率 + 召回率)

Python代码实现

使用sklearn很容易实现上面的逻辑。

123456789101112131415161718192021
from sklearn import neighbors, datasets, metricsfrom sklearn.model_selection import train_test_split # import some data to play withIRIS = datasets.load_iris() # prepare dataX_train, X_test, Y_train, Y_test = train_test_split(IRIS.data, IRIS.target, test_size=0.5, random_state=0)# we create an instance of Neighbours Classifier and fit the data.knn = neighbors.KNeighborsClassifier(n_neighbors=2, weights='distance')knn.fit(X_train, Y_train)# make predictionpredicted = knn.predict(X_test) # evaluateprint(metrics.classification_report(Y_test, predicted))print(metrics.confusion_matrix(Y_test, predicted))
]]>
0
<![CDATA[Accessing external USB disk attached to my ASUS RT-AC68U router]]> http://www.udpwork.com/item/16525.html http://www.udpwork.com/item/16525.html#reviews Wed, 06 Dec 2017 11:46:15 +0800 Haidong Ji http://www.udpwork.com/item/16525.html I have my own cloud storage server usingownCloud for many years now, and love it. It’s like DropBox, only better.

However, even with that, it’s still nice to have a shared storage for my home network. So today I bought a Seatate Ultra Slim USB 3 disk from Costco, with 2TB capacity. It is attached to my router, ASUS RT-AC68U. Here are the steps for:

  • Router set up;
  • Mount a drive on Windows;
  • Mount a drive on Linux;

Router:
1. Go to 192.168.1.1 through your browser;
2. USB Application (left side);
3. Media Services and Servers;
4. Network Place (Samba) Share / Cloud Disk;
5. Enable Share. I also enabled Allow guest login. Leave everything and click “Apply”.

Windows:
Map a drive to \\192.168.1.1\Seagate_Backup_Plus_Drive\Seagate

Linux:
sudo apt install cifs-utils
sudo mkdir /media/routerUSB
Edit /etc/fstab, adding this line:
//192.168.1.1/Seagate_Backup_Plus_Drive/Seagate /media/routerUSB cifs guest 0 0
Run sudo mount -a

Enjoy!

]]>
I have my own cloud storage server usingownCloud for many years now, and love it. It’s like DropBox, only better.

However, even with that, it’s still nice to have a shared storage for my home network. So today I bought a Seatate Ultra Slim USB 3 disk from Costco, with 2TB capacity. It is attached to my router, ASUS RT-AC68U. Here are the steps for:

  • Router set up;
  • Mount a drive on Windows;
  • Mount a drive on Linux;

Router:
1. Go to 192.168.1.1 through your browser;
2. USB Application (left side);
3. Media Services and Servers;
4. Network Place (Samba) Share / Cloud Disk;
5. Enable Share. I also enabled Allow guest login. Leave everything and click “Apply”.

Windows:
Map a drive to \\192.168.1.1\Seagate_Backup_Plus_Drive\Seagate

Linux:
sudo apt install cifs-utils
sudo mkdir /media/routerUSB
Edit /etc/fstab, adding this line:
//192.168.1.1/Seagate_Backup_Plus_Drive/Seagate /media/routerUSB cifs guest 0 0
Run sudo mount -a

Enjoy!

]]>
0
<![CDATA[网站上了CDN]]> http://www.udpwork.com/item/16524.html http://www.udpwork.com/item/16524.html#reviews Tue, 05 Dec 2017 21:28:34 +0800 s5s5 http://www.udpwork.com/item/16524.html Dogfooding 我还做的不够好,把网站迁移到腾讯云后,竟然一直不知道可以免费用 10G 每月的 CDN ,昨天才用上,感觉如下

痛点:

  • 真心有点难发现,CVM 控制台竟然一点指引也没有
  • 为了上 CDN ,还要设置 云解析 就是 DNS ,这又是一个控制台
  • 如果网站上了 HTTPS ,还要进 SSL 管理的控制台
  • 这几个关系这么紧密,是不是可以互相打通?

坑点:

  • 网站上 CDN 后,一定要开启 高级过期缓存配置
  • 设置后台管理目录 /wp-admin 不 CACHE
  • 还有什么 .php 什么的也不要 CACHE 了
  • 最终还是只 CACHE 图片、样式、JS这些静态资源了,想全站 CACHE 导致很多功能失效了。
  • 如果不这么设置,WP会更新失败,我找了半天原因,好像是什么 WAF ,反正设置好就OK了

最近还有用Cloudflare,在他这里做网站 DNS 解析,可以直接点一下就自动做 CDN,可惜国内他没有节点。

Cloudflare 基本是以域名为中心,然后通过 TAB 切换几大功能点。而还有以网站为中心的,比如宝塔面板,在网站管理这里,把网站常用功能做了整合,然后在左侧 TAB 进行切换。

宝塔面板感觉比AMH要漂亮易用,AMH 功能多但功能分离的,来回点来点去的,宝塔感觉要超越 AMH 了。

  • 界面上宝塔更漂亮
  • 功能交互上比如网站这一块,宝塔比 AMH 点击路径要短,AMH 虽然可以建快捷方式,但用的人比较少吧,因为进面板操作也不是经常来看,会忘记建的快捷方式。
  • 功能的组织上宝塔要更合理些,比如网站的 SSL 功能,AMH 到处来回要点多少下啊
  • 版本发布宝塔基本一个月一个版本,让用户感觉到他们一直在改进,AMH 主要放在软件商店里了,这个对用户的感知有点弱,用户要感觉到你们的努力啊。
  • 总体功能上感觉 AMH 要多和好,但我有些细节不到位,感觉不好用的点:SSL 功能不方便,AMH 连自动 LES 证书都没有。备份啊等网站常用功能可以和环境功能组合吧。环境功能感觉建好了就没人爱折腾改来改去吧。

大体就这些希望 AMH 早日改进,我可是付费用户啊!

另外:迁移网站后记得用chown -R -v root:mail test6改文件的拥有者啊!

扫码关注米随随

]]>
Dogfooding 我还做的不够好,把网站迁移到腾讯云后,竟然一直不知道可以免费用 10G 每月的 CDN ,昨天才用上,感觉如下

痛点:

  • 真心有点难发现,CVM 控制台竟然一点指引也没有
  • 为了上 CDN ,还要设置 云解析 就是 DNS ,这又是一个控制台
  • 如果网站上了 HTTPS ,还要进 SSL 管理的控制台
  • 这几个关系这么紧密,是不是可以互相打通?

坑点:

  • 网站上 CDN 后,一定要开启 高级过期缓存配置
  • 设置后台管理目录 /wp-admin 不 CACHE
  • 还有什么 .php 什么的也不要 CACHE 了
  • 最终还是只 CACHE 图片、样式、JS这些静态资源了,想全站 CACHE 导致很多功能失效了。
  • 如果不这么设置,WP会更新失败,我找了半天原因,好像是什么 WAF ,反正设置好就OK了

最近还有用Cloudflare,在他这里做网站 DNS 解析,可以直接点一下就自动做 CDN,可惜国内他没有节点。

Cloudflare 基本是以域名为中心,然后通过 TAB 切换几大功能点。而还有以网站为中心的,比如宝塔面板,在网站管理这里,把网站常用功能做了整合,然后在左侧 TAB 进行切换。

宝塔面板感觉比AMH要漂亮易用,AMH 功能多但功能分离的,来回点来点去的,宝塔感觉要超越 AMH 了。

  • 界面上宝塔更漂亮
  • 功能交互上比如网站这一块,宝塔比 AMH 点击路径要短,AMH 虽然可以建快捷方式,但用的人比较少吧,因为进面板操作也不是经常来看,会忘记建的快捷方式。
  • 功能的组织上宝塔要更合理些,比如网站的 SSL 功能,AMH 到处来回要点多少下啊
  • 版本发布宝塔基本一个月一个版本,让用户感觉到他们一直在改进,AMH 主要放在软件商店里了,这个对用户的感知有点弱,用户要感觉到你们的努力啊。
  • 总体功能上感觉 AMH 要多和好,但我有些细节不到位,感觉不好用的点:SSL 功能不方便,AMH 连自动 LES 证书都没有。备份啊等网站常用功能可以和环境功能组合吧。环境功能感觉建好了就没人爱折腾改来改去吧。

大体就这些希望 AMH 早日改进,我可是付费用户啊!

另外:迁移网站后记得用chown -R -v root:mail test6改文件的拥有者啊!

扫码关注米随随

]]>
0
<![CDATA[北方的空地]]> http://www.udpwork.com/item/16523.html http://www.udpwork.com/item/16523.html#reviews Tue, 05 Dec 2017 20:29:12 +0800 阮一峰 http://www.udpwork.com/item/16523.html 最近有一部电影《77天》,讲述一个探险者在荒原独自旅行的故事。

我以为电影是虚构的,没想到改编自真人真事。2010年,一个名叫杨柳松的青年,徒步穿越羌塘,1400公里的无人区,平均海拔5000米,走了77天。

他把这段经历,写成了一本书《北方的空地》

我读了,越读越佩服。这完全不是普通的冒险,跟川藏线的骑行是两个概念。不仅仅需要勇气和意志,还需要广博的知识,以及准确的现场判断,一个决定错误可能就会送命了。我惊叹,杨柳松是什么人啊,怎么知道这么多?!

电影《77天》挺一般的,可以不看。但是,热爱旅行和大自然的朋友都应该读一下这本书。它的雏形是杨柳松发在8624论坛的一个长篇连载,现在还能读到。

一、羌塘

杨柳松穿越的羌塘位于西藏的北部,藏语的意思是"北方的未知之地"。它的北面是昆仑山脉和可可西里山脉,南面是冈底斯山脉和念青唐古拉山脉,属于高山之间的一块高原盆地。

......南北最宽760公里,东西长约1200公里。面积59.70万平方公里,占青藏高原总面积的1/4。 行政上属西藏自治区的那曲与阿里两地区管辖。

羌塘高原日照强烈,天气变化无常,风力强劲,十一级大风是家常便饭。冬季极寒缺水,雨季沼泽遍布,是地球上最大的无人区之一。

(图片说明:杨柳松的实际路线)

没有后援,没有补给,与外界断绝联系,几十天不遇人,一个人徒步横穿羌塘,谈何容易。此前,还没有人完成过。

最大困惑有三个方面。一是食物补给,在如此长的天数里完全靠自给确无先例,我到底能承受怎样的饥饿状态?二是体能,在海拔五千米的恶劣环境中超负重推行能坚持多久?三是心理状态,孤身荒原中,面对周而复始的困顿何以应对?

由于太多天没有消息,杨柳松的朋友都以为他遇难了。2014年,另一个网友沿着这条路线进入羌塘,就失踪了,没有走出来。

二、物资准备

1400公里的路程,预计80天左右完成,平均每天推进20公里左右。

我必须达到均速二十公里才有可能完成计划中的旅行。二十公里是理想值,比较安全。但算上大雪封路、地貌巨糟、无端生病、洪水卷地、偷懒睡觉、外星人突访等事件,所以每天行距必须超过二十公里才稳妥。十八公里是危险值,最低均速,底线,咬咬牙能接受。低于十八公里,马克思会很想念我。回顾,在遇人救助前的七十五天独行中,每天均速差那么一点到十八公里,悬而又悬的速度。

20公里看上去并不快,但不要忘记这是在5000米的高原。

有过高原经历的人就知道,每天近二十公里的速度是怎么来的。除了极少的路可小骑过个瘾,其余路况很少能一次推百米不停下喘口气的。硬草地一般推个五十米喘口气,寒漠土二十米喘口气,沙土路十米喘口气,重沙地三五米喘口气,陡坡半个轮圈喘口气。这羌塘就是这么一口口气喘过来的。

既然预计80天,那么就需要准备足够的食物。

此行,食物约有一百斤,其中主食七十七斤,包括五十斤糌粑、二十五斤压缩饼干、二斤麦片,实际使用七十五斤,前期糟蹋了二斤主食。辅食二十五斤,大蒜、酥油、花生米三样就占了一半。其余是少量的盐、紫菜、辣椒粉、茶叶等,以及一点打牙祭的奶粉和白糖。全程无肉,吃过一次蔬菜,微量元素靠金施尔康药片。

这些食物根本不够提供足够的热量和营养,但也没法带更多了。

前四十五天日均摄取热量在一千四百大卡左右,大致四两多糌粑和二点五两压缩饼干,一些汤料为辅。热量摄取水平属于联合国认定的中度饥饿状态,下午四点以后基本就无力了。

为了加热食物和取暖,还需要携带汽油。

此行带了8.6升93号汽油,两个锅,1.5升的大锅主要用于烧水。经过几天实测,烧水需时如下:液体水,早晨要用十六分钟左右烧开,晚上则快些;化冰三十五至四十分钟;化雪耗时最长,四十五至五十分钟。为了节省汽油,所以在雪和轻度盐碱水同存的情况下,我多数会选择后者作为饮用水。汽油同样计划八十天用量,理论上是够了,但实际环境中无法准确计算,它包括化冰雪的次数、高原缺氧对汽油燃烧效率的影响,低温下的散温系数、炉头积碳导致的热量损耗等等。唯一有利因素就是低压环境中水的沸点低,只有80℃左右。因此,汽油使用非常节约,每天就是烧两锅水,早晚各一锅,便是所有生活所需了。

除了这些,其他的物品还有许多。

就拿此行束缚类装备来说,就有六毫米登山辅力绳、四毫米登山辅力绳、一点五毫米魔术贴捆扎带、一米魔术贴捆扎带、二十厘米魔术贴捆扎带、硅胶弹力带、普通松紧绳、红头风筝绳、凯夫拉鱼线、两色缝补线、五号铁丝、二号铁丝、大号长尾夹、小号长尾夹、橡皮筋、别针、小快扣、主锁......所有装备乱七八糟地加起来,到底有多少,我现在也没法记清了。

三、自行车

所有行李一共200斤,背是背不动的,必须带上自行车。

从徒步角度看,推行速度确实太低了,一半都达不到。选择自行车作为穿越工具并非为骑,它只是一个驮货工具。四个驮包分别置于前后轮两侧,相机包挂在车头,一个二十五升的骑行包随身背着。后货架上还有一个六十升防水驮包,用弹性松紧绳捆绑,其间夹塞着防潮垫、水袋、拖鞋等。驮包总容量一百八十五升左右,不包括外置的盛水容器、闲杂物品等,所有装备总重二百斤左右。试想,推着一辆二百斤的自行车在海拔五千米的沙地里推行,实在是一件令人沮丧的事情。

大部分路段,自行车没法骑,只能推着走。为了提速,杨柳松用伞做了一个帆,利用风力推动自行车。可惜风太大了,只用了两天,伞就被吹坏了。

为了解决用电问题,他带了太阳能电池。

由于单独采用了一块轻薄的太阳能板给GPS供电,使得GPS高耗电成为历史,七十多天里只因故障换过一次电池,节省了需携带的大量干电池。同时备了一块功率5.4瓦的太阳能板,给7.4伏的相机、DV及其他数码设备供电,路上没缺过电只缺过水。

四、出发

原定4月1日进入荒原。这个日期是精心选择的。

太早过于寒冷与干旱,人扛不住,水的问题也没法解决。太晚会推迟进入东羌塘的时间,雨季来临,冻土消融,几乎没有通过的可能性。所以四月初进入荒原是个最折中选择,可以使我吃一个半月的西风和冻土路面,剩下时间则在雨季变得强势前迅速撤离。

但是遇到了许多意外,比如在江孜县丢了一个包,不得不重回拉萨采购装备,导致比预定日期推迟了20天,注定后期遭遇雨季,几乎困死于沼泽。

2010年4月20日,他进入羌塘。

五、无人区

羌塘是无人区,地理和气候都不适合人类生存。首先,高海拔对生理产生巨大影响。

在海拔五千米荒原行路,相当于在平原地区背负十五公斤物品。高海拔对身体的伤害,首先是视力衰退,所以把狼看成外星人,不仅只是端不稳望远镜。其次是听力,海拔越高影响越大,不仅听力急剧减弱,连声音方位也难辨别,这也是高海拔登山容易发生事故的原因之一。高海拔对大脑的伤害尤为大,记忆力会明显下降,譬如,刚向你借钱就忘了,真不是我想赖账。甚者,导致严重的思维障碍。

然后,终日大风,风声足以致聋。

风大到只敢以屁股相对。风,也是羌塘唯一的声音。时常一整天,耳畔都是巨大的轰鸣声,即便饿狼贴着后脑勺,大声喊"我想你",也决然感觉不到半点危机。曾经有位边防战士,独自巡逻两天,被荒原大风吹得失聪。

遇到暴风雪,只能躲在自行车后面。

我猥琐地蜷缩在自行车后面,躲避着狂风冰雹。这是标准姿势,面对恶劣天气,将自行车横风倒下,整个人团成个肉球,趴在隆起的驮包后面。远处看,则脑袋不见,屁股半撅,有如鸵鸟。

......狂风大作,沙尘滚滚,眼前一片昏黄,视野近无。测量,瞬间最高风速达到了27.8m/s,接近十一级。蜷缩在自行车后面,用一个防雨罩把脑袋完全包住,蓦然,不知什么东西击中了屁股,被人踢了一脚似的。理智思量,不会有人路过,并这么无聊地踢我屁股吧。过了一会儿,屁股又被踢了一下,又一下......后背有点发凉,决定用手摸摸,到底何物。摸到屁股位置有根带子,噼里啪啦地风中乱抽。胡思乱想,难不成恶人不是用脚,而是用鞭子抽我屁股?顺着摸,是车首包的背带。

气候极度寒冷,有时帐篷搭得慢一点就没命了。

搭好帐篷去打水,溪流表面已结了薄冰,破之取水。格外酷寒,水杯捞起转瞬间,杯壁上便结了冰。手冷得不行,几秒钟就冻僵。......羌塘冻死,唯雨季风险最大,因为来不及防范,就可能在一场猛烈的冻雨湿雪中失尽体温。

长时间野外行走,手指和脚趾都会冻伤。

一只手握在车座下的竖管上,使劲地往前拉,这也是手指关节处裂口长达几十天不愈的原因。有几次拉车,感觉不到手指,以为被拉断了。脱下手套看看,还在,继续拉。

遇到大雪,甚至会不知不觉窒息而死。

昨夜,睡得很不踏实,中途迷迷糊糊闷醒多次,胸口如压巨石,喘不过气来。再次被闷醒时,见天亮了,但帐篷上明下黑,难道陷到沼泽里了?用手一推才知被大雪埋了。这是此行遭遇的最大一场雪,深度四十厘米左右,最深处一米。后怕,帐篷被大雪完全封死,内部只有一个小透气窗,半掩着,且冰雪又封了一些。简单计算,夜里帐内氧气含量比珠峰峰顶还低,这还不算体内呼出的二氧化碳的致命影响。半夜闷醒多次,居然浑然不觉大难临头。

六、野生动物

荒原里面有很多危险的野生动物,遇到狼是家常便饭。

此次遇狼七次,其中五次是直面。这是指看清脸的,幽魂般闪烁的不配我记录在案。

......先是一只狼从前方沿着土埂小跑,又觉得眼花,土埂后有一黑物闪烁,果然也是只狼,便盯着隐狼看它去向。隐狼完全现身一会儿后又不见了,再看前狼朝我直奔而来。先是放倒车子,故意和车保持一点距离,是想传达我可不是一个人孤军奋战。这招貌似不管用,如果自行车能弯弯车把,向狼打个招呼就好了。我又扶起车子,用身体靠住,万一时刻还能充当下防弹衣。

有时早上起床,发现帐篷外面都是狼的脚印。

帐篷周边发现了一些兽迹,应该是狼的。脚印从戈壁深处来,围了帐篷一圈,在头部位置零散纷乱,估计在判断我的气息,然后脚印沿着湖岸远去。

其他比较危险的,主要是棕熊和野牦牛。

此次旅行遇熊五次,同样,那些小脸都不让我看清的不配记录在案。

有两次刻骨地与野牦牛对峙的经历,距离之近,仿若能看见牛眼里的红血丝,我脊背上的寒气也足以给一间客厅降温避暑了。

再怎么防备,都难以保证百分之百的安全。

晚上宿营没有过任何防范,觉得实在没有必要,睡在哪里不是睡在黑夜里?白天遇野兽连贴身小刀也没摸过,徒步探路时基本无防范。羌塘真的很大,有什么东西早发现了。如果棕熊、野牦牛之类的真攻击你,恐怕带枪也不行,所以说心态最重要,学会相处比学会打架管用。

比野生动物更危险的,其实是人。荒原遇到人,比遇狼还令人担心。

凡是能进入荒原的人,大半我都惹不起,譬如杀人灭口的盗猎者,见财起意的淘金人,恪尽职守的巡山队......

七、断水

二分之一的路段都缺水,即使有水,也是盐碱水,不喝渴死,喝了毒死。

晚上,唯一的水就是前日为以防万一、灌在保温杯里的碱水。一打开盖子,恶臭扑鼻,捂了几天,水质更恶化了。喝是不喝,是个问题。

整个旅程最危险的时候是第40天,已经连续三天断水,滴水不存。必须在一天内找到水。

找水有几个方向,一是附近可能有水的地方,二是回到两天前的营地,三是标准求生方式----挖坑蒸水。先排除最后一点,挖坑蒸水对地貌要求极高,我周遭环境至少挖坑半米才能见到湿土。假使挖坑还没把我累死,假使一整天都是艳阳高照,我所得到的水够不够我继续挖下一个坑的力气都不好说。回到两天前的地方,往返八十公里寻水太纠结了,且以当下身体状态,这不是一件靠意志力就能胜任的工作,远水解不了近渴。

他不得不停止行程,专程找水,依然一无所获,只能用尿液解渴。

用尿液伴着饼干,勉强吃了一点,明天还得要有体力继续找水。

眼看即将渴死在这个极旱的盐碱地,所幸第41天早上下雪了。

听见打在帐篷上的沙沙声,以为又是风沙作祟,一整夜都是这样。微明,掀开帐篷一角,地上散落着小雪籽,再看了眼天际,灰云铺顶。不喜不悲,白天下雪很难积蓄,雪落无痕。心中也并无打算今天如何找水,身体透支太多,再难强打精神。也无祈祷,该怎样怎样。再次醒来,掀开帐门,雪越下越大,才清醒些,认为是生机。随后,三两分钟掀帐查看一下雪情,生怕老天赏赐的大礼会长脚溜走。待雪稍有积淀,便赶紧用纸片掠雪盛在锅里,沾上的沙土也舍不得丢弃。

八、雨季

六月以后,雨季到来,雪水融化,冻土变成沼泽和湖泊。对于旅行者来说,这时才是真正的噩梦开始。

鞋子早就破了,虽然尽量控制不要灌水,但依然是最煎熬的体感。试想,脚上套着两坨沉重的冰行路是何等苦楚,而这种苦楚将一直伴随我走出荒原。

第57天 原定的南下路线被雪水汇成的大河阻断,走不过去了。

晚上考量许久,何去何从?两天来只推行了十多公里,这沼泽路没法走。眼前又是条大河阻挡,豁命也不是没有过去的可能,但过去之后呢?至少还要横切汇入多格错仁强错的三条大河以及大片湖盆沼泽。

最后决定走回头路,掉头北上,穿越阿尔金无人区求生,因为那里的地势高,不易积水。但是,粮食是肯定不够了,只能寄望于路上遇到人。

从阿尔金出去,这是最后一个选择,没法拒绝。从地理上分析,或许北上是条出路,因为要翻越数道山脉,属山地貌,沼泽大河会少很多。北上的最大障碍是线路太长,食物肯定是不够了,且又是"未知"区域。我的打算是尽量赶到鲸鱼湖,传说,那里有获得补给的可能。

九、弹尽粮绝

第55天,蔬菜只剩下最后四瓣大蒜,吃光。

第59天,盐和粮食所剩无几

第64天,吃光了最后一包压缩饼干。

再次清点粮食,只剩下五斤左右糌粑,比乒乓球大小还少的盐,一点茶叶,三两左右酥油,再无其他了。

后面的路,都是冻土融化的沼泽地带。

一陷到膝盖,立马倒地匍匐爬出来。不能往前倒,而是后仰,往前倒,如果沼泽过稠,速度过快会折断小腿,如果沼泽过稀,会一下子把脸给埋住,无法呼吸,错失转身时机。后仰的好处是安全,抽腿也容易。

自行车也惨遭灭顶。

自行车再度陷入沼泽,一脚陷脚踝,二脚陷半小腿,三脚弃车跑人。黏性太大了,再不撤来不及了。随后试着拖车,地越踩越烂,地下水都踩了出来,最后连自行车的边都摸不上了。都考虑弃车了,但装备总得要弄出来。用那十块钱的救生膜和两个防雨罩铺在烂泥上,人趴在上面,一番折腾,好歹把驮包给弄了出来。再看垫脚物早没影了,光救生膜铺开可就两平方米。再一会儿,放驮包的地方也无法立足了。也就是说,即便很硬实的地,多踩上几脚,也变成了沼泽嘴脸。转移驮包后,思量着怎么把自行车弄出来,现在弃车毕竟很不理智。用铁丝套上绳子圈在自行车上,拉拽,车子勉强移动一点,再用力,居然把后轮生生地拽掉了,最终和沼泽打了个平手。

第73天,丢弃自行车,徒步逃生。为了减轻负重,把望远镜和水袋也扔了。

第75天,只剩最后一口粮食。幸运的是,在这一天遇见了探矿者。

第77天,走出荒原。

十、动机

很多人都有这样的问题:他究竟为什么要进入荒原,如此危险,难道只是为了探险吗?

杨柳松这样解释,对于城市生活,他始终不太适应。

身体每天都被什么东西紧紧束缚似的,那种感觉就像每天衣服都小一号,每天都必须换上大一号衣服,身体才会舒畅。

每年春天,只要在西藏,他都要去雅鲁藏布江大峡谷看桃花。最喜欢的是下面这样的景色。

最喜孤立的某处,一树粉红桃花与世无争地怒放。

三年前,他第一次见到荒原,就下决心要深入这片土地。这个刹那的闪念,让他长久酝酿,一定要付诸行动。荒原更适合那一颗自由自在的心。

始终没有逃离荒原的心,这样一片神奇土地,怎舍得轻易离去。只要不是毫无生机的绝境,在我的意识里,荒原彼端将是没有尽头的远方。远方,依然是那永远也到达不了的地方。

旅途中,每一次艰难时刻,选择继续前进还是放弃,他都选择前进,始终没有放弃。

事实上,选择北上的真正原因,是自己还没玩够。如果,世界上再无有意思的事,为之无条件追求的事,那活着多没趣。如果真得撑不住了,我就南下徒步逃命去了,三四天时间就可找到牧民,终结这痛苦的旅程。甚至,过河办法都想好了,就是抱着那空油桶,漂啊漂过河,像八仙成员之一似的。

那是一种从骨头里,对喧嚣的平庸生活的厌倦。

这是一个浮躁的时代,很拥挤,很冷漠,不管你愿不愿意,都必须随流而动,因此旅行成了一部分人变相的逃离,而非遵循内心的渴望。就我而言,为何旅行,同样没有一个靠谱的答案,热爱是最接近的答案,去追寻荒野的旷寂。

人生就像没有尽头的荒原,如果不找到一点乐趣,如何坚持着走完?

人生就是一场漫无目的的旅行,之所以茫然,是因生与死限定了旅行的终与结。有些人乐此不疲地怀揣梦想继续前行,没有目标,没有问题,只是收获一路感受。

他写道,自己很享受在荒野中看云。

困守的唯一好处,就是有足够时间凝视一朵云的万般变化,生成,绚烂,湮灭。

人生难道不是这样吗?我们都是飘荡在天空中的云,匆匆涌起,转眼消散得无影无踪,那么为什么不索性飘荡得远一点呢?

十一、未来

许多友人问,走出荒原是什么感觉?

事实上,走出荒原没有想象的幸福感,或是什么成就感,甚至是一种轻度的抑郁和迷茫。巨大的幸福并未如期而至,偶尔的幸福也是短暂。生命是一条贯通的河流,一切皆是没有开始的复始。我们所期望的终点并不存在。

如果我拥有足够的热情,如果这片荒原对我有足够的诱惑,那我就继续往前。如果激情退却,诱惑不再,我就哪来哪回。如果激情与诱惑从未真实存在,所有问题也就不是问题了。暮色中,面对荒原,我必须做出抉择,明日之路是前行,还是后退。这个抉择其实从未存在,只是一个矫情的过程,从心底深处再次认识到自己究竟在做些什么。

虽然,路的尽头什么也没有,但不能因此停止步履,因为你就是路本身。

-- 摘自杨柳松的公众号《逆流之河》(ID: s7s7s777)

(完)

文档信息

]]>
最近有一部电影《77天》,讲述一个探险者在荒原独自旅行的故事。

我以为电影是虚构的,没想到改编自真人真事。2010年,一个名叫杨柳松的青年,徒步穿越羌塘,1400公里的无人区,平均海拔5000米,走了77天。

他把这段经历,写成了一本书《北方的空地》

我读了,越读越佩服。这完全不是普通的冒险,跟川藏线的骑行是两个概念。不仅仅需要勇气和意志,还需要广博的知识,以及准确的现场判断,一个决定错误可能就会送命了。我惊叹,杨柳松是什么人啊,怎么知道这么多?!

电影《77天》挺一般的,可以不看。但是,热爱旅行和大自然的朋友都应该读一下这本书。它的雏形是杨柳松发在8624论坛的一个长篇连载,现在还能读到。

一、羌塘

杨柳松穿越的羌塘位于西藏的北部,藏语的意思是"北方的未知之地"。它的北面是昆仑山脉和可可西里山脉,南面是冈底斯山脉和念青唐古拉山脉,属于高山之间的一块高原盆地。

......南北最宽760公里,东西长约1200公里。面积59.70万平方公里,占青藏高原总面积的1/4。 行政上属西藏自治区的那曲与阿里两地区管辖。

羌塘高原日照强烈,天气变化无常,风力强劲,十一级大风是家常便饭。冬季极寒缺水,雨季沼泽遍布,是地球上最大的无人区之一。

(图片说明:杨柳松的实际路线)

没有后援,没有补给,与外界断绝联系,几十天不遇人,一个人徒步横穿羌塘,谈何容易。此前,还没有人完成过。

最大困惑有三个方面。一是食物补给,在如此长的天数里完全靠自给确无先例,我到底能承受怎样的饥饿状态?二是体能,在海拔五千米的恶劣环境中超负重推行能坚持多久?三是心理状态,孤身荒原中,面对周而复始的困顿何以应对?

由于太多天没有消息,杨柳松的朋友都以为他遇难了。2014年,另一个网友沿着这条路线进入羌塘,就失踪了,没有走出来。

二、物资准备

1400公里的路程,预计80天左右完成,平均每天推进20公里左右。

我必须达到均速二十公里才有可能完成计划中的旅行。二十公里是理想值,比较安全。但算上大雪封路、地貌巨糟、无端生病、洪水卷地、偷懒睡觉、外星人突访等事件,所以每天行距必须超过二十公里才稳妥。十八公里是危险值,最低均速,底线,咬咬牙能接受。低于十八公里,马克思会很想念我。回顾,在遇人救助前的七十五天独行中,每天均速差那么一点到十八公里,悬而又悬的速度。

20公里看上去并不快,但不要忘记这是在5000米的高原。

有过高原经历的人就知道,每天近二十公里的速度是怎么来的。除了极少的路可小骑过个瘾,其余路况很少能一次推百米不停下喘口气的。硬草地一般推个五十米喘口气,寒漠土二十米喘口气,沙土路十米喘口气,重沙地三五米喘口气,陡坡半个轮圈喘口气。这羌塘就是这么一口口气喘过来的。

既然预计80天,那么就需要准备足够的食物。

此行,食物约有一百斤,其中主食七十七斤,包括五十斤糌粑、二十五斤压缩饼干、二斤麦片,实际使用七十五斤,前期糟蹋了二斤主食。辅食二十五斤,大蒜、酥油、花生米三样就占了一半。其余是少量的盐、紫菜、辣椒粉、茶叶等,以及一点打牙祭的奶粉和白糖。全程无肉,吃过一次蔬菜,微量元素靠金施尔康药片。

这些食物根本不够提供足够的热量和营养,但也没法带更多了。

前四十五天日均摄取热量在一千四百大卡左右,大致四两多糌粑和二点五两压缩饼干,一些汤料为辅。热量摄取水平属于联合国认定的中度饥饿状态,下午四点以后基本就无力了。

为了加热食物和取暖,还需要携带汽油。

此行带了8.6升93号汽油,两个锅,1.5升的大锅主要用于烧水。经过几天实测,烧水需时如下:液体水,早晨要用十六分钟左右烧开,晚上则快些;化冰三十五至四十分钟;化雪耗时最长,四十五至五十分钟。为了节省汽油,所以在雪和轻度盐碱水同存的情况下,我多数会选择后者作为饮用水。汽油同样计划八十天用量,理论上是够了,但实际环境中无法准确计算,它包括化冰雪的次数、高原缺氧对汽油燃烧效率的影响,低温下的散温系数、炉头积碳导致的热量损耗等等。唯一有利因素就是低压环境中水的沸点低,只有80℃左右。因此,汽油使用非常节约,每天就是烧两锅水,早晚各一锅,便是所有生活所需了。

除了这些,其他的物品还有许多。

就拿此行束缚类装备来说,就有六毫米登山辅力绳、四毫米登山辅力绳、一点五毫米魔术贴捆扎带、一米魔术贴捆扎带、二十厘米魔术贴捆扎带、硅胶弹力带、普通松紧绳、红头风筝绳、凯夫拉鱼线、两色缝补线、五号铁丝、二号铁丝、大号长尾夹、小号长尾夹、橡皮筋、别针、小快扣、主锁......所有装备乱七八糟地加起来,到底有多少,我现在也没法记清了。

三、自行车

所有行李一共200斤,背是背不动的,必须带上自行车。

从徒步角度看,推行速度确实太低了,一半都达不到。选择自行车作为穿越工具并非为骑,它只是一个驮货工具。四个驮包分别置于前后轮两侧,相机包挂在车头,一个二十五升的骑行包随身背着。后货架上还有一个六十升防水驮包,用弹性松紧绳捆绑,其间夹塞着防潮垫、水袋、拖鞋等。驮包总容量一百八十五升左右,不包括外置的盛水容器、闲杂物品等,所有装备总重二百斤左右。试想,推着一辆二百斤的自行车在海拔五千米的沙地里推行,实在是一件令人沮丧的事情。

大部分路段,自行车没法骑,只能推着走。为了提速,杨柳松用伞做了一个帆,利用风力推动自行车。可惜风太大了,只用了两天,伞就被吹坏了。

为了解决用电问题,他带了太阳能电池。

由于单独采用了一块轻薄的太阳能板给GPS供电,使得GPS高耗电成为历史,七十多天里只因故障换过一次电池,节省了需携带的大量干电池。同时备了一块功率5.4瓦的太阳能板,给7.4伏的相机、DV及其他数码设备供电,路上没缺过电只缺过水。

四、出发

原定4月1日进入荒原。这个日期是精心选择的。

太早过于寒冷与干旱,人扛不住,水的问题也没法解决。太晚会推迟进入东羌塘的时间,雨季来临,冻土消融,几乎没有通过的可能性。所以四月初进入荒原是个最折中选择,可以使我吃一个半月的西风和冻土路面,剩下时间则在雨季变得强势前迅速撤离。

但是遇到了许多意外,比如在江孜县丢了一个包,不得不重回拉萨采购装备,导致比预定日期推迟了20天,注定后期遭遇雨季,几乎困死于沼泽。

2010年4月20日,他进入羌塘。

五、无人区

羌塘是无人区,地理和气候都不适合人类生存。首先,高海拔对生理产生巨大影响。

在海拔五千米荒原行路,相当于在平原地区背负十五公斤物品。高海拔对身体的伤害,首先是视力衰退,所以把狼看成外星人,不仅只是端不稳望远镜。其次是听力,海拔越高影响越大,不仅听力急剧减弱,连声音方位也难辨别,这也是高海拔登山容易发生事故的原因之一。高海拔对大脑的伤害尤为大,记忆力会明显下降,譬如,刚向你借钱就忘了,真不是我想赖账。甚者,导致严重的思维障碍。

然后,终日大风,风声足以致聋。

风大到只敢以屁股相对。风,也是羌塘唯一的声音。时常一整天,耳畔都是巨大的轰鸣声,即便饿狼贴着后脑勺,大声喊"我想你",也决然感觉不到半点危机。曾经有位边防战士,独自巡逻两天,被荒原大风吹得失聪。

遇到暴风雪,只能躲在自行车后面。

我猥琐地蜷缩在自行车后面,躲避着狂风冰雹。这是标准姿势,面对恶劣天气,将自行车横风倒下,整个人团成个肉球,趴在隆起的驮包后面。远处看,则脑袋不见,屁股半撅,有如鸵鸟。

......狂风大作,沙尘滚滚,眼前一片昏黄,视野近无。测量,瞬间最高风速达到了27.8m/s,接近十一级。蜷缩在自行车后面,用一个防雨罩把脑袋完全包住,蓦然,不知什么东西击中了屁股,被人踢了一脚似的。理智思量,不会有人路过,并这么无聊地踢我屁股吧。过了一会儿,屁股又被踢了一下,又一下......后背有点发凉,决定用手摸摸,到底何物。摸到屁股位置有根带子,噼里啪啦地风中乱抽。胡思乱想,难不成恶人不是用脚,而是用鞭子抽我屁股?顺着摸,是车首包的背带。

气候极度寒冷,有时帐篷搭得慢一点就没命了。

搭好帐篷去打水,溪流表面已结了薄冰,破之取水。格外酷寒,水杯捞起转瞬间,杯壁上便结了冰。手冷得不行,几秒钟就冻僵。......羌塘冻死,唯雨季风险最大,因为来不及防范,就可能在一场猛烈的冻雨湿雪中失尽体温。

长时间野外行走,手指和脚趾都会冻伤。

一只手握在车座下的竖管上,使劲地往前拉,这也是手指关节处裂口长达几十天不愈的原因。有几次拉车,感觉不到手指,以为被拉断了。脱下手套看看,还在,继续拉。

遇到大雪,甚至会不知不觉窒息而死。

昨夜,睡得很不踏实,中途迷迷糊糊闷醒多次,胸口如压巨石,喘不过气来。再次被闷醒时,见天亮了,但帐篷上明下黑,难道陷到沼泽里了?用手一推才知被大雪埋了。这是此行遭遇的最大一场雪,深度四十厘米左右,最深处一米。后怕,帐篷被大雪完全封死,内部只有一个小透气窗,半掩着,且冰雪又封了一些。简单计算,夜里帐内氧气含量比珠峰峰顶还低,这还不算体内呼出的二氧化碳的致命影响。半夜闷醒多次,居然浑然不觉大难临头。

六、野生动物

荒原里面有很多危险的野生动物,遇到狼是家常便饭。

此次遇狼七次,其中五次是直面。这是指看清脸的,幽魂般闪烁的不配我记录在案。

......先是一只狼从前方沿着土埂小跑,又觉得眼花,土埂后有一黑物闪烁,果然也是只狼,便盯着隐狼看它去向。隐狼完全现身一会儿后又不见了,再看前狼朝我直奔而来。先是放倒车子,故意和车保持一点距离,是想传达我可不是一个人孤军奋战。这招貌似不管用,如果自行车能弯弯车把,向狼打个招呼就好了。我又扶起车子,用身体靠住,万一时刻还能充当下防弹衣。

有时早上起床,发现帐篷外面都是狼的脚印。

帐篷周边发现了一些兽迹,应该是狼的。脚印从戈壁深处来,围了帐篷一圈,在头部位置零散纷乱,估计在判断我的气息,然后脚印沿着湖岸远去。

其他比较危险的,主要是棕熊和野牦牛。

此次旅行遇熊五次,同样,那些小脸都不让我看清的不配记录在案。

有两次刻骨地与野牦牛对峙的经历,距离之近,仿若能看见牛眼里的红血丝,我脊背上的寒气也足以给一间客厅降温避暑了。

再怎么防备,都难以保证百分之百的安全。

晚上宿营没有过任何防范,觉得实在没有必要,睡在哪里不是睡在黑夜里?白天遇野兽连贴身小刀也没摸过,徒步探路时基本无防范。羌塘真的很大,有什么东西早发现了。如果棕熊、野牦牛之类的真攻击你,恐怕带枪也不行,所以说心态最重要,学会相处比学会打架管用。

比野生动物更危险的,其实是人。荒原遇到人,比遇狼还令人担心。

凡是能进入荒原的人,大半我都惹不起,譬如杀人灭口的盗猎者,见财起意的淘金人,恪尽职守的巡山队......

七、断水

二分之一的路段都缺水,即使有水,也是盐碱水,不喝渴死,喝了毒死。

晚上,唯一的水就是前日为以防万一、灌在保温杯里的碱水。一打开盖子,恶臭扑鼻,捂了几天,水质更恶化了。喝是不喝,是个问题。

整个旅程最危险的时候是第40天,已经连续三天断水,滴水不存。必须在一天内找到水。

找水有几个方向,一是附近可能有水的地方,二是回到两天前的营地,三是标准求生方式----挖坑蒸水。先排除最后一点,挖坑蒸水对地貌要求极高,我周遭环境至少挖坑半米才能见到湿土。假使挖坑还没把我累死,假使一整天都是艳阳高照,我所得到的水够不够我继续挖下一个坑的力气都不好说。回到两天前的地方,往返八十公里寻水太纠结了,且以当下身体状态,这不是一件靠意志力就能胜任的工作,远水解不了近渴。

他不得不停止行程,专程找水,依然一无所获,只能用尿液解渴。

用尿液伴着饼干,勉强吃了一点,明天还得要有体力继续找水。

眼看即将渴死在这个极旱的盐碱地,所幸第41天早上下雪了。

听见打在帐篷上的沙沙声,以为又是风沙作祟,一整夜都是这样。微明,掀开帐篷一角,地上散落着小雪籽,再看了眼天际,灰云铺顶。不喜不悲,白天下雪很难积蓄,雪落无痕。心中也并无打算今天如何找水,身体透支太多,再难强打精神。也无祈祷,该怎样怎样。再次醒来,掀开帐门,雪越下越大,才清醒些,认为是生机。随后,三两分钟掀帐查看一下雪情,生怕老天赏赐的大礼会长脚溜走。待雪稍有积淀,便赶紧用纸片掠雪盛在锅里,沾上的沙土也舍不得丢弃。

八、雨季

六月以后,雨季到来,雪水融化,冻土变成沼泽和湖泊。对于旅行者来说,这时才是真正的噩梦开始。

鞋子早就破了,虽然尽量控制不要灌水,但依然是最煎熬的体感。试想,脚上套着两坨沉重的冰行路是何等苦楚,而这种苦楚将一直伴随我走出荒原。

第57天 原定的南下路线被雪水汇成的大河阻断,走不过去了。

晚上考量许久,何去何从?两天来只推行了十多公里,这沼泽路没法走。眼前又是条大河阻挡,豁命也不是没有过去的可能,但过去之后呢?至少还要横切汇入多格错仁强错的三条大河以及大片湖盆沼泽。

最后决定走回头路,掉头北上,穿越阿尔金无人区求生,因为那里的地势高,不易积水。但是,粮食是肯定不够了,只能寄望于路上遇到人。

从阿尔金出去,这是最后一个选择,没法拒绝。从地理上分析,或许北上是条出路,因为要翻越数道山脉,属山地貌,沼泽大河会少很多。北上的最大障碍是线路太长,食物肯定是不够了,且又是"未知"区域。我的打算是尽量赶到鲸鱼湖,传说,那里有获得补给的可能。

九、弹尽粮绝

第55天,蔬菜只剩下最后四瓣大蒜,吃光。

第59天,盐和粮食所剩无几

第64天,吃光了最后一包压缩饼干。

再次清点粮食,只剩下五斤左右糌粑,比乒乓球大小还少的盐,一点茶叶,三两左右酥油,再无其他了。

后面的路,都是冻土融化的沼泽地带。

一陷到膝盖,立马倒地匍匐爬出来。不能往前倒,而是后仰,往前倒,如果沼泽过稠,速度过快会折断小腿,如果沼泽过稀,会一下子把脸给埋住,无法呼吸,错失转身时机。后仰的好处是安全,抽腿也容易。

自行车也惨遭灭顶。

自行车再度陷入沼泽,一脚陷脚踝,二脚陷半小腿,三脚弃车跑人。黏性太大了,再不撤来不及了。随后试着拖车,地越踩越烂,地下水都踩了出来,最后连自行车的边都摸不上了。都考虑弃车了,但装备总得要弄出来。用那十块钱的救生膜和两个防雨罩铺在烂泥上,人趴在上面,一番折腾,好歹把驮包给弄了出来。再看垫脚物早没影了,光救生膜铺开可就两平方米。再一会儿,放驮包的地方也无法立足了。也就是说,即便很硬实的地,多踩上几脚,也变成了沼泽嘴脸。转移驮包后,思量着怎么把自行车弄出来,现在弃车毕竟很不理智。用铁丝套上绳子圈在自行车上,拉拽,车子勉强移动一点,再用力,居然把后轮生生地拽掉了,最终和沼泽打了个平手。

第73天,丢弃自行车,徒步逃生。为了减轻负重,把望远镜和水袋也扔了。

第75天,只剩最后一口粮食。幸运的是,在这一天遇见了探矿者。

第77天,走出荒原。

十、动机

很多人都有这样的问题:他究竟为什么要进入荒原,如此危险,难道只是为了探险吗?

杨柳松这样解释,对于城市生活,他始终不太适应。

身体每天都被什么东西紧紧束缚似的,那种感觉就像每天衣服都小一号,每天都必须换上大一号衣服,身体才会舒畅。

每年春天,只要在西藏,他都要去雅鲁藏布江大峡谷看桃花。最喜欢的是下面这样的景色。

最喜孤立的某处,一树粉红桃花与世无争地怒放。

三年前,他第一次见到荒原,就下决心要深入这片土地。这个刹那的闪念,让他长久酝酿,一定要付诸行动。荒原更适合那一颗自由自在的心。

始终没有逃离荒原的心,这样一片神奇土地,怎舍得轻易离去。只要不是毫无生机的绝境,在我的意识里,荒原彼端将是没有尽头的远方。远方,依然是那永远也到达不了的地方。

旅途中,每一次艰难时刻,选择继续前进还是放弃,他都选择前进,始终没有放弃。

事实上,选择北上的真正原因,是自己还没玩够。如果,世界上再无有意思的事,为之无条件追求的事,那活着多没趣。如果真得撑不住了,我就南下徒步逃命去了,三四天时间就可找到牧民,终结这痛苦的旅程。甚至,过河办法都想好了,就是抱着那空油桶,漂啊漂过河,像八仙成员之一似的。

那是一种从骨头里,对喧嚣的平庸生活的厌倦。

这是一个浮躁的时代,很拥挤,很冷漠,不管你愿不愿意,都必须随流而动,因此旅行成了一部分人变相的逃离,而非遵循内心的渴望。就我而言,为何旅行,同样没有一个靠谱的答案,热爱是最接近的答案,去追寻荒野的旷寂。

人生就像没有尽头的荒原,如果不找到一点乐趣,如何坚持着走完?

人生就是一场漫无目的的旅行,之所以茫然,是因生与死限定了旅行的终与结。有些人乐此不疲地怀揣梦想继续前行,没有目标,没有问题,只是收获一路感受。

他写道,自己很享受在荒野中看云。

困守的唯一好处,就是有足够时间凝视一朵云的万般变化,生成,绚烂,湮灭。

人生难道不是这样吗?我们都是飘荡在天空中的云,匆匆涌起,转眼消散得无影无踪,那么为什么不索性飘荡得远一点呢?

十一、未来

许多友人问,走出荒原是什么感觉?

事实上,走出荒原没有想象的幸福感,或是什么成就感,甚至是一种轻度的抑郁和迷茫。巨大的幸福并未如期而至,偶尔的幸福也是短暂。生命是一条贯通的河流,一切皆是没有开始的复始。我们所期望的终点并不存在。

如果我拥有足够的热情,如果这片荒原对我有足够的诱惑,那我就继续往前。如果激情退却,诱惑不再,我就哪来哪回。如果激情与诱惑从未真实存在,所有问题也就不是问题了。暮色中,面对荒原,我必须做出抉择,明日之路是前行,还是后退。这个抉择其实从未存在,只是一个矫情的过程,从心底深处再次认识到自己究竟在做些什么。

虽然,路的尽头什么也没有,但不能因此停止步履,因为你就是路本身。

-- 摘自杨柳松的公众号《逆流之河》(ID: s7s7s777)

(完)

文档信息

]]>
0
<![CDATA[游戏和游戏化(上)]]> http://www.udpwork.com/item/16451.html http://www.udpwork.com/item/16451.html#reviews Mon, 04 Dec 2017 22:18:32 +0800 唐巧 http://www.udpwork.com/item/16451.html 引言

最近看完了两本书:《游戏改变世界》和《游戏化思维》。前者是讲游戏的,后者是讲游戏化的。

《游戏改变世界》的作者简•麦戈尼格尔 (Jane McGonigal) 认为游戏不但好,而且建立了相对于真实社会的一种“平行宇宙”,进入游戏其实就像进入了另一个社会一样。所以本书的英文名更为贴切,叫:《Reality is Broken》。作者从头到尾都在夸游戏中的社会是如何如何好,现实是如何如何让人沮丧。

《游戏化思维》从另外一个角度来讲游戏:主要是将游戏中的一些元素融入到非游戏中,使得人们更加轻松地完成一些现实中的任务。

我们先说游戏吧。

游戏

《游戏改变世界》认为所有游戏都有一个决定性的特征:目标、规则、反馈系统和自愿参与。前三者比较好理解,最后一项自愿参与:要求所有玩游戏的人都了解并愿意接受目标,规则和反馈。这一条其实建立了玩家的安全感,保证玩家把游戏中的高压挑战当作愉快的活动。

《游戏改变世界》将游戏世界对现实世界的改造比喻成“补丁”。在书中,作者一共介绍了 14 个补丁,用于描述游戏世界相对于现实世界的优越性。

但是,我觉得作者的解释过于拖沓,并且没有重点。其实游戏世界相对于现实世界的最最核心的理由是:游戏世界构造了一些机制,使得玩家一直处于一种心流(flow)的状态。所谓的心流,是指游戏构造了一种合适的难度,这种难度使得玩家一直处于自己可以在能力上“够得着”的状态。在书中,这种状态被称作:”个人最优化的障碍”,这种状态可以让人始终产生成就感。

这种持续的成就感很快就会让人上瘾。上瘾之后,游戏会加大难度,使得大家为了再次体会到类似的快感欲罢不能。我们来看几个例子。

俄罗斯方块恐怕是最简单又让人上瘾的游戏了。在游戏的刚开始,一切操作速度都很慢,玩家很容易适应游戏的节奏。然后俄罗斯方块通过各种即时反馈,让人们感觉到”正反馈“。比如随时在增加的分数,消除掉多行的快感,于是人们觉得”有点意思“。但是这种心流状态很快随着熟练就丧失了,于是游戏的难度加大,你不得不集中精力来处理快速下落的方块,最终你输了,虽然有所遗憾,但是你得到了历史最高的分数。于是你顾不得休息,又开始了新一轮的尝试。

最近流行的游戏王者荣耀也是这样。如果你也玩王者荣耀,并且是iOS平台的玩家,建议你尝试拿个Android手机重新玩一次,由于苹果的限制,在王者荣耀中iOS和Android 手机并不能互通角色。然后你就可以重新体验一次游戏的整个生命期。

我玩了大概半年的王者荣耀,一次偶然的机会,我拿起身边的一台 Android 测试机重新登录了一个新号。于是我立马发现了王者荣耀为新手构建了一个异常强大的”个人最优化的障碍”。在我用 Android 重新玩的那几天,王者荣耀为我模拟了一个非常舒服的比赛环境,我在游戏中把把拿 MVP,几乎没有输过。我突然想到,我在 iOS 平台刚开始玩黄忠的时候,基本上每次都赢,但是之后才发现黄忠其实被很多英雄相克,我很好奇,在相当长的几个月中,我都没有遇到过一次黄忠的克星。

所以,我几乎可以肯定的是:王者荣耀为玩家构建了一个相当长时期的保护期。在这个保护期内,你就如同进入了 iOS 的沙盒环境,所有的对手都是为你特别准备的,刚好够让你杀个痛快还有一点挑战。我有一次细心观察了一下,对手有一个走位明显就是机器人。我才一下子明白了,原来这个环境竟然是虚拟的。

所以大家就可以明白了,为什么有那么多妹子喜欢玩王者荣耀,其实游戏玩得好的女生非常少,但是王者荣耀的”个人最优化的障碍”机制,使得很多陪练参与其中,真实的玩家得到了极大地鼓励。当然,你如果一直这么玩,难道水平就不会有涨进吗?答案是肯定的,这就像《异类》中提到的那些成功人士一样让人羡慕。

在《异类》中,作者分析发现那些各种所谓的天才,其实只是比别人更加早完成 10000 小时练习而已,而他们的坚持动力,很大程度上都来自于刚开始比别人稍微强一点点,这稍微一点点的优势,成为了他的激励和成就感来源。这其实就是真实社会的”个人最优化的障碍”。在书中印象最深的是加拿大冰球队队员大多集中在 1 月份的例子:

加拿大冰球队按年龄分组所依据的分界线是 1 月 1 日,即从 1 月 1 日到当年 12 月 31 日之间出生的球员将会被分在同一组。也就是说,一个 1 月 1 日出生的选手,是在跟许多年纪比他小的队友争夺晋级权。

而年龄大几个月而显现的微弱优势,会在孩子的成长过程中不断积累,最终引导孩子走向成功或不成功,自信或不自信的轨道中,其影响会延伸许多年。

社会学家罗伯特·默顿援引《新约·马太福音》,把这种现象叫作 “马太效应”。“凡是有的,还要加给他,叫他有余;没有的,连他所有的,也要夺过来。” 成功者,换句话说,就是获得这些特殊机遇的人,他们因此最终取得了更大的进步。

我们脑洞一下,如果整个人类社会都是虚拟的,然后整个社会为了某个真实玩家而构建”个人最优化的障碍”,例如让你每次努力和别人比赛的时候都能赢。每次成绩都获得家长、同学、老师的肯定。当然,给玩家刻意构造几次挫折和失败也是必要的。然后社会再给这个玩家各种千载难逢的机会。最终这个人从虚拟社会中”毕业“的时候,他会不会在各个需要后天培养的地方都达到我们理想中的标准?我觉得答案是肯定的。

这个设定在各种科幻题材的作品中常常看到。比如经典的电影《黑客帝国》,人类就生存在一个机器构造的虚拟世界(电影中叫Matrix)中。在美剧《Rick and Morty》的第一季第四集,也脑洞了一个完全虚拟的世界(下图)。

需要注意的是,”个人最优化的障碍”并不意味着一定要让玩家通关。在玩家对于游戏熟悉之后,通关并不是最爽的,相反,不能通关反倒是让人欲罢不能的事情。

国内的火爆游戏“天天爱消除”,其实就是让你一遍一遍不能通关,让你产生反复尝试的动力。最终有些人买了道具通关了,其实他的乐趣也结束了。“王者荣耀”也是这样,最终人们会陷在游戏给他的一个几乎不能完成的目标里面不能自拔,但是自己还乐在其中。

其它因素

当然,一款游戏光有”个人最优化的障碍”也是不够的,书中介绍的清晰的目标和规则、即时的反馈、低成本的失败压力、多人协作等元素,也是游戏成功必不可少的环节。

“个人最优化的障碍”的实现相对来说是有很多套路的。但是游戏的目标和规则却是真正比拼创意的事情。很多成功的游戏都开创了一些新的玩法从而成功。“王者荣耀”将 PC 时代的 DOTA 成功简化到手机上,优化掉各种装备购买,补刀等逻辑,这里面肯定花费了大量精力来做可玩性的设计。

单单是一款游戏里面的各种参数值,都会对游戏产生致命性影响。高中时我玩过一个 PC 网游叫“决战”,那款游戏的失败之处就在于法师的数值远高于别的职业,造成游戏完全没有平衡性可言。所以游戏开发里面专门有“数值设定”这个职位,他们的工作就是一遍一遍地调整游戏参数,使得整个世界的平衡性得以保证。

小结

总结一下,一款成功的游戏都有着”个人最优化的障碍”作为基础,在这之上,通过清晰的目标和规则、即时的反馈、低成本的失败压力、社交元素来构建出一个让人上瘾的环境。

更新(2017.12.04)

非常有意思的是,当本文在博客上发表之后,我的一个同事转给我一篇文章,上面介绍了 EA 公司申请的「动态难度调节」(Dynamic Difficulty Adjustment)专利。文章是这么介绍这个功能的:

当玩家的表现不尽如人意的时候,游戏会自动降低难度,让游戏变简单;当玩家表现神勇的时候,游戏又会调高难度,让游戏充满挑战——早在 1986 年,「红白机」FC 上的飞行射击游戏 Zanac 就曾引入过「动态难度调节」系统,当玩家 Game Over 的次数越多,游戏难度就越低。后来,《求生之路》(Left 4 Dead)、《生化危机 4》上也都采用过类似的系统。

这,不就是我们说了半天的“个人最优化障碍”么?

]]>
引言

最近看完了两本书:《游戏改变世界》和《游戏化思维》。前者是讲游戏的,后者是讲游戏化的。

《游戏改变世界》的作者简•麦戈尼格尔 (Jane McGonigal) 认为游戏不但好,而且建立了相对于真实社会的一种“平行宇宙”,进入游戏其实就像进入了另一个社会一样。所以本书的英文名更为贴切,叫:《Reality is Broken》。作者从头到尾都在夸游戏中的社会是如何如何好,现实是如何如何让人沮丧。

《游戏化思维》从另外一个角度来讲游戏:主要是将游戏中的一些元素融入到非游戏中,使得人们更加轻松地完成一些现实中的任务。

我们先说游戏吧。

游戏

《游戏改变世界》认为所有游戏都有一个决定性的特征:目标、规则、反馈系统和自愿参与。前三者比较好理解,最后一项自愿参与:要求所有玩游戏的人都了解并愿意接受目标,规则和反馈。这一条其实建立了玩家的安全感,保证玩家把游戏中的高压挑战当作愉快的活动。

《游戏改变世界》将游戏世界对现实世界的改造比喻成“补丁”。在书中,作者一共介绍了 14 个补丁,用于描述游戏世界相对于现实世界的优越性。

但是,我觉得作者的解释过于拖沓,并且没有重点。其实游戏世界相对于现实世界的最最核心的理由是:游戏世界构造了一些机制,使得玩家一直处于一种心流(flow)的状态。所谓的心流,是指游戏构造了一种合适的难度,这种难度使得玩家一直处于自己可以在能力上“够得着”的状态。在书中,这种状态被称作:”个人最优化的障碍”,这种状态可以让人始终产生成就感。

这种持续的成就感很快就会让人上瘾。上瘾之后,游戏会加大难度,使得大家为了再次体会到类似的快感欲罢不能。我们来看几个例子。

俄罗斯方块恐怕是最简单又让人上瘾的游戏了。在游戏的刚开始,一切操作速度都很慢,玩家很容易适应游戏的节奏。然后俄罗斯方块通过各种即时反馈,让人们感觉到”正反馈“。比如随时在增加的分数,消除掉多行的快感,于是人们觉得”有点意思“。但是这种心流状态很快随着熟练就丧失了,于是游戏的难度加大,你不得不集中精力来处理快速下落的方块,最终你输了,虽然有所遗憾,但是你得到了历史最高的分数。于是你顾不得休息,又开始了新一轮的尝试。

最近流行的游戏王者荣耀也是这样。如果你也玩王者荣耀,并且是iOS平台的玩家,建议你尝试拿个Android手机重新玩一次,由于苹果的限制,在王者荣耀中iOS和Android 手机并不能互通角色。然后你就可以重新体验一次游戏的整个生命期。

我玩了大概半年的王者荣耀,一次偶然的机会,我拿起身边的一台 Android 测试机重新登录了一个新号。于是我立马发现了王者荣耀为新手构建了一个异常强大的”个人最优化的障碍”。在我用 Android 重新玩的那几天,王者荣耀为我模拟了一个非常舒服的比赛环境,我在游戏中把把拿 MVP,几乎没有输过。我突然想到,我在 iOS 平台刚开始玩黄忠的时候,基本上每次都赢,但是之后才发现黄忠其实被很多英雄相克,我很好奇,在相当长的几个月中,我都没有遇到过一次黄忠的克星。

所以,我几乎可以肯定的是:王者荣耀为玩家构建了一个相当长时期的保护期。在这个保护期内,你就如同进入了 iOS 的沙盒环境,所有的对手都是为你特别准备的,刚好够让你杀个痛快还有一点挑战。我有一次细心观察了一下,对手有一个走位明显就是机器人。我才一下子明白了,原来这个环境竟然是虚拟的。

所以大家就可以明白了,为什么有那么多妹子喜欢玩王者荣耀,其实游戏玩得好的女生非常少,但是王者荣耀的”个人最优化的障碍”机制,使得很多陪练参与其中,真实的玩家得到了极大地鼓励。当然,你如果一直这么玩,难道水平就不会有涨进吗?答案是肯定的,这就像《异类》中提到的那些成功人士一样让人羡慕。

在《异类》中,作者分析发现那些各种所谓的天才,其实只是比别人更加早完成 10000 小时练习而已,而他们的坚持动力,很大程度上都来自于刚开始比别人稍微强一点点,这稍微一点点的优势,成为了他的激励和成就感来源。这其实就是真实社会的”个人最优化的障碍”。在书中印象最深的是加拿大冰球队队员大多集中在 1 月份的例子:

加拿大冰球队按年龄分组所依据的分界线是 1 月 1 日,即从 1 月 1 日到当年 12 月 31 日之间出生的球员将会被分在同一组。也就是说,一个 1 月 1 日出生的选手,是在跟许多年纪比他小的队友争夺晋级权。

而年龄大几个月而显现的微弱优势,会在孩子的成长过程中不断积累,最终引导孩子走向成功或不成功,自信或不自信的轨道中,其影响会延伸许多年。

社会学家罗伯特·默顿援引《新约·马太福音》,把这种现象叫作 “马太效应”。“凡是有的,还要加给他,叫他有余;没有的,连他所有的,也要夺过来。” 成功者,换句话说,就是获得这些特殊机遇的人,他们因此最终取得了更大的进步。

我们脑洞一下,如果整个人类社会都是虚拟的,然后整个社会为了某个真实玩家而构建”个人最优化的障碍”,例如让你每次努力和别人比赛的时候都能赢。每次成绩都获得家长、同学、老师的肯定。当然,给玩家刻意构造几次挫折和失败也是必要的。然后社会再给这个玩家各种千载难逢的机会。最终这个人从虚拟社会中”毕业“的时候,他会不会在各个需要后天培养的地方都达到我们理想中的标准?我觉得答案是肯定的。

这个设定在各种科幻题材的作品中常常看到。比如经典的电影《黑客帝国》,人类就生存在一个机器构造的虚拟世界(电影中叫Matrix)中。在美剧《Rick and Morty》的第一季第四集,也脑洞了一个完全虚拟的世界(下图)。

需要注意的是,”个人最优化的障碍”并不意味着一定要让玩家通关。在玩家对于游戏熟悉之后,通关并不是最爽的,相反,不能通关反倒是让人欲罢不能的事情。

国内的火爆游戏“天天爱消除”,其实就是让你一遍一遍不能通关,让你产生反复尝试的动力。最终有些人买了道具通关了,其实他的乐趣也结束了。“王者荣耀”也是这样,最终人们会陷在游戏给他的一个几乎不能完成的目标里面不能自拔,但是自己还乐在其中。

其它因素

当然,一款游戏光有”个人最优化的障碍”也是不够的,书中介绍的清晰的目标和规则、即时的反馈、低成本的失败压力、多人协作等元素,也是游戏成功必不可少的环节。

“个人最优化的障碍”的实现相对来说是有很多套路的。但是游戏的目标和规则却是真正比拼创意的事情。很多成功的游戏都开创了一些新的玩法从而成功。“王者荣耀”将 PC 时代的 DOTA 成功简化到手机上,优化掉各种装备购买,补刀等逻辑,这里面肯定花费了大量精力来做可玩性的设计。

单单是一款游戏里面的各种参数值,都会对游戏产生致命性影响。高中时我玩过一个 PC 网游叫“决战”,那款游戏的失败之处就在于法师的数值远高于别的职业,造成游戏完全没有平衡性可言。所以游戏开发里面专门有“数值设定”这个职位,他们的工作就是一遍一遍地调整游戏参数,使得整个世界的平衡性得以保证。

小结

总结一下,一款成功的游戏都有着”个人最优化的障碍”作为基础,在这之上,通过清晰的目标和规则、即时的反馈、低成本的失败压力、社交元素来构建出一个让人上瘾的环境。

更新(2017.12.04)

非常有意思的是,当本文在博客上发表之后,我的一个同事转给我一篇文章,上面介绍了 EA 公司申请的「动态难度调节」(Dynamic Difficulty Adjustment)专利。文章是这么介绍这个功能的:

当玩家的表现不尽如人意的时候,游戏会自动降低难度,让游戏变简单;当玩家表现神勇的时候,游戏又会调高难度,让游戏充满挑战——早在 1986 年,「红白机」FC 上的飞行射击游戏 Zanac 就曾引入过「动态难度调节」系统,当玩家 Game Over 的次数越多,游戏难度就越低。后来,《求生之路》(Left 4 Dead)、《生化危机 4》上也都采用过类似的系统。

这,不就是我们说了半天的“个人最优化障碍”么?

]]>
0
<![CDATA[Lua 下的 ECS 框架]]> http://www.udpwork.com/item/16522.html http://www.udpwork.com/item/16522.html#reviews Sun, 03 Dec 2017 23:01:21 +0800 云风 http://www.udpwork.com/item/16522.html 前段时间,我写了一篇浅谈《守望先锋》中的 ECS 构架。最近想试试在 Lua 中实现一个简单的 ECS 框架,又仔细琢磨了一下。

我思考后的结论是:ECS 并不是一个新概念,它的提出其实是和语言相关的。ECS 概念的诞生起于游戏行业,相关框架基本都是基于 C++ 来开发的。它其实是对 C++ 对象模型的一个反思。ECS 针对组件组合对象,而反对 C++ 固有的基于继承的对象模型。对象模型才是 ECS 的设计核心理念。而离开 C++ 的对象模型,ECS 并不是什么新鲜的东西。

我的这个观点也不新鲜,在 ECS 的 Wikipedia 页上也有类似的说法:

In the original talk at GDC Scott Bilas compares C++ object system and his new Custom component system. This is consistent with a traditional use of this term in general Systems engineering with Common Lisp Object System and Type system as examples. Therefore, the ideas of "Systems" as a first-class element is a personal opinion essay. Overall, ECS is a mixed personal reflection of orthogonal well-established ideas in general Computer science and Programming language theory. For example, components can be seen as a mixin idiom in various programming languages. Alternatively, components are just a small case under the general Delegation (object-oriented programming) approach and Meta-object protocol. I.e. any complete component object system can be expressed with templates and empathy model within The Orlando Treaty vision of Object-oriented programming,

抛开理论不谈,如果要在 Lua 中实践,我们到底可以做点什么呢?

我认为需要有这几个方面:

首先应该对 Lua 加强类型系统。Lua 的动态性天然支持把不同的组件聚合在一起,我们把不同的 Component 放在一张表里组合成 Entity 就足够了。但如果 Component 分的很细的话,用很多的表组合成一个 Entity 对象的额外开销不小。不像 C++ ,结构体聚合的额外开销几乎为零。我们完全可以把不同 Component 的数据直接平坦放在一个 table 中,只要键值不冲突即可。但是我们需要额外的类型信息方便运行时从 Entity 中萃取出 Component 来。另外,如果是 C / Lua 混合设计的话,某些 Component 还应该可以是 userdata 。

从节省空间及方便遍历的角度讲,我们甚至可以把同类的 C Component 聚合在一大块内存中,然后在 Entity 的 table 中只保留一个 lightuserdata 即可。ECS 的 System 最重要的操作就是遍历处理同类 Component ,这样天然就可以分为 C System 和 Lua System 。数据的内聚性很高,可以直接区分开 C data 和 Lua Data 。

然后、就是方便的遍历。ECS 的 System 需要做的就是筛选出它关心的 Entity ,针对其中的 Component 做操作。如果需要筛选结果大大少于全体 Entity 数量,遍历逐个判断就会效率很低。好在在 Lua 中,我们可以非常容易地做出 cache ,只需要遍历筛选一次,在监控新的 Component 的诞生就可以方便的维护遍历用的集合了。

我写了一个初步的版本。打算等到实际使用起来再慢慢完善。

它可以实现成一个纯 Lua 版,但我特地尝试把里面的两个函数编写了 C 的等价版,看起来可以提高不少性能。

这里的 API 中, Entity 全部使用唯一数字 id 标识,而不主张直接引用 entity 的 table 。这也是一般 ECS 框架的通用做法。数字 id 可以提高健壮性,还可以避免对已经销毁的 Entity 错误的引用。如果需要 C / Lua 混合编程的话,在 C 中引用 id 也方便的多。

类型系统是这样设计的:

每个 Component 有一个 16bit 的唯一类型 id ,每个 Entity 是由若干 Component 组合而成,我将它们的类型 id 升序排列成一个字符串,作为整个 Entity 的动态类型。然后 Lua 中实现了一个 cache ,可以从这个类型字符串转换成易用的类型对象。类型对象用来对 Entity 做筛选,从里面分离出 Component 。这个类型字符串的拼接我实现的是一个 C 版本,虽然 Lua 也能实现的出来,但是效率会低很多。

大部分 Component 我主张直接平坦的放在 Entity 对象表中,但若需要用 C 结构来承载,或单独用一个子表,也提供了 Component 类型注册方法,可以在 new component 时定义一个专门的构造函数(以及 delete 时的析构函数)。

这里还提供了遍历包含特定 Component 的 Entity 集合 的迭代器。它由一个弱表实现的 cache 来管理。在第一次遍历时,会创建一个集合,收集 Entity 全体集合中符合要求的部分,把筛选出来的 id 记录下来。一旦遍历集合创建好,它还会跟踪新的 Component 的创建,自动加进来。

这里的迭代器,我同时实现了 Lua 版本和 C 版本。C 版本的性能会高一些,我认为这个 C 版本在遍历相关集合时,性能表现不会差于用 C/C++ 实现的原生容器。

以上是对 Entity 和 Component 的支持。System 相关的方法,我还没有想好可以做点什么。等用到了再完善。

]]>
前段时间,我写了一篇浅谈《守望先锋》中的 ECS 构架。最近想试试在 Lua 中实现一个简单的 ECS 框架,又仔细琢磨了一下。

我思考后的结论是:ECS 并不是一个新概念,它的提出其实是和语言相关的。ECS 概念的诞生起于游戏行业,相关框架基本都是基于 C++ 来开发的。它其实是对 C++ 对象模型的一个反思。ECS 针对组件组合对象,而反对 C++ 固有的基于继承的对象模型。对象模型才是 ECS 的设计核心理念。而离开 C++ 的对象模型,ECS 并不是什么新鲜的东西。

我的这个观点也不新鲜,在 ECS 的 Wikipedia 页上也有类似的说法:

In the original talk at GDC Scott Bilas compares C++ object system and his new Custom component system. This is consistent with a traditional use of this term in general Systems engineering with Common Lisp Object System and Type system as examples. Therefore, the ideas of "Systems" as a first-class element is a personal opinion essay. Overall, ECS is a mixed personal reflection of orthogonal well-established ideas in general Computer science and Programming language theory. For example, components can be seen as a mixin idiom in various programming languages. Alternatively, components are just a small case under the general Delegation (object-oriented programming) approach and Meta-object protocol. I.e. any complete component object system can be expressed with templates and empathy model within The Orlando Treaty vision of Object-oriented programming,

抛开理论不谈,如果要在 Lua 中实践,我们到底可以做点什么呢?

我认为需要有这几个方面:

首先应该对 Lua 加强类型系统。Lua 的动态性天然支持把不同的组件聚合在一起,我们把不同的 Component 放在一张表里组合成 Entity 就足够了。但如果 Component 分的很细的话,用很多的表组合成一个 Entity 对象的额外开销不小。不像 C++ ,结构体聚合的额外开销几乎为零。我们完全可以把不同 Component 的数据直接平坦放在一个 table 中,只要键值不冲突即可。但是我们需要额外的类型信息方便运行时从 Entity 中萃取出 Component 来。另外,如果是 C / Lua 混合设计的话,某些 Component 还应该可以是 userdata 。

从节省空间及方便遍历的角度讲,我们甚至可以把同类的 C Component 聚合在一大块内存中,然后在 Entity 的 table 中只保留一个 lightuserdata 即可。ECS 的 System 最重要的操作就是遍历处理同类 Component ,这样天然就可以分为 C System 和 Lua System 。数据的内聚性很高,可以直接区分开 C data 和 Lua Data 。

然后、就是方便的遍历。ECS 的 System 需要做的就是筛选出它关心的 Entity ,针对其中的 Component 做操作。如果需要筛选结果大大少于全体 Entity 数量,遍历逐个判断就会效率很低。好在在 Lua 中,我们可以非常容易地做出 cache ,只需要遍历筛选一次,在监控新的 Component 的诞生就可以方便的维护遍历用的集合了。

我写了一个初步的版本。打算等到实际使用起来再慢慢完善。

它可以实现成一个纯 Lua 版,但我特地尝试把里面的两个函数编写了 C 的等价版,看起来可以提高不少性能。

这里的 API 中, Entity 全部使用唯一数字 id 标识,而不主张直接引用 entity 的 table 。这也是一般 ECS 框架的通用做法。数字 id 可以提高健壮性,还可以避免对已经销毁的 Entity 错误的引用。如果需要 C / Lua 混合编程的话,在 C 中引用 id 也方便的多。

类型系统是这样设计的:

每个 Component 有一个 16bit 的唯一类型 id ,每个 Entity 是由若干 Component 组合而成,我将它们的类型 id 升序排列成一个字符串,作为整个 Entity 的动态类型。然后 Lua 中实现了一个 cache ,可以从这个类型字符串转换成易用的类型对象。类型对象用来对 Entity 做筛选,从里面分离出 Component 。这个类型字符串的拼接我实现的是一个 C 版本,虽然 Lua 也能实现的出来,但是效率会低很多。

大部分 Component 我主张直接平坦的放在 Entity 对象表中,但若需要用 C 结构来承载,或单独用一个子表,也提供了 Component 类型注册方法,可以在 new component 时定义一个专门的构造函数(以及 delete 时的析构函数)。

这里还提供了遍历包含特定 Component 的 Entity 集合 的迭代器。它由一个弱表实现的 cache 来管理。在第一次遍历时,会创建一个集合,收集 Entity 全体集合中符合要求的部分,把筛选出来的 id 记录下来。一旦遍历集合创建好,它还会跟踪新的 Component 的创建,自动加进来。

这里的迭代器,我同时实现了 Lua 版本和 C 版本。C 版本的性能会高一些,我认为这个 C 版本在遍历相关集合时,性能表现不会差于用 C/C++ 实现的原生容器。

以上是对 Entity 和 Component 的支持。System 相关的方法,我还没有想好可以做点什么。等用到了再完善。

]]>
0
<![CDATA[畅销书的套路 - 读《疯传》有感]]> http://www.udpwork.com/item/16521.html http://www.udpwork.com/item/16521.html#reviews Sun, 03 Dec 2017 20:49:38 +0800 唐巧 http://www.udpwork.com/item/16521.html 最近看完了《疯传》,作者是乔纳·伯杰(Jonah Berger),宾夕法尼亚大学沃顿商学院市场营销学教授。

不得不说,这是一本不错的书,他的观点角度和《引爆点》的作者格拉德威尔不太一样,《引爆点》主要讲的是事件流行的内部因素和外部因素,而《疯传》更加强调内部因素。在书的前言部分,作者甚至直言《引爆点》的「关键人物法则」观点是错误的,他认为好的流行事件,就是应该让普通老百姓都有传播的欲望。哇!作为吃瓜群众,我们最喜欢看这种撕逼大戏上演了!那接下来,让我们来看看《疯传》的观点是什么,到底后面还会怎么喷《引爆点》。

《疯传》接着用六章来介绍如何使传播的内容具备感染力。它把这些因素归纳为:

  • 社交货币(Social Currency)
  • 诱因(Triggers)
  • 情绪(Emotion)
  • 公共性(Public)
  • 实用价值(Practical Value)
  • 故事(Stories)

我把这些因素简单解释一下:

  • 社交货币:你的内容应该易于通过社交关系传播。比如你看了文章想分享到朋友圈,这篇文章就有了「社交货币」。
  • 诱因:你的内容应该有很多机会让大家想起来,进而谈论到。
  • 情绪:用户谈论你的内容的时候,应该能够勾起各种情绪,例如惊喜,惋惜,惊讶等。
  • 公共性:你的内容适合在公共场合谈论。
  • 实用价值:内容有实际的用处。
  • 故事:用讲故事的方式来传播内容。乔布斯在卖 iPod 的时候,不是说它是一个多么牛逼的播放器,而是讲一个「把1000首歌装进口袋」的故事。

我看完本书,除了感觉内容非常对之外,主要有两点不爽:第一点就是完全没有看到任何接力的撕逼内容,这让吃瓜群众情何以堪?另外,你讲这么多因素,我如何能够记得住?

于是我总结了一下,其实一句话就能讲明白,这本书讲的是:

让一个人有很多机会,在公开场合,给别人兴奋地讲有意义的故事。

我给解释一下,这句话中:

  • 「有很多机会」是指 Triggers
  • 「在公开场合」是指 Public
  • 「给别人」是指 Social Currency
  • 「兴奋」是指 Emotion
  • 「有意义」是指 Practical Value
  • 「故事」是指 Stories

好了,六点都记住了吧?那么你就一定能设计出一个疯传的事件出来么?其实也不是,但在设计营销方案的时候,至少可以参考一下。

其实这本书设计得这么厚,但是讲这么点东西,也是为了照顾大家的 Emotion,如果我让你花 48 块钱就看这么点东西,你会理性看待这件事情吗?大部分人都不会。所以现在的书才搞得这么厚,浪费大家时间。出版社也学得特别精明,如果你仔细观察就会发现,一些字数特别少的书,出版社会通过排版,让这些书显得很厚,于是你才愿意花钱买。

一切套路都是为了符合人性。

]]>
最近看完了《疯传》,作者是乔纳·伯杰(Jonah Berger),宾夕法尼亚大学沃顿商学院市场营销学教授。

不得不说,这是一本不错的书,他的观点角度和《引爆点》的作者格拉德威尔不太一样,《引爆点》主要讲的是事件流行的内部因素和外部因素,而《疯传》更加强调内部因素。在书的前言部分,作者甚至直言《引爆点》的「关键人物法则」观点是错误的,他认为好的流行事件,就是应该让普通老百姓都有传播的欲望。哇!作为吃瓜群众,我们最喜欢看这种撕逼大戏上演了!那接下来,让我们来看看《疯传》的观点是什么,到底后面还会怎么喷《引爆点》。

《疯传》接着用六章来介绍如何使传播的内容具备感染力。它把这些因素归纳为:

  • 社交货币(Social Currency)
  • 诱因(Triggers)
  • 情绪(Emotion)
  • 公共性(Public)
  • 实用价值(Practical Value)
  • 故事(Stories)

我把这些因素简单解释一下:

  • 社交货币:你的内容应该易于通过社交关系传播。比如你看了文章想分享到朋友圈,这篇文章就有了「社交货币」。
  • 诱因:你的内容应该有很多机会让大家想起来,进而谈论到。
  • 情绪:用户谈论你的内容的时候,应该能够勾起各种情绪,例如惊喜,惋惜,惊讶等。
  • 公共性:你的内容适合在公共场合谈论。
  • 实用价值:内容有实际的用处。
  • 故事:用讲故事的方式来传播内容。乔布斯在卖 iPod 的时候,不是说它是一个多么牛逼的播放器,而是讲一个「把1000首歌装进口袋」的故事。

我看完本书,除了感觉内容非常对之外,主要有两点不爽:第一点就是完全没有看到任何接力的撕逼内容,这让吃瓜群众情何以堪?另外,你讲这么多因素,我如何能够记得住?

于是我总结了一下,其实一句话就能讲明白,这本书讲的是:

让一个人有很多机会,在公开场合,给别人兴奋地讲有意义的故事。

我给解释一下,这句话中:

  • 「有很多机会」是指 Triggers
  • 「在公开场合」是指 Public
  • 「给别人」是指 Social Currency
  • 「兴奋」是指 Emotion
  • 「有意义」是指 Practical Value
  • 「故事」是指 Stories

好了,六点都记住了吧?那么你就一定能设计出一个疯传的事件出来么?其实也不是,但在设计营销方案的时候,至少可以参考一下。

其实这本书设计得这么厚,但是讲这么点东西,也是为了照顾大家的 Emotion,如果我让你花 48 块钱就看这么点东西,你会理性看待这件事情吗?大部分人都不会。所以现在的书才搞得这么厚,浪费大家时间。出版社也学得特别精明,如果你仔细观察就会发现,一些字数特别少的书,出版社会通过排版,让这些书显得很厚,于是你才愿意花钱买。

一切套路都是为了符合人性。

]]>
0
<![CDATA[协程并发模型及使用感受]]> http://www.udpwork.com/item/16520.html http://www.udpwork.com/item/16520.html#reviews Sun, 03 Dec 2017 00:00:00 +0800 Kevin Lynx http://www.udpwork.com/item/16520.html 协程可以简单理解为更轻量的线程,但有很多显著的不同:

  • 不是OS级别的调度单元,通常是编程语言或库实现
  • 可能需要应用层自己切换
  • 由于切换点是可控制的,所以对于CPU资源是非抢占式的
  • 通常用于有大量阻塞操作的应用,例如大量IO

协程与actor模式的实现有一定关系。由于协程本身是应用级的并发调度单元,所以理论上可以大量创建。在协程之上做队列及通信包装,即可得到一个actor框架,例如python-actor

最近1年做了一个python项目。这个项目中利用gevent wsgi对外提供HTTP API,使用gevent greelet来支撑上层应用的开发。当可以使用协程后,编程模型会得到一定简化,例如相对于传统线程池+队列的并发实现,协程可以抛弃这个模型,直接一个协程对应于一个并发任务,例如网络服务中一个协程对应一个socket fd。

但是python毕竟是单核的,这个项目内部虽然有大量IO请求,但随着业务增长,CPU很快就到达了瓶颈。后来改造为多进程结构,将业务单元分散到各个worker进程上。

python gevent中的协议切换是自动的,在遇到阻塞操作后gevent会自动挂起当前协程,并切换到其他需要激活的协程。阻塞操作完成,对应的协程就会处于待激活状态。

在这个项目过程中,我发现协程也存在很多陷阱。

协程的陷阱

  • 死循环

普通的死循环很难遇到,间接的死循环一旦发生,就会一直占用CPU资源,导致其他协程被饿死。

  • 留意非协程化的阻塞接口

gevent中通常会将python内置的各种阻塞操作green化,也就是我这里说的协程化,例如socket IO接口、time.sleep、各种锁等待。如果在系统中引入一个不能被协程化的库,例如MySQL-python。当协程被阻塞在这种库的接口时,协程不能被切走,而是等到python内线程的抢占式切换,实际上对于gevent的协程调度其总计可用的CPU就不是100%了。在压力较大的情况下,协程就可能出现延迟调度。意思是在协程阻塞操作完成后,在负载较小的情况下,该协程会立即得到切换。

这里有一个小技巧,可以写一个time.sleep延时的协程,检查真实的延时情况和time.sleep的延时参数相差多少,就可以衡量整个系统中协程切换的延时情况。

  • 注意不同角色协程的CPU资源分配

这个问题本质上类似于在基于线程的应用中,需要为不同角色的线程设定不同的优先级。在多核程序中由于总的CPU资源比较多,所以一般也不会遇到需要分配不同优先级的情况。但在基于协程的单核程序中,由于单核CPU资源很快就会被压榨到80-90%,所以就需要关注不同角色协程的优先级。

例如,系统中有用于服务HTTP API的协程集,有用于做耗时任务的协程集。耗时任务正常情况下可能需要分钟级,所以做任务的协程就算慢几秒也没什么关系。但是对外提供API的协程,本身API时延就在毫秒到秒级,如果晚几秒到几十秒,对上游系统或者用于就会造成不良的影响,表现为服务质量差。

但是通常协程库是没有设定优先级的功能的。所以这个时候就要从应用层解决。例如前面的耗时任务例子,一般情况下,为了编程简单,我们会为每一个任务分配一个协程去做。由于所有协程优先级相同,大家被切换的机会是均等的,那么当任务增多后,API相关的协程获得的切换机会更少,影响服务质量。所以这个时候,就会创建一个用于完成耗时任务的协程池,以限制耗时任务占用的总协程数量。这就又回到了基于线程的并发模型中。

  • 留意协程切换

在gevent这种协程切换不需要程序员显示操作的协程库中,程序员会慢慢忘掉自己是在协程环境下编程。前面的例子中,我们创建了一个协程池去限制耗时任务可用的协程数量。在实际项目中可能会对调度做一些包装,让应用层只关注自己的业务代码。那么,在业务代码中,对于一些需要重试的失败操作,我sleep一段较长的时间也很合情理吧。这个时候如果由于外部依赖服务异常,而导致部分业务协程失败,处于sleep中。这个时候,协程池内有限的协程都被挂起了。导致很多本来可以获得CPU资源的任务无法得到消费,导致整个系统的吞吐量下降。

总结

协程会在低CPU系统中获得不少易于编程的好处,但是当系统总CPU上去后就需要付出等价于甚至大于多线程编程中的代价。

原文地址:http://codemacro.com/2017/12/03/coroutine/
written byKevin Lynx posted athttp://codemacro.com

]]>
协程可以简单理解为更轻量的线程,但有很多显著的不同:

  • 不是OS级别的调度单元,通常是编程语言或库实现
  • 可能需要应用层自己切换
  • 由于切换点是可控制的,所以对于CPU资源是非抢占式的
  • 通常用于有大量阻塞操作的应用,例如大量IO

协程与actor模式的实现有一定关系。由于协程本身是应用级的并发调度单元,所以理论上可以大量创建。在协程之上做队列及通信包装,即可得到一个actor框架,例如python-actor

最近1年做了一个python项目。这个项目中利用gevent wsgi对外提供HTTP API,使用gevent greelet来支撑上层应用的开发。当可以使用协程后,编程模型会得到一定简化,例如相对于传统线程池+队列的并发实现,协程可以抛弃这个模型,直接一个协程对应于一个并发任务,例如网络服务中一个协程对应一个socket fd。

但是python毕竟是单核的,这个项目内部虽然有大量IO请求,但随着业务增长,CPU很快就到达了瓶颈。后来改造为多进程结构,将业务单元分散到各个worker进程上。

python gevent中的协议切换是自动的,在遇到阻塞操作后gevent会自动挂起当前协程,并切换到其他需要激活的协程。阻塞操作完成,对应的协程就会处于待激活状态。

在这个项目过程中,我发现协程也存在很多陷阱。

协程的陷阱

  • 死循环

普通的死循环很难遇到,间接的死循环一旦发生,就会一直占用CPU资源,导致其他协程被饿死。

  • 留意非协程化的阻塞接口

gevent中通常会将python内置的各种阻塞操作green化,也就是我这里说的协程化,例如socket IO接口、time.sleep、各种锁等待。如果在系统中引入一个不能被协程化的库,例如MySQL-python。当协程被阻塞在这种库的接口时,协程不能被切走,而是等到python内线程的抢占式切换,实际上对于gevent的协程调度其总计可用的CPU就不是100%了。在压力较大的情况下,协程就可能出现延迟调度。意思是在协程阻塞操作完成后,在负载较小的情况下,该协程会立即得到切换。

这里有一个小技巧,可以写一个time.sleep延时的协程,检查真实的延时情况和time.sleep的延时参数相差多少,就可以衡量整个系统中协程切换的延时情况。

  • 注意不同角色协程的CPU资源分配

这个问题本质上类似于在基于线程的应用中,需要为不同角色的线程设定不同的优先级。在多核程序中由于总的CPU资源比较多,所以一般也不会遇到需要分配不同优先级的情况。但在基于协程的单核程序中,由于单核CPU资源很快就会被压榨到80-90%,所以就需要关注不同角色协程的优先级。

例如,系统中有用于服务HTTP API的协程集,有用于做耗时任务的协程集。耗时任务正常情况下可能需要分钟级,所以做任务的协程就算慢几秒也没什么关系。但是对外提供API的协程,本身API时延就在毫秒到秒级,如果晚几秒到几十秒,对上游系统或者用于就会造成不良的影响,表现为服务质量差。

但是通常协程库是没有设定优先级的功能的。所以这个时候就要从应用层解决。例如前面的耗时任务例子,一般情况下,为了编程简单,我们会为每一个任务分配一个协程去做。由于所有协程优先级相同,大家被切换的机会是均等的,那么当任务增多后,API相关的协程获得的切换机会更少,影响服务质量。所以这个时候,就会创建一个用于完成耗时任务的协程池,以限制耗时任务占用的总协程数量。这就又回到了基于线程的并发模型中。

  • 留意协程切换

在gevent这种协程切换不需要程序员显示操作的协程库中,程序员会慢慢忘掉自己是在协程环境下编程。前面的例子中,我们创建了一个协程池去限制耗时任务可用的协程数量。在实际项目中可能会对调度做一些包装,让应用层只关注自己的业务代码。那么,在业务代码中,对于一些需要重试的失败操作,我sleep一段较长的时间也很合情理吧。这个时候如果由于外部依赖服务异常,而导致部分业务协程失败,处于sleep中。这个时候,协程池内有限的协程都被挂起了。导致很多本来可以获得CPU资源的任务无法得到消费,导致整个系统的吞吐量下降。

总结

协程会在低CPU系统中获得不少易于编程的好处,但是当系统总CPU上去后就需要付出等价于甚至大于多线程编程中的代价。

原文地址:http://codemacro.com/2017/12/03/coroutine/
written byKevin Lynx posted athttp://codemacro.com

]]>
0
<![CDATA[2017年11月]]> http://www.udpwork.com/item/16519.html http://www.udpwork.com/item/16519.html#reviews Thu, 30 Nov 2017 10:46:53 +0800 崔凯 http://www.udpwork.com/item/16519.html 新技能 get !!!
学会梳辫子了!!!
这个月很多个早上,都是自己带孩子洗漱。
很多个早上,没给女儿洗脸。
直到老师笑哒哒的问,今天洗脸了吗?(牙膏在嘴角呢)
这才痛改前非:
“闺女,把脸洗洗。”

辫子就只能我来梳了。
开始只会扎马尾辫,然而头发帘太长,各种遮挡。
自我迭代,进化成左右各一个的羊角辫。然而分印不太好弄,在后面散了一天。
终于,起早了一次。扎成了 “左、右、后” 三个辫子的终极版。
嗯,满意!!

第一次接孩子。
女儿很开心,跟老师各种炫耀,今天爸爸来接我。
请了半天假,早早的赶过去。
满满的全是车,都快停到家门口了。
穿过层层叠叠的家长,看到别人家的小女孩,三五分钟就冒头张望一下,我爸爸怎么还没来。
嗯,幸亏来早了!!

暖气不太正常,停了两个晚上。
时不时的给她盖盖被子。
然后被踢开,再盖上。
睡醒,讲一讲,你昨晚又横着睡,踹了我多少脚,为什么爸爸睡在床的另一头。
就嘿嘿傻乐一阵!!

我睡不好,视力越来越差,不能总玩手机。
只有听着别人叨逼叨,才睡的着。
「晓说」之外的日子,开始听「吴晓波频道」

醒的很早。
天不亮就精神了。
倒是多了很多时间。

带女儿去很远的体育馆看跆拳道的实战。
比表演赛精彩许多。
她也终于点头,爸爸,我想考到黑带。
嗯,续课!!

带她去了趟龙德广场。
地下停车。再想开出来的时候,严重拥堵。
得,先折回商场,吃完晚饭。再出来,依然水泄不通。
从地下车库开上路,花了两个小时。
再也不敢停地下。
然后,下一周,就被贴条了。
违反机动车停放规定且驾驶人不在现场。
偏偏那个条还被吹走了。
只留下了难看的胶水,我一直以为那是一坨鸟屎。
直到短信通知我交罚款……
嗯,后边玻璃还有一坨鸟屎!!

网上交罚款的系统是真难用。
每两小时可以试一次,每次都失败。
老司机说要凌晨试。我 5 点起床,也没成功。
好烦躁。

昨天女儿跟我讲,爸爸我以后挣很多的钱。
我问她,多少钱啊?
她说,两百。给你交罚款!

想到这个,裹了裹冬天里最厚的羽绒服。
嗯,没那么冷了。

相关日志:

]]>
新技能 get !!!
学会梳辫子了!!!
这个月很多个早上,都是自己带孩子洗漱。
很多个早上,没给女儿洗脸。
直到老师笑哒哒的问,今天洗脸了吗?(牙膏在嘴角呢)
这才痛改前非:
“闺女,把脸洗洗。”

辫子就只能我来梳了。
开始只会扎马尾辫,然而头发帘太长,各种遮挡。
自我迭代,进化成左右各一个的羊角辫。然而分印不太好弄,在后面散了一天。
终于,起早了一次。扎成了 “左、右、后” 三个辫子的终极版。
嗯,满意!!

第一次接孩子。
女儿很开心,跟老师各种炫耀,今天爸爸来接我。
请了半天假,早早的赶过去。
满满的全是车,都快停到家门口了。
穿过层层叠叠的家长,看到别人家的小女孩,三五分钟就冒头张望一下,我爸爸怎么还没来。
嗯,幸亏来早了!!

暖气不太正常,停了两个晚上。
时不时的给她盖盖被子。
然后被踢开,再盖上。
睡醒,讲一讲,你昨晚又横着睡,踹了我多少脚,为什么爸爸睡在床的另一头。
就嘿嘿傻乐一阵!!

我睡不好,视力越来越差,不能总玩手机。
只有听着别人叨逼叨,才睡的着。
「晓说」之外的日子,开始听「吴晓波频道」

醒的很早。
天不亮就精神了。
倒是多了很多时间。

带女儿去很远的体育馆看跆拳道的实战。
比表演赛精彩许多。
她也终于点头,爸爸,我想考到黑带。
嗯,续课!!

带她去了趟龙德广场。
地下停车。再想开出来的时候,严重拥堵。
得,先折回商场,吃完晚饭。再出来,依然水泄不通。
从地下车库开上路,花了两个小时。
再也不敢停地下。
然后,下一周,就被贴条了。
违反机动车停放规定且驾驶人不在现场。
偏偏那个条还被吹走了。
只留下了难看的胶水,我一直以为那是一坨鸟屎。
直到短信通知我交罚款……
嗯,后边玻璃还有一坨鸟屎!!

网上交罚款的系统是真难用。
每两小时可以试一次,每次都失败。
老司机说要凌晨试。我 5 点起床,也没成功。
好烦躁。

昨天女儿跟我讲,爸爸我以后挣很多的钱。
我问她,多少钱啊?
她说,两百。给你交罚款!

想到这个,裹了裹冬天里最厚的羽绒服。
嗯,没那么冷了。

相关日志:

]]>
0
<![CDATA[在同一个系统里使用多个版本的软件]]> http://www.udpwork.com/item/16518.html http://www.udpwork.com/item/16518.html#reviews Wed, 29 Nov 2017 18:46:07 +0800 老王 http://www.udpwork.com/item/16518.html 如果你有几房姨太太的话,那么想让她们和平共处,多半是痴人说梦。对程序员而言,虽然他们不会有娶几个老婆的好运气,但是很可能会遇到在同一个系统里使用多个版本的软件的情况,一旦处理不好,同样会焦头烂额。

下面通过一个例子来说明如何解决多版本共存的问题:PHP 如果使用带有PGO功能的 gcc 编译的话,那么可以在不修改一行业务代码的情况下,获得10%左右的性能提升。不过这要求 gcc 的版本至少要 4.5,而我的 gcc 版本是 4.4,因为 gcc 是一个基础应用,所以我不敢贸然直接升级版本。于是乎解决方案就是:我需要在不影响旧版本的前提下再装一个新版本,不过自己手动编译的话无疑恨麻烦,好在有 SCL,通过它,我们可以实现在同一个系统里使用多个版本的软件:

Software Collections give you the power to build, install, and use multiple versions of software on the same system, without affecting system-wide installed packages.

以 CentOS 为例,看看如何通过 SCL 维护多版本的 gcc:

shell> yum install centos-release-scl
shell> yum install devtoolset-7

shell> gcc -v
gcc version 4.4.7 ***

shell> scl enable devtoolset-7 bash

shell> gcc -v
gcc version 7.2.1 ***

shell> exit

shell> gcc -v
gcc version 4.4.7 ***

注意:scl 激活 devtoolset前后新旧 gcc 版本的变化。

最后,详细的版本库参考官网

 

]]>
如果你有几房姨太太的话,那么想让她们和平共处,多半是痴人说梦。对程序员而言,虽然他们不会有娶几个老婆的好运气,但是很可能会遇到在同一个系统里使用多个版本的软件的情况,一旦处理不好,同样会焦头烂额。

下面通过一个例子来说明如何解决多版本共存的问题:PHP 如果使用带有PGO功能的 gcc 编译的话,那么可以在不修改一行业务代码的情况下,获得10%左右的性能提升。不过这要求 gcc 的版本至少要 4.5,而我的 gcc 版本是 4.4,因为 gcc 是一个基础应用,所以我不敢贸然直接升级版本。于是乎解决方案就是:我需要在不影响旧版本的前提下再装一个新版本,不过自己手动编译的话无疑恨麻烦,好在有 SCL,通过它,我们可以实现在同一个系统里使用多个版本的软件:

Software Collections give you the power to build, install, and use multiple versions of software on the same system, without affecting system-wide installed packages.

以 CentOS 为例,看看如何通过 SCL 维护多版本的 gcc:

shell> yum install centos-release-scl
shell> yum install devtoolset-7

shell> gcc -v
gcc version 4.4.7 ***

shell> scl enable devtoolset-7 bash

shell> gcc -v
gcc version 7.2.1 ***

shell> exit

shell> gcc -v
gcc version 4.4.7 ***

注意:scl 激活 devtoolset前后新旧 gcc 版本的变化。

最后,详细的版本库参考官网

 

]]>
0
<![CDATA[Go语言中实现基于 event-loop 网络处理]]> http://www.udpwork.com/item/16517.html http://www.udpwork.com/item/16517.html#reviews Wed, 29 Nov 2017 17:33:48 +0800 鸟窝 http://www.udpwork.com/item/16517.html 我们知道, Go语言为并发编程提供了简洁的编程方式, 你可以以"同步"的编程风格来并发执行代码, 比如使用go关键字新开一个goroutine。 对于网络编程,Go标准库和运行时内部采用epoll/kqueue/IoCompletionPort来实现基于event-loop的网络异步处理,但是通过netpoll的方式对外提供同步的访问。具体代码可以参考runtime/netpollnetinternal/poll

Package poll supports non-blocking I/O on file descriptors with polling.
This supports I/O operations that block only a goroutine, not a thread.
This is used by the net and os packages.
It uses a poller built into the runtime, with support from the
runtime scheduler.

当然,我们平常不会设计到这些封装的细节,正常使用net包就很方便的开发网络程序了, 但是,如果我们想自己实现基于epoll的event-loop网络程序呢?

基于epoll的简单程序

man epoll可以查看epoll的相关介绍。下面这个例子来自tevino, 采用edge-triggered方式处理事件。

它采用syscall.Socket、syscall.SetNonblock、syscall.Bind、syscall.Listen系统调用来监听端口,然后采用syscall.EpollCreate1、syscall.EpollCtl、syscall.EpollWait来关联这个监听的file descriptor, 一旦有新的连接的事件过来,使用syscall.Accept接收连接请求,并对这个连接file descriptor调用syscall.EpollCtl监听数据事件。一旦连接有数据ready, 调用syscall.Read读数据,调用syscall.Write写数据。

来自https://gist.github.com/tevino/3a4f4ec4ea9d0ca66d4f
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
package mainimport (	"fmt"	"net"	"os"	"syscall")const (	EPOLLET        = 1 << 31	MaxEpollEvents = 32)func echo(fd int) {	defer syscall.Close(fd)	var buf [32 * 1024]byte	for {		nbytes, e := syscall.Read(fd, buf[:])		if nbytes > 0 {			fmt.Printf(">>> %s", buf)			syscall.Write(fd, buf[:nbytes])			fmt.Printf("<<< %s", buf)		}		if e != nil {			break		}	}}func main() {	var event syscall.EpollEvent	var events [MaxEpollEvents]syscall.EpollEvent	fd, err := syscall.Socket(syscall.AF_INET, syscall.O_NONBLOCK|syscall.SOCK_STREAM, 0)	if err != nil {		fmt.Println(err)		os.Exit(1)	}	defer syscall.Close(fd)	if err = syscall.SetNonblock(fd, true); err != nil {		fmt.Println("setnonblock1: ", err)		os.Exit(1)	}	addr := syscall.SockaddrInet4{Port: 2000}	copy(addr.Addr[:], net.ParseIP("0.0.0.0").To4())	syscall.Bind(fd, &addr)	syscall.Listen(fd, 10)	epfd, e := syscall.EpollCreate1(0)	if e != nil {		fmt.Println("epoll_create1: ", e)		os.Exit(1)	}	defer syscall.Close(epfd)	event.Events = syscall.EPOLLIN	event.Fd = int32(fd)	if e = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, fd, &event); e != nil {		fmt.Println("epoll_ctl: ", e)		os.Exit(1)	}	for {		nevents, e := syscall.EpollWait(epfd, events[:], -1)		if e != nil {			fmt.Println("epoll_wait: ", e)			break		}		for ev := 0; ev < nevents; ev++ {			if int(events[ev].Fd) == fd {				connFd, _, err := syscall.Accept(fd)				if err != nil {					fmt.Println("accept: ", err)					continue				}				syscall.SetNonblock(fd, true)				event.Events = syscall.EPOLLIN | EPOLLET				event.Fd = int32(connFd)				if err := syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, connFd, &event); err != nil {					fmt.Print("epoll_ctl: ", connFd, err)					os.Exit(1)				}			} else {				go echo(int(events[ev].Fd))			}		}	}}

上面的基于epoll只是一个简单的event-loop处理原型,而且在有些平台下(MAC OS)也不能执行,事件的处理也很粗糙,如果你想实现一个完整的event-loop的网络程序, 可以参考下节的库。

evio

evio是一个性能很高的event-loop网络库,代码简单,功能强大。它直接使用epoll和kqueue系统调用,除了Go标准net库提供了另外一种思路, 类似libuvlibevent

这个库实现redis和haproxy等同的包处理机制,但并不想完全替代标准的net包。对于一个需要长时间运行的请求(大于1毫秒), 比如数据库访问、身份验证等,建议还是使用Go net/http库。

你可能知道, 由很多基于event-loop的程序, 比如Nginx、Haproxy、redis、memcached等,性能都非常不错,而且它们都是单线程运行的,非常快。

这个库还有一个好处, 你可以在一个event-loop中处理多个network binding。

一个简单的例子:

1234567891011121314
package mainimport "github.com/tidwall/evio"func main() {	var events evio.Events	events.Data = func(id int, in []byte) (out []byte, action evio.Action) {		out = in		return	}	if err := evio.Serve(events, "tcp://localhost:5000", "tcp://192.168.0.10:5001", "tcp://192.168.0.10:5002","unix://socket"); err != nil {		panic(err.Error())	}}

作者对性能做了对比,性能非常不错。

简单的echo例子
http对比
pipeline 为1
pipeline 为8

]]>
我们知道, Go语言为并发编程提供了简洁的编程方式, 你可以以"同步"的编程风格来并发执行代码, 比如使用go关键字新开一个goroutine。 对于网络编程,Go标准库和运行时内部采用epoll/kqueue/IoCompletionPort来实现基于event-loop的网络异步处理,但是通过netpoll的方式对外提供同步的访问。具体代码可以参考runtime/netpollnetinternal/poll

Package poll supports non-blocking I/O on file descriptors with polling.
This supports I/O operations that block only a goroutine, not a thread.
This is used by the net and os packages.
It uses a poller built into the runtime, with support from the
runtime scheduler.

当然,我们平常不会设计到这些封装的细节,正常使用net包就很方便的开发网络程序了, 但是,如果我们想自己实现基于epoll的event-loop网络程序呢?

基于epoll的简单程序

man epoll可以查看epoll的相关介绍。下面这个例子来自tevino, 采用edge-triggered方式处理事件。

它采用syscall.Socket、syscall.SetNonblock、syscall.Bind、syscall.Listen系统调用来监听端口,然后采用syscall.EpollCreate1、syscall.EpollCtl、syscall.EpollWait来关联这个监听的file descriptor, 一旦有新的连接的事件过来,使用syscall.Accept接收连接请求,并对这个连接file descriptor调用syscall.EpollCtl监听数据事件。一旦连接有数据ready, 调用syscall.Read读数据,调用syscall.Write写数据。

来自https://gist.github.com/tevino/3a4f4ec4ea9d0ca66d4f
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394
package mainimport (	"fmt"	"net"	"os"	"syscall")const (	EPOLLET        = 1 << 31	MaxEpollEvents = 32)func echo(fd int) {	defer syscall.Close(fd)	var buf [32 * 1024]byte	for {		nbytes, e := syscall.Read(fd, buf[:])		if nbytes > 0 {			fmt.Printf(">>> %s", buf)			syscall.Write(fd, buf[:nbytes])			fmt.Printf("<<< %s", buf)		}		if e != nil {			break		}	}}func main() {	var event syscall.EpollEvent	var events [MaxEpollEvents]syscall.EpollEvent	fd, err := syscall.Socket(syscall.AF_INET, syscall.O_NONBLOCK|syscall.SOCK_STREAM, 0)	if err != nil {		fmt.Println(err)		os.Exit(1)	}	defer syscall.Close(fd)	if err = syscall.SetNonblock(fd, true); err != nil {		fmt.Println("setnonblock1: ", err)		os.Exit(1)	}	addr := syscall.SockaddrInet4{Port: 2000}	copy(addr.Addr[:], net.ParseIP("0.0.0.0").To4())	syscall.Bind(fd, &addr)	syscall.Listen(fd, 10)	epfd, e := syscall.EpollCreate1(0)	if e != nil {		fmt.Println("epoll_create1: ", e)		os.Exit(1)	}	defer syscall.Close(epfd)	event.Events = syscall.EPOLLIN	event.Fd = int32(fd)	if e = syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, fd, &event); e != nil {		fmt.Println("epoll_ctl: ", e)		os.Exit(1)	}	for {		nevents, e := syscall.EpollWait(epfd, events[:], -1)		if e != nil {			fmt.Println("epoll_wait: ", e)			break		}		for ev := 0; ev < nevents; ev++ {			if int(events[ev].Fd) == fd {				connFd, _, err := syscall.Accept(fd)				if err != nil {					fmt.Println("accept: ", err)					continue				}				syscall.SetNonblock(fd, true)				event.Events = syscall.EPOLLIN | EPOLLET				event.Fd = int32(connFd)				if err := syscall.EpollCtl(epfd, syscall.EPOLL_CTL_ADD, connFd, &event); err != nil {					fmt.Print("epoll_ctl: ", connFd, err)					os.Exit(1)				}			} else {				go echo(int(events[ev].Fd))			}		}	}}

上面的基于epoll只是一个简单的event-loop处理原型,而且在有些平台下(MAC OS)也不能执行,事件的处理也很粗糙,如果你想实现一个完整的event-loop的网络程序, 可以参考下节的库。

evio

evio是一个性能很高的event-loop网络库,代码简单,功能强大。它直接使用epoll和kqueue系统调用,除了Go标准net库提供了另外一种思路, 类似libuvlibevent

这个库实现redis和haproxy等同的包处理机制,但并不想完全替代标准的net包。对于一个需要长时间运行的请求(大于1毫秒), 比如数据库访问、身份验证等,建议还是使用Go net/http库。

你可能知道, 由很多基于event-loop的程序, 比如Nginx、Haproxy、redis、memcached等,性能都非常不错,而且它们都是单线程运行的,非常快。

这个库还有一个好处, 你可以在一个event-loop中处理多个network binding。

一个简单的例子:

1234567891011121314
package mainimport "github.com/tidwall/evio"func main() {	var events evio.Events	events.Data = func(id int, in []byte) (out []byte, action evio.Action) {		out = in		return	}	if err := evio.Serve(events, "tcp://localhost:5000", "tcp://192.168.0.10:5001", "tcp://192.168.0.10:5002","unix://socket"); err != nil {		panic(err.Error())	}}

作者对性能做了对比,性能非常不错。

简单的echo例子
http对比
pipeline 为1
pipeline 为8

]]>
0
<![CDATA[[译]Go TCP Socket的实现]]> http://www.udpwork.com/item/16515.html http://www.udpwork.com/item/16515.html#reviews Wed, 29 Nov 2017 12:01:13 +0800 鸟窝 http://www.udpwork.com/item/16515.html 原文:TCP Socket Implementation On Golangby Gian Giovani.

译者注 : 作者并没有从源代码级别去分析Go socket的实现,而是利用strace工具来反推Go Socket的行为。这一方法可以扩展我们分析代码的手段。
源代码级别的分析可以看其实现:net poll,以及一些分析文章:The Go netpoller,The Go netpoller and timeout

Go语言是我写web程序的首选, 它隐藏了很多细节,但仍然不失灵活性。最新我用strace工具分析了一下一个http程序,纯属手贱但还是发现了一些有趣的事情。

下面是strace的结果:

1234567891011121314151617181920
% time     seconds  usecs/call     calls    errors syscall------ ----------- ----------- --------- --------- ---------------- 91.24    0.397615         336      1185        29 futex  4.13    0.018009           3      7115           clock_gettime  2.92    0.012735          19       654           epoll_wait  1.31    0.005701           6       911           write  0.20    0.000878           3       335           epoll_ctl  0.12    0.000525           1       915       457 read  0.02    0.000106           2        59           select  0.01    0.000059           0       170           close  0.01    0.000053           0       791           setsockopt  0.01    0.000035           0       158           getpeername  0.01    0.000034           0       170           socket  0.01    0.000029           0       160           getsockname  0.01    0.000026           0       159           getsockopt  0.00    0.000000           0         7           sched_yield  0.00    0.000000           0       166       166 connect  0.00    0.000000           0         3         1 accept4------ ----------- ----------- --------- --------- ----------------100.00    0.435805                 12958       653 total

在这个剖析结果中有很多有趣的东东,但本文中要特别指出的是read的错误数和futex调用的错误数。

一开始我没有深思futex的调用, 大部分情况它无非是一个唤醒调用(wake call)。既然这个程序会处理每秒几百个请求,它应该包含很多go routine。另一方面,它使用了channel,这也会导致很多block情况,所以有很多futex调用也很正常。 不过后来我发现这个数也包含来自其它的逻辑,后面再表。

Why you no read

有谁喜欢错误(error)?短短一分钟就有几百次的错误,太糟糕了, 这是我看到这个剖析结果后最初的印象。那么read call又是什么东东?

123
read(36, "GET /xxx/v3?q=xx%20ch&d"..., 4096) = 520...read(36, 0xc422aa4291, 1)               = -1 EAGAIN (Resource temporarily unavailable)

每次read调用同一个文件描述符,总是(可能)伴随着一个EAGAINerror。我记得这个错误,当文件描述符还没有准备(ready)某个操作的时候就会返回这个错,上面的例子中操作是read。问题是为什么Go会这样做呢?

我猜想这可能是epoll_wait的一个bug, 它为每一个文件描述符提供了错误的ready事件?每一个文件描述符? 看起来read事件是错误事件的两倍,为什么是两倍?

老实说,我的epoll知识很了了,程序只是一个简单的处理事件的socket handler(类似)。没有多线程,没有同步,非常简单。

通过Google我找到了一篇极棒的文章分析评论epoll,由Marek所写,。

这篇文章重要的摘要就是:在多线程中使用epoll, 不必要的唤醒(wake up)通常是不可避免的,因为我们想通知每个等待事件的worker。

这也正好解释了我们的futex 唤醒数。还是让我们看一个简化版本来好好理解怎么在基于事件的socket处理程序中使用epoll吧:

  1. Bindsocket listener到file descriptor, 我们称之为s_fd
  2. 使用epoll_create创建epoll file descriptor, 我们称之为e_fd
  3. 通过epol_ctlbinds_fd到e_fd, 处理特殊的事件(通常EPOLLIN|EPOLLOUT)
  4. 创建一个无限循环 (event loop), 它会在每次循环中调用epoll_wait得到已经ready连接
  5. 处理ready的连接, 在多worker实现中会通知每一个worker

Using strace I found that golang using edge triggered epoll
使用strace我发现 golang使用edge triggered epoll:

1
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2490298448, u64=140490870550608}}) = 0

这意味着下面的过程应该是go socket的实现:

1、Kernel : 收到一个新连接.
2、Kernel : 通知等待的线程 threads A 和 B. 由于level-triggered 通知的"惊群"(“thundering herd”)行为,kernel必须唤醒这两个线程.
3、Thread A : 完成 epoll_wait().
4、Thread B : 完成 epoll_wait().
5、Thread A : 执行 accept(), 成功.
6、Thread B : 执行 accept(), 失败, EAGAIN错误.

现在我有八成把握就是这个case,不过还是让我们用一个简单的程序来分析。

12345678910111213
package mainimport "net/http"func main() {	http.HandleFunc("/", handler)	http.HandleFunc("/test", handler)	http.ListenAndServe(":8080", nil)}func handler(w http.ResponseWriter, r *http.Request) {}

一个简单的请求后的strace结果:

12345678
epoll_wait(4, [{EPOLLIN|EPOLLOUT, {u32=2186919600, u64=140542106779312}}], 128, -1) = 1futex(0x7c1bd8, FUTEX_WAKE, 1)          = 1futex(0x7c1b10, FUTEX_WAKE, 1)          = 1read(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 4096) = 348futex(0xc420060110, FUTEX_WAKE, 1)      = 1write(5, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 116) = 116futex(0xc420060110, FUTEX_WAKE, 1)      = 1read(5, 0xc4200f6000, 4096)             = -1 EAGAIN (Resource temporarily unavailable)

看到epoll_wait有两个futex调用,我认为是worker执行以及一次 error read。

如果GOMAXPROCS设置为1,在单worker情况下:

12345678910111213
epoll_wait(4,[{EPOLLIN, {u32=1969377136, u64=140245536493424}}], 128, -1) = 1futex(0x7c1bd8, FUTEX_WAKE, 1)          = 1accept4(3, {sa_family=AF_INET6, sin6_port=htons(54400), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1969376752, u64=140245536493040}}) = 0getsockname(6, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [180], 4) = 0setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0accept4(3, 0xc42004db78, 0xc42004db6c, SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)read(6, "GET /test?kjhkjhkjh HTTP/1.1\r\nHo"..., 4096) = 92write(6, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 139) = 139read(6, "", 4096)

当使用1个worker,epoll_wait之后只有一次futex唤醒,并没有error read。然而我发现并不总是这样, 有时候我依然可以得到read error和两次futex 唤醒。

And then what to do?

在Marek的文章中他谈到Linux 4.5之后可以使用EPOLLEXCLUSIVE。我的Linux版本是4.8,为什么问题还是出现?或许Go并没有使用这个标志,我希望将来的版本可以使用这个标志。

从中我学到了很多知识,希望你也是。

[0]https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
[1]https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/
[2]https://gist.github.com/wejick/2cef1f8799361318a62a59f6801eade8

]]>
原文:TCP Socket Implementation On Golangby Gian Giovani.

译者注 : 作者并没有从源代码级别去分析Go socket的实现,而是利用strace工具来反推Go Socket的行为。这一方法可以扩展我们分析代码的手段。
源代码级别的分析可以看其实现:net poll,以及一些分析文章:The Go netpoller,The Go netpoller and timeout

Go语言是我写web程序的首选, 它隐藏了很多细节,但仍然不失灵活性。最新我用strace工具分析了一下一个http程序,纯属手贱但还是发现了一些有趣的事情。

下面是strace的结果:

1234567891011121314151617181920
% time     seconds  usecs/call     calls    errors syscall------ ----------- ----------- --------- --------- ---------------- 91.24    0.397615         336      1185        29 futex  4.13    0.018009           3      7115           clock_gettime  2.92    0.012735          19       654           epoll_wait  1.31    0.005701           6       911           write  0.20    0.000878           3       335           epoll_ctl  0.12    0.000525           1       915       457 read  0.02    0.000106           2        59           select  0.01    0.000059           0       170           close  0.01    0.000053           0       791           setsockopt  0.01    0.000035           0       158           getpeername  0.01    0.000034           0       170           socket  0.01    0.000029           0       160           getsockname  0.01    0.000026           0       159           getsockopt  0.00    0.000000           0         7           sched_yield  0.00    0.000000           0       166       166 connect  0.00    0.000000           0         3         1 accept4------ ----------- ----------- --------- --------- ----------------100.00    0.435805                 12958       653 total

在这个剖析结果中有很多有趣的东东,但本文中要特别指出的是read的错误数和futex调用的错误数。

一开始我没有深思futex的调用, 大部分情况它无非是一个唤醒调用(wake call)。既然这个程序会处理每秒几百个请求,它应该包含很多go routine。另一方面,它使用了channel,这也会导致很多block情况,所以有很多futex调用也很正常。 不过后来我发现这个数也包含来自其它的逻辑,后面再表。

Why you no read

有谁喜欢错误(error)?短短一分钟就有几百次的错误,太糟糕了, 这是我看到这个剖析结果后最初的印象。那么read call又是什么东东?

123
read(36, "GET /xxx/v3?q=xx%20ch&d"..., 4096) = 520...read(36, 0xc422aa4291, 1)               = -1 EAGAIN (Resource temporarily unavailable)

每次read调用同一个文件描述符,总是(可能)伴随着一个EAGAINerror。我记得这个错误,当文件描述符还没有准备(ready)某个操作的时候就会返回这个错,上面的例子中操作是read。问题是为什么Go会这样做呢?

我猜想这可能是epoll_wait的一个bug, 它为每一个文件描述符提供了错误的ready事件?每一个文件描述符? 看起来read事件是错误事件的两倍,为什么是两倍?

老实说,我的epoll知识很了了,程序只是一个简单的处理事件的socket handler(类似)。没有多线程,没有同步,非常简单。

通过Google我找到了一篇极棒的文章分析评论epoll,由Marek所写,。

这篇文章重要的摘要就是:在多线程中使用epoll, 不必要的唤醒(wake up)通常是不可避免的,因为我们想通知每个等待事件的worker。

这也正好解释了我们的futex 唤醒数。还是让我们看一个简化版本来好好理解怎么在基于事件的socket处理程序中使用epoll吧:

  1. Bindsocket listener到file descriptor, 我们称之为s_fd
  2. 使用epoll_create创建epoll file descriptor, 我们称之为e_fd
  3. 通过epol_ctlbinds_fd到e_fd, 处理特殊的事件(通常EPOLLIN|EPOLLOUT)
  4. 创建一个无限循环 (event loop), 它会在每次循环中调用epoll_wait得到已经ready连接
  5. 处理ready的连接, 在多worker实现中会通知每一个worker

Using strace I found that golang using edge triggered epoll
使用strace我发现 golang使用edge triggered epoll:

1
epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=2490298448, u64=140490870550608}}) = 0

这意味着下面的过程应该是go socket的实现:

1、Kernel : 收到一个新连接.
2、Kernel : 通知等待的线程 threads A 和 B. 由于level-triggered 通知的"惊群"(“thundering herd”)行为,kernel必须唤醒这两个线程.
3、Thread A : 完成 epoll_wait().
4、Thread B : 完成 epoll_wait().
5、Thread A : 执行 accept(), 成功.
6、Thread B : 执行 accept(), 失败, EAGAIN错误.

现在我有八成把握就是这个case,不过还是让我们用一个简单的程序来分析。

12345678910111213
package mainimport "net/http"func main() {	http.HandleFunc("/", handler)	http.HandleFunc("/test", handler)	http.ListenAndServe(":8080", nil)}func handler(w http.ResponseWriter, r *http.Request) {}

一个简单的请求后的strace结果:

12345678
epoll_wait(4, [{EPOLLIN|EPOLLOUT, {u32=2186919600, u64=140542106779312}}], 128, -1) = 1futex(0x7c1bd8, FUTEX_WAKE, 1)          = 1futex(0x7c1b10, FUTEX_WAKE, 1)          = 1read(5, "GET / HTTP/1.1\r\nHost: localhost:"..., 4096) = 348futex(0xc420060110, FUTEX_WAKE, 1)      = 1write(5, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 116) = 116futex(0xc420060110, FUTEX_WAKE, 1)      = 1read(5, 0xc4200f6000, 4096)             = -1 EAGAIN (Resource temporarily unavailable)

看到epoll_wait有两个futex调用,我认为是worker执行以及一次 error read。

如果GOMAXPROCS设置为1,在单worker情况下:

12345678910111213
epoll_wait(4,[{EPOLLIN, {u32=1969377136, u64=140245536493424}}], 128, -1) = 1futex(0x7c1bd8, FUTEX_WAKE, 1)          = 1accept4(3, {sa_family=AF_INET6, sin6_port=htons(54400), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28], SOCK_CLOEXEC|SOCK_NONBLOCK) = 6epoll_ctl(4, EPOLL_CTL_ADD, 6, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1969376752, u64=140245536493040}}) = 0getsockname(6, {sa_family=AF_INET6, sin6_port=htons(8080), inet_pton(AF_INET6, "::ffff:127.0.0.1", &sin6_addr), sin6_flowinfo=0, sin6_scope_id=0}, [28]) = 0setsockopt(6, SOL_TCP, TCP_NODELAY, [1], 4) = 0setsockopt(6, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0setsockopt(6, SOL_TCP, TCP_KEEPINTVL, [180], 4) = 0setsockopt(6, SOL_TCP, TCP_KEEPIDLE, [180], 4) = 0accept4(3, 0xc42004db78, 0xc42004db6c, SOCK_CLOEXEC|SOCK_NONBLOCK) = -1 EAGAIN (Resource temporarily unavailable)read(6, "GET /test?kjhkjhkjh HTTP/1.1\r\nHo"..., 4096) = 92write(6, "HTTP/1.1 200 OK\r\nDate: Sat, 03 J"..., 139) = 139read(6, "", 4096)

当使用1个worker,epoll_wait之后只有一次futex唤醒,并没有error read。然而我发现并不总是这样, 有时候我依然可以得到read error和两次futex 唤醒。

And then what to do?

在Marek的文章中他谈到Linux 4.5之后可以使用EPOLLEXCLUSIVE。我的Linux版本是4.8,为什么问题还是出现?或许Go并没有使用这个标志,我希望将来的版本可以使用这个标志。

从中我学到了很多知识,希望你也是。

[0]https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
[1]https://idea.popcount.org/2017-02-20-epoll-is-fundamentally-broken-12/
[2]https://gist.github.com/wejick/2cef1f8799361318a62a59f6801eade8

]]>
0
<![CDATA[成长的套路 - 《复盘》读书感受]]> http://www.udpwork.com/item/16516.html http://www.udpwork.com/item/16516.html#reviews Wed, 29 Nov 2017 08:13:55 +0800 唐巧 http://www.udpwork.com/item/16516.html

联想有一种称为复盘的学习方式:做一件事情,失败或成功,重新演练一遍。大到战略,小到具体问题,原来目标是什么,当时怎么做,边界条件是什么,回过头做完了看,做的正确不正确,边界条件是否有变化,要重新演练一遍。我觉得这是提高自己非常重要的一种方式。
——柳传志 联想控股有限公司董事长

复盘

最近读完了一本书《复盘》,里面讲了很多的故事,全书显得很啰嗦,但是理论上其实就是简单几句话:事前做沙盘推演,做事情,事后做复盘总结。

复盘的过程又分成:

  • 回顾目标。将当时的目标和当前的现状做对比,找出不一致的地方,便于分析。
  • 叙述过程。复述整个事情的执行过程,收集尽量多的数据和细节,便于分析。
  • 反思原因。目标达到了或者是没有达到,为什么?自己的解释能够说服自己吗?
  • 总结规律。除了有总结外,还需要找有没有例子证明总结的规律。

类似的方法论

其实看完了《复盘》,我已经非常多次看到类似的方法论了。

比如在 Scrum 流程中,将回顾会议设置成一个 Sprint 标准的环节,其实就是强调复盘,以便总结出一些改进团队工作的结论。

比如在德鲁克的《卓有成效的管理者》一书中,就将决策要素分成五步,而第五步就是在执行过程中重视「反馈」。而这里的复盘,就是「反馈」的一种。

比如在美国管理学家戴明(Deming)的PDCA 理论中,将质量管理分成计划(P)、执行(D)、检查(C)、改善(A)。这里的改善,其实就是指的对检查过程中发现的问题进行总结,对于成功的经验进行标准化推广。

比如我们将产品经理的工作,分为定功能,画交互,跟项目,看结果。这里的「看结果」,其实就是一种复盘思维,看看线上的数据和自己之前的假设是否一样,以便进一步改进产品稿。

你看,虽然大家的理论不同,但是其实套路都是一样的。

是的,这就是一个非常简单的成长的套路。

程序员如何复盘

对于程序员应该如何复盘呢?

  • 写完代码后,想想下一次写,有没有可能写得更好,更快。
  • 改完需求后,想想下一次可不可能在架构上更容错。
  • 修复完线上bug后,想想有没有什么质量控制方法可以尽量避免犯同样的问题。

很多人都不知道如何提高,殊不知其实任何事情,都可以用复盘的心来做做总结,不知不觉,你就成长了。

希望大家都能学会这个「成长的套路」!

]]>

联想有一种称为复盘的学习方式:做一件事情,失败或成功,重新演练一遍。大到战略,小到具体问题,原来目标是什么,当时怎么做,边界条件是什么,回过头做完了看,做的正确不正确,边界条件是否有变化,要重新演练一遍。我觉得这是提高自己非常重要的一种方式。
——柳传志 联想控股有限公司董事长

复盘

最近读完了一本书《复盘》,里面讲了很多的故事,全书显得很啰嗦,但是理论上其实就是简单几句话:事前做沙盘推演,做事情,事后做复盘总结。

复盘的过程又分成:

  • 回顾目标。将当时的目标和当前的现状做对比,找出不一致的地方,便于分析。
  • 叙述过程。复述整个事情的执行过程,收集尽量多的数据和细节,便于分析。
  • 反思原因。目标达到了或者是没有达到,为什么?自己的解释能够说服自己吗?
  • 总结规律。除了有总结外,还需要找有没有例子证明总结的规律。

类似的方法论

其实看完了《复盘》,我已经非常多次看到类似的方法论了。

比如在 Scrum 流程中,将回顾会议设置成一个 Sprint 标准的环节,其实就是强调复盘,以便总结出一些改进团队工作的结论。

比如在德鲁克的《卓有成效的管理者》一书中,就将决策要素分成五步,而第五步就是在执行过程中重视「反馈」。而这里的复盘,就是「反馈」的一种。

比如在美国管理学家戴明(Deming)的PDCA 理论中,将质量管理分成计划(P)、执行(D)、检查(C)、改善(A)。这里的改善,其实就是指的对检查过程中发现的问题进行总结,对于成功的经验进行标准化推广。

比如我们将产品经理的工作,分为定功能,画交互,跟项目,看结果。这里的「看结果」,其实就是一种复盘思维,看看线上的数据和自己之前的假设是否一样,以便进一步改进产品稿。

你看,虽然大家的理论不同,但是其实套路都是一样的。

是的,这就是一个非常简单的成长的套路。

程序员如何复盘

对于程序员应该如何复盘呢?

  • 写完代码后,想想下一次写,有没有可能写得更好,更快。
  • 改完需求后,想想下一次可不可能在架构上更容错。
  • 修复完线上bug后,想想有没有什么质量控制方法可以尽量避免犯同样的问题。

很多人都不知道如何提高,殊不知其实任何事情,都可以用复盘的心来做做总结,不知不觉,你就成长了。

希望大家都能学会这个「成长的套路」!

]]>
0