大咖说 | 分布式数据库与i++/++i问题随想

2023/12/14 15:55
阅读数 15

正文开始:

又因为分布式吵了起来,起因是知乎上这个问题的一个回答:

为什么分布式数据库忽然就火了呢?

https://www.zhihu.com/question/624263775/answer/3314424986

我无意介入分布式与非分布式的争端。任何分岐,本质上都是认知不足。经典成语盲人摸象,就形象的描绘了这种情况。

数据库是程序,是运行在CPU上的程序。分不分布的看使用场景,在这之前,我们先夯实开发程序的基础。今天主要说说这个夯实基础。

从这个问题入手:

https://www.zhihu.com/question/564882567/answer/3232682593

i ++  ++ i 性能有什么区别?

很冷门的问题。

因为答案一目了然,没有区别。

++运算符,是大学老师折磨我们的奇技淫巧,i ++  ++ i,没什么性能区别,都一样。

但在看MySQLPostgreSQL源码时,发现一个问题,它们都有类似如下组合:

lock得到某共享数据值某共享数据自增unlock

Oracle也有类似的代码。

这是一段伪码,主要是为了说明意思。

在加锁期间,MySQL、PG包括Oracle,都先得到数据,再自增,也就是:

lockvalue = i++unlock

   
   
   

为什么他们无一例外的选择 i++ ,而不是 ++i ? 是开发者的个人偏好?还是另有玄机?

分布式的理论固然重要,但我觉得i++++i这个问题比分布式更基础。

或者说,分布式更高级,是高层建筑。i++++i问题涉及的知识点,是地基。

我个人倾向于先夯实地基,再搭建上层建筑。但是,现在大环境,大家更关注上层建筑,就很容易导致地基不稳,这是我比较担心的。所以,才有了这个篇文章。更多文章,关注公众号:IT知识刺客

继续i++++i。普通情况下,这两个真没有性能差异。但也不是编译器的功劳啊,先用后加、先加后用,这在功能上是不同的,编译器可不会擅自修改功能性代码。它们两个没性能差异,是处理器的功劳,具体是怎么回事,先卖个关子,下面详细讨论。

既然先用后加、先加后用 都一样,为什么MySQLPG都选择先用后加(即:i++),而不是先加后用(即:++i)?

仅仅是个人偏好吗?

也不是。因为MySQL/PG这里的LockUnlock,使得先用后加(即:i++)成为一个独立的程序块,这才造成了先用后加(即:i++)和先加后用(即:++i)的差别。我们详细唠唠这块啊,我们把“先用后加(i++)”写为这样的伪码:


lock1):使用数据2):数据自增 unlock

这里,(1)和(2),没有依赖。(1)和(2)可以同时被CPU执行。

现代的处理器每个周期可以同时执行多条指令,这叫指令级并行,Instruction-level parallelism,IPL。所以, 样的写法(1)和(2)一个周期同时执行完毕,锁也被释放,锁只需要持续一个周期。

如果是这样:

lock1):数据自增2):使用数据unlock

和前面相比,(1)、(2)颠倒一下。但由于自增在前,要使用自增数据的话,要等待自增完毕,才能使用数据。也就是说,(2)是依赖(1)的。这就导致IPL失效,(1)和(2)无法并行执行,只能先执行(1),再执行(2),这就最少需要两个周期。锁,要至少持有两个周期。

这就是MySQL/PG为什么都选择先用后加(即:i++)的原因。

能不能来个程序测一下啊,这样光说不练的,没意思。

完整的程序,我附加在后面,先看最重要的部分:

这是先加后用(++i)的逻辑:
先加后用( ++i

为了突出“先加后用”的时间消耗,我在先加后用外面,加了一个循环。这段程序的效果就和下面的程序是一样的:

for(j=0; j<loopNum; j++)   ++*(shareMem);

重点就是循环中的“++ *(shareMem)“,++在前,先加后用。

再来看“先用后加(i++)“的:

先用后加(i++)

类似的,上面的嵌入汇编,效果如下:

for(j=0; j<loopNum; j++) *(shareMem)++;

属于典型的“先用后加(i++)”

之所以使用嵌入汇编,一是为了更好的控制指令流,二是避免涉及编译器问题,一旦涉及编译器,还要考虑编译器的原理等等吧。

下面,看看效果如何:

效果对比

Vage_++i,当然就是先加后用(++i)vage_i++,就是先用后加(i++)了。

从结果上看,无论是++i,还是i++,并无不同,都是35万多个周期。

说到这个周期,我再补充下,这是可以直接换算为时间的,如果是3GHz的主频,那就是3个周期1纳秒。如是5Ghz的主频呢,5个周期1纳秒。你自己换算吧。

我计时用的rdtscp(),是号称Intel CPU中最快最节省的计时器,它的代码实现如下:


4 rdtscp()实现

CPU中,有一组额外的、独立的电路,每个周期加1,用于计时,通常称为TSC计数器。因为它是独立的,因此可以不受其他因素影响的获得周期数。rdtscp/rdtsc指令,用于读取这个计时器的值。短时、高精度的计时器,通常都用TSC计数器实现。

但因为会受核的切换影响,所以长时间的计时,就不使用这个TSC了。

我们拐回来说结果吧,为什么i++++i并无不同?

这个结果并不意外。但是,不要说是这是编译器会优化的结果啊,我都上嵌入式汇编了,如果你还认为是编译器的功劳,唉,我也无话可说了。

按前文所述, 先加后用(++i),这种方式有依赖,要多一个周期才能执行完。为什么测试结果并没有显示多一个周期?

看下图:


5

图左,是先加后用(++i)的程序,但在CPU眼中,它其实是一系列的指令流。

我们眼中,程序像是二维的,有前有后,有循环有条件有分枝……。CPU是一维的,就是向前走,向前向前向前,我们的队伍向太阳……。

图中主要表达的意思,循环会变成一段段的指令流。

在这个基础上,为什么有依赖也不影响效率就好理解了,看下图:

图6 乱序执行 第一周期

图右边,第二条指令依赖第一条执行的结果,在第一个周期中,第二条指令不能执行。
没关系,那就跳过它,继续执行后续的指令。
假设CPU每一周期可以执行4条指令。第一周期,跳过第二条,执行第1、3、4、5条指令。
第二周期,如下图:

图7 乱序执行 第二周期

看图右,我用绿色字体表示执行完成,灰色表示还没执行,红色是正在执行。

在第二周期,第1、3、4、5条指令执行完成,开始执行2、6、7、8条指令。

你看,这就是乱序执行。

在乱序执行的规则下,第二条指令依赖第一条指令,完全不影响总的执行时间。

(免骂声明:图中所画,及文字说明,并不精确,主要为说明乱序的方式,请不要纠结细节。例如,sub和jne会宏合并为一条uOP, 分枝预测会导致执行流停在jne处等等。这些细节和本例的结论并不冲突,我将来另开系列详细解说。可持续关注公众号:IT知识刺客。喜欢我的内容请为我投放超赞包,为我打call~。)

既然不影响,还浪费我们时间干吗,Oracle/PG/MySQL中爱用i++,那就还是编码情习惯呗。

当然不是了,记得我们前面假设的场景吗,有一对lock/unlock:

lock1):数据自增 2):使用数据unlock

乱序执行不能穿越锁。锁,将++i的指令流封闭为独立的代码块,不能混在一起执行。

这个怎么测试一下?

So easy,只要在前面的测试代码中,加个屏障指令就可以了,lfence,或mfence,都行。(sfence不行,这个这里不展开讨论。)

我加了个lfence,下面是新的代码:

8-1 加屏障后的 ++i

图8-2 加屏障后的 i++

60行,有一个lfence,它可以挡住后面的指令不执行,也是CPU提供的三大屏障指令中的一个。

好,再来看效果吧:

图9 效果对比

各自执行了三遍。程序名字中的lf,是lfence之意。

先加后用(++i),550万多周期。

先用后加(i++),510万、520万多周期。

先用后加,i++,明显快了一些。少了30万多周期。

但因为这里主要的时间消耗在lfence指令上,提升也不是那么明显。

但结合没有lfence时的结果,仔细算算这笔帐,先用后加的提升还是明显的?

怎么算?如此这般:

1)、没有lfence时,10万次循环,先加后用(++i)、先用后加(i++),都是35万周期。35/ 10万,一次循环,大约3.5个周期。

2)、加了lfence之后,先加后用(++i),由于乱序不能穿越循环,多了30万周期。除以10万次循环,一次循环多了3个周期。

1)和(2)的结果结合,先加后用(++i),一次++i,将从3.5个周期,增加到6.5个周期。(3.5周期还包括了循环的指令)。

算了,不细算了,就算提升不多。在Oracle/PG/MySQL中,这种类型的代码,多是在锁当中。释放锁很多情况下,是要加屏障类操作的,十分符合我们测试案例中的情况。

而锁,对于OLTP型应用来说,特别是高别发的情况下,是非常重要的。

在讲课的时候,我常跟学生讲,没锁的时候,你别说多3个周期了,你多3000个周期,关系都不大。

3000周期,3GHz CPU1000纳秒,1微秒,还真没关系。

有锁的时候不一样,特别是全局唯一的那种锁,你将挡住所有人,这个时候你多一个周期,影响都很大。

打个比方,跑步,你自己跑,你多跑1公里,10公里,多少公里都行。

但你先持有了一把公有锁,然后跑步。你跑的时候全国人民都等着你,啥事不干等着你。等你跑完,锁释放,我们再开始干活。这个时候,你每多跑那怕一毫米,都是罪过。

好了,本篇完结了。本篇不是一个系列,只是有感而发,字数不多,3千多字而已。

我们正在开发PG Shared Pool模块,关于Shared Pool,其实大家有很多误解,下一篇好好说道说道。

无论怎么样吧,我们会按周期,逐段的分析代码。那怕结省一个周期,我认为也是值得的,因为我们是基础软件,你把基础交给我,你放心,我为你榨干CPU的每一个周期。

对了,提到讲课,说一说我的课。不是广告,公益性的。

明年开春,我主讲的北京大学《开源软件开发基础与实践 – PostgreSQL数据库内核》课程,又要开讲了。这个课程属于北京大学研一阶段课程,占3学分,是北京大学正式课程。

(我不是北大老师啊,我只是校外导师,学历太低,约等于高中肆业,无法到任何一所大学任教。至于高中肆业为什么能去北大讲数据库,这是另一个故事,这里不展开,我将来专门写一篇文章跟大家唠唠)

继续说课程,中国开源软件推进联盟PG分会,将这套北京大学的课程打造成为面向PG内核人员的认证:PGCH内核认证工程师。

课程难度很高,毕竟面向高等学府的,学费不贵,大概也就吃顿大餐的钱吧,想挑战的可以公众号私信我们。

「喜欢我的内容请为我投放超赞包,为我打call~」


本文分享自微信公众号 - 开源软件联盟PostgreSQL分会(kaiyuanlianmeng)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部