并发计数,简单如一二三!

标题:Case Study: Concurrent Counting—As Easy as 1, 2, 3!

日期:2024/10/01

作者:Paul McKenney

链接:https://kernel-recipes.org/en/2024/schedule/case-study-concurrent-counting/

注意:此为 AI 翻译生成 的中文转录稿,详细说明请参阅仓库中的 README 文件。


早上好。那么这次我们讲计数。上次我们讲的是硬件。如果我能把鼠标移到幻灯片上,我可能会……现在如果我能到这里。好了。这是去年的配方。去年我们按对了按钮,而不是像那次一样按错了。去年我们做了蓝色的前两行。今年我们要做一点绿色的部分。对于那些更喜欢英制单位的人,这就是了。现在,我们要快速回顾一下。快速回顾一下那些内容。我不会试图慢慢来,硬着头皮讲完。如果感到困惑或有疑问,我鼓励你们去看去年的录像。当然,随时提问。因为讨论更重要。然后我们会试着慢慢来,硬着头皮讲完。因为讨论比我讲完一堆固定的幻灯片更重要。好的。分发。谢谢,安妮,你的妙语(法语:merci, Anne, pour le bon mot)。我们要把这些发下去。分发的原因是,我们需要在结束时了解你们是否真的从这次演示中学到了东西。好的。想吃的话你们可以吃掉它们。那可能会对评估造成误差。当然,你们也可以留着包装纸那样做。但我会让你们自己判断。总之,这些伙计们会分发下去。我会在他们分发时继续讲,因为这或多或少是回顾。

那么,物理定律是什么?我小时候可不会相信这个。但事实是,对于计算而言,原子特么太大了。我的意思是,它们大得不方便。问题是,如果你有一个晶体管,除了少数研究特例,你能得到的最小、最薄的基区就是一个原子。基区越薄,晶体管速度越快。而我们的基区真的没有几个原子那么厚。所以我们在这方面有点被卡住了。所以这是我们的一个问题。我们另一个问题是光太慢了。我雇主的计算机,最常见的那种,大概这么大(手势)。好的。在一个时钟周期内,电波在硅中能走这么远(手势)。你可以看到,你不可能在一个周期内穿过整个复合体。那需要很多个周期。此外,除了移动数据本身的成本,还有很多其他东西。协议开销、电子器件、复用和解复用、时钟域转换。据我所知,从一个时钟域转换到另一个时钟域,需要最慢时钟的三个周期,除非它们是整数倍关系,但这从来不可能,因为我们有热节流(thermal throttling)和变化。时钟频率会变化。然后实际的存储器本身也必须改变。所以,我们面临一些相当棘手的物理定律。我想下面红色部分才是重点。我亲耳听到摩尔定律的戈登·摩尔说过这个。他说他们把史蒂芬·霍金请到英特尔研究院,让他看看他们在做什么,看看他们能做些什么来利用基本物理定律让东西跑得更快。戈登·摩尔说史蒂芬·霍金说了以下的话:先生们,你们有两个根本问题。第一,光速有限;第二,物质的原子性。那么,你们中也许有人能让某些东西超光速,或者以某种方式让原子变小。如果你们做到了,大概会有诺贝尔奖等着你们,那会很酷。我很想看到。与此同时,在这一切发生之前,我们必须处理我们拥有的世界和我们目前所知的物理定律。顺便说一句,这些都是经验性的,对吧?光速不是数学上设定的。只是我们测量了它。谁知道呢,对吧?

原子也是如此,在不同的化合物中它们的大小基本保持不变。但关键是硬件架构师们都非常清楚这些限制。他们所做的,这是一个老系统。这是英特尔酷睿2,很多年前的。我用它的原因是你们在幻灯片上还能看清。如果我用现代的,你们就什么也看不清了,因为所有东西都太小了。但关键是这里,我不会详细讲这个,但如果你们看左边下来的东西和顶部那边的东西,有超过100条指令正在执行过程中。好的。他们这样做的原因是,一条给定的指令可能会被光速、物质或其他什么拖慢。如果他们有什么东西能更快完成,他们不想干等着。他们希望能够并发地执行那些指令。他们还用了一堆其他技巧。现在,你们知道,现在的(芯片)和以前不一样了。这个(Core 2)也没那么老。左边那个有胡子的家伙,那是80年代的东西。那是一个摩托罗拉68000,我80年代初还玩过。如果看70年代,你会看到像那样的东西(指冰箱大小的机器)。好的。这是个冰箱大小的机器。没多少内存。非常慢。最快的指令执行需要1.6微秒。好的。我很自豪这东西在博物馆里。他们在运行一个程序。我和另一个家伙在77年大学时写的程序。我很自豪我们在一个最小指令延迟为1.6微秒的系统上,在不到21微秒内计算出了三角正弦和余弦。好的。但关键是,这东西又好又简单。我的意思是,如果你想知道编程所需的一切知识,它在六张纸上。就这些。好的。我不知道有没有人看过当前芯片的参考手册。我上次看到的好像是,3000页左右。也许现在5000页了。我不知道,但它们巨大无比。好的。另一方面,这东西很慢。好的。真的很慢。现在的问题是,我们有一个旧系统,按现代标准并不那么复杂。而我展示的那个(Core 2)有方块图,显示着大约一百条指令在等待并行运行。

在某些情况下,你可能需要处理所有这些复杂性。如果你试图从你的计算机系统中榨取最后一点性能,并且你愿意在它开始变旧时扔掉它。并且你愿意为每一代新硅片重新移植你的软件,并且,并且,对。在大多数情况下,问题是这些东西,有些能自我修复。它们会退化、老化,就像我一样。有些部分工作得不太好。所以在某些情况下,有些CPU能发现这点并关掉ALU的那部分不再使用它。到那时,你精心优化的软件就不再是最优的了。所以我们采取的做法是,采用一个更卡通化的视图(cartoony view)。我们就说,有一个CPU,有一个存储缓冲区(store buffer),在等待缓存行出现时记录东西。我们有一个缓存。好的。如果我们这样做,我们也许能获得80%或90%的收益,而不会失去可移植性,也不会有那么多复杂性。好的。所以我们要看的是,CPU、存储缓冲区、缓存,以及我称之为总线的东西。它过去真的只是一组并行的电线。现在是一些非常复杂的互连网络,但没办法。有时它仍然被称为总线。CPU用字计算,也许是64位。我的意思是,这取决于计算机。存储缓冲区保存着等待缓存行出现的字。那些缓存行保存在缓存中,可以看作是一个硬件哈希表。行(lines)可能是64字节,也就是512位,也可能更大或更小。这取决于系统。所以系统使用这个硬件哈希表来让东西靠近CPU。好的。这样CPU在需要时就能拿到它。这不是一个新概念。约翰·冯·诺依曼在1945年就指出了这种可能性,我相信是。也许是1947年,但不管怎样,它已经存在一段时间了。至少概念上存在了。然后我们有一个总线,或者也叫互连(interconnect),它在CPU之间以及通往内存的路线上传输这些缓存行。所以如果我们用图示来看这个,我们有CPU。我们让CPU一和二,什么也不做,因为图表上需要空间。我们有一个总线,它把缓存线运送到内存和各个缓存,这样如果一个CPU需要什么东西,它可以从内存或者从拥有最新值的缓存那里得到,以某种方式,保持,保持事情进行下去。这个想法是程序倾向于具有访问局部性。它们倾向于频繁地使用相同的东西或附近的东西。这种结构允许硬件利用这一点,并在某些情况下绕过光速的限制。但在我们的例子中,数据根本没有机会进入内存。所以我们会把这部分省略。我们只让总线在线路周围传输。那么我们来做一些并发计数吧。谁准备好数数了?有几个人。好的。我想是早上太早了。我的意思是,好吧,我们就这么做吧。对吧?我们只是计数,你知道,我们加然后返回计数器。我们有一个无符号长整型计数器。如果是有符号计数器,当我们回绕或溢出时,我们会遇到未定义行为。那么有谁想点评一下这段代码吗?

我看到几个人点头说是,但他们没说什么。它不可扩展。好的。还有别的问题吗?确实如此。没有互斥锁。为什么,为什么需要互斥锁?好的。是的。所以这个(代码)的问题是,如果你在一个没有内存加指令的系统上运行,而该指令碰巧是原子的,在6个CPU上,你会丢失87%的计数。好的。所以你数了一百次,最后只得到13而不是一百,这我可能……计算中近似是有作用的,别误会。但根据我的经验,这种近似通常不是你们想要的。

那么计数为什么会丢失呢?好的。我们再看这个(图)。我们让CPU一和二当观众,让CPU零和三真正干活。发生的是计数器的缓存行在CPU三上。所以计数器是零,它在CPU三的缓存里。CPU零试图递增这个东西,但它没有缓存行。所以它做的是把新值存到存储缓冲区里。好的。那边那个家伙,这并不完全是它的工作原理,比那更复杂,但我们就说它在缓存里递增了那个东西。好的。所以那里我们得到了一个1。与此同时,这个家伙(CPU零)说,嘿,我没有缓存行。把它给我。这个请求通过总线发出去,我们数到二(CPU三递增),我们得到了三(CPU三再递增)。然后我们说,好吧,这时我们返回三。CPU三没有缓存行。所以它在存储缓冲区(指CPU零的存储缓冲区)里工作。所以我们得到了两个4(可能指CPU零和CPU三各自在本地操作后的值),加起来是8。现在我们回来,我们把3存入CPU零的缓存。这时,他们两个都计了5次数,总共10次。然后他们再数一次,我们得到计数器等于6(可能某个CPU的值)。实际总数是12,每个CPU各6次。在这个图上根本看不到12。这就是为什么我们会丢失计数。问题是CPU在做计算。好的。但在这里的内存系统(指缓存和总线层面)中,我们只是在覆盖东西。所以每次它来回穿梭,我们就丢失了其他CPU计数的部分。这就是为什么在6个CPU上,我们丢失了六分之五的计数。我们只得到一个CPU的计数值,但它非常快。所以,你知道,我们获得了快速的写入完成(write completion)。更新非常快,但我们付出了丢失大量计数的代价,准确性非常糟糕。

这就是为什么我们有原子操作(atomic operations),就像刚才后面有人稍微提到的那样。那么我们就原子地计数吧。能有多难呢?如果我们在Linux内核中,我们可能会这样做。我们有一个atomic_t计数器,说明这是一个32位原子计数器。要递增,我们调用atomic_inc函数,它接受计数器的地址。如果我们有一大堆人试图同时递增,它会全部整理好,所有的计数都会生效。对于读取计数(read count),我们只是对计数器做一个原子读取,它会获取当前值。

有谁想点评一下这段代码吗?串行化且慢。非常好。你们跟上了。是的。这发生在我以前用过的一台电脑上。所有硬件都终有一死,现在已经过世了,但它当时真的很酷。现在,440 CPU,440 CPU的至强。如果我们只有一个CPU做原子操作,我们大概在5纳秒内完成。但如果我们让所有的CPU都做,我们就超过10微秒了。好的。所以我们增加CPU,它变得更慢,不只是慢一点,而是慢很多。你们在后面听到的原因是我们在串行化一切。这让Tux(Linux吉祥物企鹅)不高兴了。我很抱歉。你知道,这个可怜的Tux在哭,因为我们做了这么可怕的事情。

好的。那是一个老系统。我们试试新一点的。这是我碰巧有的当前机器。是单个机器。166个CPU的AMD。实际上是一个客户操作系统。我不知道是否有真正的硬件有166个CPU,但那是一个客户OS。米兰(Milan)。它是2GHz。它没那么糟。我的意思是,我们达到了2微秒,而不是超过10微秒。另一方面,我们只有三分之一数量的CPU(相比440)。我们确实有更紧密的集成。所以不管怎样,它仍然不好。好的。我的意思是,当我增加CPU时,我希望它更快,而不是更慢。也许这只是因为我是一个50年代出生的老家伙,但这就是我的世界观。好的。你知道,Tux在哭,但想想那些可怜的CPU。你知道,它们在等待另一个CPU做它的事情。然后这花了好长好长时间。我的意思是,20微秒对于现代CPU来说等待所有这些事情真的是非常、非常、非常长的时间。你知道,你为什么让它们经历所有这些麻烦?这就是正在发生的事情。现在,这个图(指之前提到的440个CPU的图)确实有440个你在那八个集群中看到的小方块。缓存行必须在所有这些CPU之间来回传送。它一次只能在一个CPU上。只有一份副本。它必须穿过所有的CPU。那需要很长时间。再次强调,光速有限、原子大小非零、缓存一致性协议以及其他一切。有一些硬件优化可以应用。这些幻灯片将提供,你们可以查看那个URL获取更多信息。

但有一个问题是,我们应该总是避免原子操作吗?我的意思是,我们不想让Tux哭,也不想让CPU不耐烦,对吧?和往常一样,这取决于情况。如果你设法构造了像左边那样的算法,在特定时间只有少数几个CPU在争用,那就没那么糟。特别是如果你不经常做的话。但如果你在右边那种情况,那就是个坏主意。你真的需要避免那样。那是个好方法,如果它发生在我雇主的服务器群里,轮到谁值班谁就会在深夜被叫醒去修复问题。所以我们不要那样做。

好的。但有时我们可以做得更好得多。有谁想说说我们怎么能做得更好吗?它被用得很重。非常好。这家伙甚至没看源代码就说了。那很好。但是,我们必须考虑的另一件事是,如果我们那样做,我们会得到过时的值。但我们看看原子操作的情况。好的。假设我们这样做。我们读取计数,把它传给一个函数,传给第二个函数,传给第三个函数。好的。这个东西是原子的,所以计数是精确的。但当我们把它传给第三个函数时,它有多精确呢?嗯,看看这个。我们有计数器随时间变化的值。如果我们只是对计数器做++,我们最终会得到下面那个烂摊子,我们丢失了大部分计数,底部的红线。如果我们使用原子操作,我们就在顶部那里,它实际上给了我们所有的计数。它实际上记录了所有的计数。现在,如果我们在那些时间点调用这些函数,所以我们读取一次,把它传给第一个函数、第二个函数、第三个函数,我们将在所有三个函数中使用相同的值,尽管它后来(的值)已经变了。所以我们后来得到了一个过时的值。是的,我们费了这么大劲来确保我们有精确的原子计数,完美地计数一切,但我们最终仍然得到了过时的值。好的。事实上,在那个值甚至还没有机会从读取函数返回之前,它就可能已经过时了。所以并不是说你只要够快就行,因为你甚至可能在拿到它之前它就过时了。

那么,如果我们反正得不到(最新)值,我们为什么要为原子操作付出如此高的代价呢?再说一遍?好的。那么我试着,让我重复一下。如果我说的不是你希望我说的,请喊出来纠正我。基本上,是的,它会过时,但我们可以再读一次,然后我们就得到一个精确的值。所以随着时间的推移,我们会有一个合理的东西。这是计数器的一个好特性。

所以问题是,我们能否以不那么昂贵的代价获得这个特性?因为是的,那很重要。我们希望这个计数,如果我们这周读一次,下周读一次,我们希望看到期间有很多计数发生。我们之前有答案了。如果读操作不频繁,我们能做得更好吗?假设我们有一个读操作不频繁的情况。我们能做得更好吗?我们之前听到我们可以使用每个CPU(per-CPU)的变量,它们被大量使用。我们来看看这是怎么运作的。所以每个CPU有自己的变量,计数器0、1、2、3在那里。然后它们在它们的缓存里,因为它们是操作它的。所以它们递增,它们都在同一时间递增。那发生得很快。我们不必让东西在整个复合体(commute complex? 应为compute complex,计算复合体)之间踢来踢去。它就在那儿。一次又一次。然后如果第三个CPU想把它们加起来,它必须请求那些计数器。它会拿到它们。它们在另一个(CPU的缓存)中被标记为共享(shared),这样我们就知道不会让它们不同步。我不会深入探讨这个,但它可以把它们加起来。我们得到实际计数15,这实际上是计数,而不是四分之一(每个CPU计数)的计数(指如果串行丢失计数的情况)。

好的。所以在这里,如果我们这样做,更新真的非常快。我的意思是,那个东西就留在我们的缓存里。我们递增它。很棒,但读取有点慢。我们得把所有这些东西加起来。如果你有,哦,我以前有过一台404个CPU的机器。我在2012年收到过一台4096个CPU机器的错误报告。把所有这些加起来需要一些时间。另一方面,误差受到在求和期间那个,理想计数器变化量的限制。所以我们在这里开始求和。我们在这里结束。计数器有一个概念上的值,我们会得到介于两者之间的某个值。所以我们得到了,合理的误差。我们得到了计数器在某个中间点的真实值。这就是,现在,现在在底部,那些正好在X轴上的小X,大概在几纳秒的地方。实际上,是几分之一纳秒。这就是我们更新所得到的速度。所以如果读取不频繁,这太棒了。你知道,我们让Tux高兴了,但如果,所以如果更新频繁,我们很棒。如果读取不频繁,但如果我们做很多读取,我们仍然在这里有麻烦。

现在,这是我们使用的伪代码。所以我们只是给计数器加一,使用一次写入(write once)来避免编译器捣乱。我们遍历并读取每个值,把它们加起来,从总和等于零开始。代码大概在那里。这就是我们开始的东西。问题是,我们能在更新端做得更好吗?我们能否,我们能否鱼与熊掌兼得?我们能否既享有更新端的蛋糕,也享用读端的蛋糕?让它两端都很快。有什么想法怎么做吗?

翻转(Flipping),那边有个问题箱给你。所以分组。分组。是的,我不确定用每个CPU变量实际怎么工作。思考一下。你可以做类似的事情。有,有办法,但我想试试更简单的。好的。Blastomil(人名)。Blastomil。标记(Badging),就像当每个CPU计数器超过阈值时计数。一次原子操作。好的。所以我们可以做的是,我们可以缓存值,有点像Blastomil可能知道一点的slab分配器。然后一旦每个CPU的(计数)超过某个点,我们就把它转储到全局计数器里。然后我们就把全局的当作一个估计值。好的。我要做点类似的,但我要让它更,更简单。因为这只是一个演示,而不是实际的Linux内核代码。你说的可能对实际的Linux内核代码更好用。

我们要做的是,在我们之前幻灯片看到的代码基础上增加这些函数。所以我们要有另一个变量counter_sum,然后要读取时,我们就读取那个变量。我们就做这些。当有人说read_count_fast时,我们就给他们那个变量。你仍然可以说read_count,如果你想慢点读,也许能得到更新鲜点的东西。我们要有一个线程来遍历。你可以用很多其他方法做这个,但你可以有一个线程,偶尔去把那些计数器加起来,然后塞进那个counter_sum里。这里我们在每次做之间等待10毫秒,10个jiffies,在大多数系统上是10毫秒。这意味着你可以非常快地读取那个东西。你读到的值可能会过时10毫秒。如果你每分钟左右读一次,谁在乎10毫秒呢?但有时你确实在乎。

如果我们这样做,我们就让Tux在两端都高兴。这个图在远端看起来难看,但它只上升到6纳秒。好的。相比之前的那些图,它们上升到了微秒甚至几十微秒。我们在高端确实看到了一些性能下降和热节流。在某些系统上,超线程会导致这种情况。但它比我们以前好多了。所以所有操作都是几纳秒。为什么不一直这么做呢?对吧?当然,这就像其他事情一样。这是工程,不是科学。有缺点。好的。我们不能忽视事情。这里有一堆原因。我的意思是,你有这个额外的线程,有时你负担不起。这个线程每10毫秒就要唤醒一次。如果你那样做并且把它放在核心内核里,我上次那么做时,接到了一个非常不愉快的电话。他们对在Linux内核邮件列表上发火还不满意。他们打电话给我,对我大喊大叫。所以如果你那样做,电池电量(battery power)组的人会对你非常生气。

另一件事是,对于统计用途,计数读取通常是不频繁的。你在每个数据包接收时做统计,但你只在每次,每隔几秒或几分钟才把它们读出来。所以何必费那个劲?好的。是的,那次读取可能要花160微秒。但如果你不经常做,谁在乎呢?好的。还有其他一些事情。我们看看。前两个是Velostomil建议的。我有一本书里面有这些。URL也在里面,我不会,我时间有限。所以我不必讲它们。

我要做的是,非常快地看一下Linux内核中计数的一些案例研究。我只是,这只是一个概述。请不要期望你们拿着这张幻灯片就能把它变成生产质量的代码。实际上,如果你想那样做,去看代码本身。好的。因为我在这里只做高层次概述。

那么统计计数器是第一个。这到处都在用。this_cpu_inc是那里的主力。它有超过300个使用实例。有时它们做什么,例如对于网络,它们做this_cpu_inc来计数数据包。用this_cpu_add来计数字节。好的。this_cpu_add接受一个数字或其他东西。它通常是开放编码的。没有为它设计抽象层。我不清楚是否应该有。也许应该有,但无所谓。我有一本1987年出版的教科书。它附带说了一句,好像这是显而易见的,每个人都知道,这就是你做统计的方法。所以这已经知道很久了。我不知道是谁发明的。它可能是在60年代甚至50年代发明的,据我所知。好的。

那么每个CPU的引用计数。这将是一个非常卡通化的视图。想法是这个引用计数非常、非常快。但当你需要判断它是否为零时,它会把它压缩(squishes)成一个单一的变量。我们看图。所以通常我们有每个CPU的计数器(per CPU counters)。然后有一个函数调用把这个压缩成一个单一的计数器,在那一刻我们会慢一阵子,但我们可以立即判断它是否为零。每个CPU计数的问题是,你把它们加起来,但你有偏移。所以你并不知道真正的值是多少。然后你也可以切换回来。虽然在很多情况下,人们做的是当他们想销毁时把它转为全局的。然后当它为零时,他们就把它清理掉,完事。这非常卡通化。有很多奇怪的事情会出错,竞争条件,各种问题。不要试图根据这个图来写代码。去看内核里的代码。好的。这上面有很多毛刺,但概念上大致就是这样。

现在这里发生的是,我们从每个CPU,到全局,以进行完全同步。所以一个问题是,我们能否在保持它为每个CPU的同时做任何同步?我们该怎么做?Matthew(人名),你已经知道了,所以不准回答。

我们能做的是SRCU(可睡眠读复制更新)所做的。再次,这将是非常卡通化的。发生的是我们有每个CPU的计数器(per CPU counters)。我们有四个。在,在每一边的左边,我们有上锁的数量,在右边是解锁的数量,但我们为每个(CPU)有两个变量,只是一个数组。我们有一个指示器说明该数组的当前边是哪个。所以现在当前的是索引为0的那一边。所以如果我们从CPU零做一个SRCU读锁(SRCU read lock),它会增加CPU零计数器左上角的那个(计数器)。所以你可以看到它现在是1而不是0。但当前(current)也被更新了。我们要做一个宽限期。所以我们更新那个(current)。那意味着,如果我们有另一个SRCU读锁,比如说在CPU一上,它会增加底部那个(指CPU一数组的当前边)。与此同时,来自CPU零的SRCU读锁可能已经去睡眠并在CPU二上醒来。当它解锁时,它会做零(指递减操作),但在另一个CPU上。一旦完成,注意,如果你把锁加起来,把解锁加起来,它们是相等的。那就意味着所有旧的读者都走了。好的。因为新的读者在做另一边。所以有,有些技巧可以用。再次,这里有很多我没有展示的毛刺,但那大致是你可以做的事情的总体概念。再次,请在学习SRCU代码之后再尝试(去实现),不要仅仅根据这些幻灯片去尝试。

好的。所以计数很酷的一点是它简单。哦,请讲。关于上一张幻灯片,当前索引(current index),它是全局的还是每个CPU的?当前索引是全局的的。所以我们,所以每个人都看那个全局的,并用它来选择他们数组的哪一边将被使用。所以它是用原子操作读写的,对吧?实际上,它只是,它使用读一次(read once)和写一次(write once),但在机器层面它只是直接的加载(load)和存储(store)指令。好的。所以它不需要在所有CPU上完全同步。这是毛刺的一部分。你必须处理它不同步的事实。这就是为什么你不应该试图根据这个图来编写SRCU的原因之一。好的。所以,我很高兴哪天带你们过一遍,但可能不是今天早上。好的。

所以,是的,好问题。那么关键点是,为什么要折腾计数?嗯,事情是,每个人都懂计数或者自以为懂。它其实相当简单。所以我们没有那些用比较交换原子更新的奇怪链表数据结构的所有复杂性。它是一个相当简单的操作,但它向我们展示了分区(partitioning)。它向我们展示了部分分区(partial partitioning)有帮助。它向我们展示了延迟有帮助,就像我们在那个把东西聚合到单一计数器以获得快速读取的例子中看到的那样。然而,这是工程,不是科学。我的意思是,偶尔有科学成分,但主要是工程,而那是关于权衡的。这意味着硬件和工作负载会影响最优设计。好的。有时你必须选择一个对任何单一工作负载都不是最优,但对所有人来说都相当不错的设计。所以它,我从中获得很多乐趣。有些人会感到沮丧。这就是生活。好的。

粒子计算(应为Practical counting,实际计数)。你们学到东西了吗?所以,我们有几个分发糖果的人,我需要你们做的是数它们。你们需要想好怎么做。也许每行做一个和,然后他们再把这些和加起来。你们可以自由使用智能手机或其他任何你们想用来帮助算术的东西,以防你们年轻得没在学校学过心算。所以我们给你们几分钟时间来做这个。都是乐趣的一部分。我们再给你们一分钟。最后一分钟。

我看到有些人对此采取了非常实际的观点。务实就是胜利。好的。事情安静下来了。那么我们有数字了吗?我听到一片沉默。这有点不公平的问题。我高中时曾在杂货店工作过。你知道,那时库存(inventories)是手动的。所以你有一群人在杂货店里跑,实际点数所有东西并记录下来。那真是,所以我,很早就见识到计数能有多难。好的。你只需要让某人搞混了。两个人把同一个东西数了两次。所以,你知道,它很快就变得,或者他们忘了什么。它很快就变得混乱不堪。但另一方面,你可以争辩说这不是一个公平的问题。但你知道,如果你只能回答公平的问题,会有很多非常重要的问题你无法回答。好的。所以,你知道,无论如何接受它。这就是为什么我们有电脑为我们计数。好的。好的。

总之,现代硬件高度优化,但它是为特定东西优化的。渐进式的改进正在做集成。关键是你想使用现有的软件。所以如果你的Linux内核使用已经存在的东西,并且,结构化代码和数据以避免大的障碍。根据你在做什么,有很多快速并发计数的技巧。更多信息在这里(URL)。这是我,去年的配方。在最后这张(指配方幻灯片),如果你更喜欢英制单位,在这张上,我收到了很多奇怪的问题,我给出了可能更奇怪的答案。但据我所知,人们真正想问的是,怎样算过量服用?你知道,如果你,如果你喝这个伏特加和黑莓的混合物,多少量是?多少量算太多?所以今年的配方回答了这个问题。好的。

这里是,它的版本(指配方)。再次,为那些喜欢英制单位的人。所以基本上用爆米花做东西,你煮它五个小时,或者直到发生什么激动人心的事情。现在这个配方,你并不是通过真正使用这个配方来从中受益。不是通过真正遵循它。它有效。这有点像元配方,有点像RCU(读复制更新)那种方式。如果你看着这个配方觉得它是个好主意,那你已经严重过量了。

并且永远记得,杀鸡焉用牛刀。如果没有合适的工具,就发明一个。

如果我们还有一两分钟时间提问的话,我想。谢谢。谢谢。如果你们没有问题,非常感谢你们的时间和关注。希望这对你们有用,再次感谢Ann提供道具,我看到你们有些人已经在享用了。下一场演讲见。干杯。干杯。是的。干杯。干杯。干杯。干杯。干杯。