Blog

不确定性的核心思想

Content #

实际上,不对称结果是本书的核心思想:我永远不可能知道未知,因为从定义上讲,它是未知的。但是,我总是可以猜测它会怎样影响我,并且我应该基于这一点做出自己的决策。

这一观点通常被错误地称为“帕斯卡的赌注”,取自哲学家及(思想)数学家布莱斯·帕斯卡。他的观点如下:我不知道上帝是否存在,但我知道,如果他存在,我做无神论者就会损失很大,而假如他不存在,做无神论者也得不到好处,所以我应该相信上帝。

但帕斯卡的赌注背后的思想在神学之外有十分重要的用途。它颠覆了整个知识的概念,消除了理解稀有事件的可能性的必要(我们对于这一问题的知识有根本上的局限性);相反,我们可以只关注某个事件发生带给我们的好处。

稀有事件的概率是不可计算的;确定一个事件对我们的影响却容易得多(事件越稀有,可能性越模糊)。我们能清楚地知道某个事件的影响,即使我们不知道它发生的可能性。我不知道地震的可能性,但我能想象地震对旧金山会造成怎样的影响。做决策时,你只需要了解事件的影响(这是你能知道的),不需要了解事件的可能性(这是你不可能知道的),这一思想就是不确定性的核心思想。我生活的大部分都以它为基础。

From #

黑天鹅

区分正面意外和负面意外

Content #

区分正面意外和负面意外。学会区分在不具可预测时从事哪些事会(或一直)对我们极为有利,在我们无法预测未来时从事哪些事有害。既有正面黑天鹅现象,又有负面黑天鹅现象。威廉·戈德曼从事电影行业,这是一个会发生正面黑天鹅现象的行业。不确定性确实不时给该行业带来了福音。

在负面黑天鹅行业,意外事件能造成极大的冲击和严重的后果。如果你从事军事、巨灾保险或国家安全工作,你总是面临不利影响。同样,如果你在银行从事贷款业,意外事件很可能对你不利。你把钱借出去,最好的情况是你能收回贷款,但如果借款人违约,你可能损失所有的钱。即使借款人获得巨大的财务成功,他也不太可能付给你额外的利息。

除电影业之外,正面黑天鹅行业还有:出版业、科学研究和风险投资。

From #

黑天鹅事件与杠铃策略

Content #

应该把一定比例的钱,比如85%~90%,投入极为安全的投资工具,比如国债,总之投入你能找到的最安全的投资工具。余下的10%~15%投入极具投机性的赌博中,用尽可能多的财务杠杆(比如期权),最好是类似风险资本的投资组合。这样一来,你就不受错误的风险管理的影响。没有黑天鹅事件能够超越你的“底线”伤害你了,因为你的储备金最大限度地投入了安全的投资工具。或者,同理,你可以拥有一个投机性投资组合,并确保(如果可能的话)它的损失不超过15%。这样,你就“剪掉”了对你有害的不可计算的风险。你不是承担中等风险,而是一边承担高风险,一边不承担风险。二者的平均值是中等风险,但能使你从黑天鹅事件中获益。用更为专业的术语,可以称之为“凸性”组合。让我们看看如何在生活的所有方面运用这一点

在这些行业,假如你什么也不知道,那么你是幸运的,尤其在别人同样什么都不知道,也不知道这一点的时候。假如你知道自己对哪些东西无知,假如你是唯一注意到那些没有被读过的书的人,你会是最棒的。这与“杠铃”策略是吻合的,在将正面黑天鹅事件的影响最大化的同时,保持对负面黑天鹅事件的警惕。要从正面黑天鹅事件中获益,你不需要对不确定性有任何精确的理解。有一点我很难解释,那就是在你只有非常有限的损失的时候,你必须尽可能主动出击,大胆投机,甚至“失去理智”。

From #

杠铃策略(Barbell Approach)

杠铃策略(Barbell Approach)

Content #

杠铃策略是指使投资组合中债券的久期集中在收益率曲线的两端,适用于收益率曲线两头下降较中间下降更多的蝶式变动。

在杠铃策略的形式下:银行只购买短期证券和长期证券,不购买或只购买很少数量的中期证券。比如银行将50%的可用资金购买1~4年期的证券,另外50%的资金购买8~10年期的证券,不购买5~7年期的证券,如图所示。

上图的形状看起来像一个杠铃,因而称为杠铃策略。这种方法从长期证券中获得高收益以满足盈利要求,短期证券提供充足的流动性。维持这种投资策略的方法之一是,变现到期的短期证券,所得资金投资于期限最长的短期证券;出售到期 13最短的长期证券,并投资于期限最长的长期证券。

From #

MESI有限状态机

Content #

整个 MESI 的状态,可以用一个有限状态机来表示它的状态流转。需要注意的是,对于不同状态触发的事件操作,可能来自于当前 CPU 核心,也可能来自总线里其他 CPU 核心广播出来的信号。

Viewpoints #

From #

39 | MESI协议:如何让多核CPU的高速缓存保持一致?

我把对应的状态机流转图放在了下面,你可以对照着Wikipedia 里面 MESI 的内容,仔细研读一下。

独占状态与共享状态

Content #

“独占”和“共享”这两个状态是 MESI 协议的精华所在。无论是独占状态还是共享状态,缓存里面的数据都是“干净”的。这个“干净”,自然对应的是前面所说的“脏”的,也就是说,这个时候,Cache Block 里面的数据和主内存里面的数据是一致的。

那么“独占”和“共享”这两个状态的差别在于:

在独占状态下,对应的 Cache Line 只加载到了当前 CPU 核所拥有的 Cache 里。其他的 CPU 核,并没有加载对应的数据到自己的 Cache 里。这个时候,如果要向独占的 Cache Block 写入数据,我们可以自由地写入数据,而不需要告知其他 CPU 核。

在独占状态下的数据,如果收到了一个来自于总线的读取对应缓存的请求,它就会变成共享状态。这个共享状态是因为,这个时候,另外一个 CPU 核心,也把对应的 Cache Block,从内存里面加载到了自己的 Cache 里来。

而在共享状态下,因为同样的数据在多个 CPU 核心的 Cache 里都有。所以,当我们想要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他 CPU 核心里面的 Cache,都变成无效的状态,然后再更新当前 Cache 里面的数据。这个广播操作,一般叫作 RFO(Request For Ownership),也就是获取当前对应 Cache Block 数据的所有权。

有没有觉得这个操作有点儿像我们在多线程里面用到的读写锁。在共享状态下,大家都可以并行去读对应的数据。但是如果要写,我们就需要通过一个锁,获取当前写入位置的所有权。

Viewpoints #

From #

39 | MESI协议:如何让多核CPU的高速缓存保持一致?

写失效与写广播

Content #

MESI 协议,是一种叫作写失效(Write Invalidate)的协议。在写失效协议里,只有一个 CPU 核心负责写入数据,其他的核心,只是同步读取到这个写入。在这个 CPU 核心写入 Cache 之后,它会去广播一个“失效”请求告诉所有其他的 CPU 核心。其他的 CPU 核心,只是去判断自己是否也有一个“失效”版本的 Cache Block,然后把这个也标记成失效的就好了。

相对于写失效协议,还有一种叫作写广播(Write Broadcast)的协议。在那个协议里,一个写入请求广播到所有的 CPU 核心,同时更新各个核心里的 Cache。

写广播在实现上自然很简单,但是写广播需要占用更多的总线带宽。写失效只需要告诉其他的 CPU 核心,哪一个内存地址的缓存失效了,但是写广播还需要把对应的数据传输给其他 CPU 核心。

Viewpoints #

From #

39 | MESI协议:如何让多核CPU的高速缓存保持一致?

CPU缓存中的事务串行化

Content #

事务的串行化(Transaction Serialization),事务串行化是说,我们在一个 CPU 核心里面的读取和写入,在其他的节点看起来,顺序是一样的。

第一点写传播很容易理解。既然我们数据写完了,自然要同步到其他 CPU 核的 Cache 里。但是第二点事务的串行化,可能没那么好理解,我这里仔细解释一下。

我们还拿刚才修改 iPhone 的价格来解释。这一次,我们找一个有 4 个核心的 CPU。1 号核心呢,先把 iPhone 的价格改成了 6000 块。差不多在同一个时间, 2 号核心把 iPhone 的价格改成了 5000 块。这里两个修改,都会传播到 3 号核心和 4 号核心。

然而这里有个问题,3 号核心先收到了 2 号核心的写传播,再收到 1 号核心的写传播。所以 3 号核心看到的 iPhone 价格是先变成了 6000 块,再变成了 5000 块。而 4 号核心呢,是反过来的,先看到变成了 5000 块,再变成 6000 块。虽然写传播是做到了,但是各个 Cache 里面的数据,是不一致的。

事实上,我们需要的是,从 1 号到 4 号核心,都能看到相同顺序的数据变化。比如说,都是先变成了 5000 块,再变成了 6000 块。这样,我们才能称之为实现了事务的串行化。

事务的串行化,不仅仅是缓存一致性中所必须的。比如,我们平时所用到的系统当中,最需要保障事务串行化的就是数据库。多个不同的连接去访问数据库的时候,我们必须保障事务的串行化,做不到事务的串行化的数据库,根本没法作为可靠的商业数据库来使用。

而在 CPU Cache 里做到事务串行化,需要做到两点,第一点是一个 CPU 核心对于数据的操作,需要同步通信给到其他 CPU 核心。第二点是,如果两个 CPU 核心里有同一个数据的 Cache,那么对于这个 Cache 数据的更新,需要有一个“锁”的概念。只有拿到了对应 Cache Block 的“锁”之后,才能进行对应的数据更新。

...

写回(Write-Back)

Content #

不再是每次都把数据写入到主内存,而是只写到 CPU Cache 里。只有当 CPU Cache 里面的数据要被“替换”的时候,我们才把数据写入到主内存里面去。

如果发现我们要写入的数据,就在 CPU Cache 里面,那么我们就只是更新 CPU Cache 里面的数据。同时,我们会标记 CPU Cache 里的这个 Block 是脏(Dirty)的。所谓脏的,就是指这个时候,我们的 CPU Cache 里面的这个 Block 的数据,和主内存是不一致的。

如果我们发现,我们要写入的数据所对应的 Cache Block 里,放的是别的内存地址的数据,那么我们就要看一看,那个 Cache Block 里面的数据有没有被标记成脏的。如果是脏的话,我们要先把这个 Cache Block 里面的数据,写入到主内存里面。然后,再把当前要写入的数据,写入到 Cache 里,同时把 Cache Block 标记成脏的。如果 Block 里面的数据没有被标记成脏的,那么我们直接把数据写入到 Cache 里面,然后再把 Cache Block 标记成脏的就好了。

在用了写回这个策略之后,我们在加载内存数据到 Cache 里面的时候,也要多出一步同步脏 Cache 的动作。如果加载内存里面的数据到 Cache 的时候,发现 Cache Block 里面有脏标记,我们也要先把 Cache Block 里的数据写回到主内存,才能加载数据覆盖掉 Cache。

可以看到,在写回这个策略里,如果我们大量的操作,都能够命中缓存。那么大部分时间里,我们都不需要读写主内存,自然性能会比写直达的效果好很多。

Viewpoints #

From #

38 | 高速缓存(下):你确定你的数据更新了么?

...

写直达(Write-Through)

Content #

每一次数据都要写入到主内存里面。写入前,我们会先去判断数据是否已经在 Cache 里面了。如果数据已经在 Cache 里面了,我们先把数据写入更新到 Cache 里面,再写入到主内存里面;如果数据不在 Cache 里,我们就只更新主内存。

写直达的这个策略很直观,但是问题也很明显,那就是这个策略很慢。无论数据是不是在 Cache 里面,我们都需要把数据写到主内存里面。这个方式就有点儿像在Java中使用 volatile 关键字,始终都要把数据同步到主内存里面。

Viewpoints #

From #

38 | 高速缓存(下):你确定你的数据更新了么?