卡卷网
当前位置:卡卷网 / 每日看点 / 正文

为什么我感觉开发操作系统并不是件很难的事?

作者:卡卷网发布时间:2024-11-30 16:07浏览数量:118次评论数量:0次

当你在一堆现代裸硬件上(CPU、主板、显卡(有可能是集显)、显示器、硬盘……),点亮显示器,显示出 Hello Word 时,无论如何,这都是令人激动的。

如果满分是100,在没有OS支持的裸硬件上显示Hello word的难度,个人认为可以算60分。

你的感觉不错,写一个能玩的OS,真的并不难。如果时间许可,玩一玩这个也挺好的。

但,很多事情,你会发现,能做,和做好,是有区别的。而且区别很大。

写一个OS并不难。难在写一个好的OS。

打个比方,Linux原来的版本,有那种静态探针机制,类似:

void func(。。。) { if ( 探针 == 1 ) { 执行探针任务 } 继续 func() 函数 }

如果真如上面的示例代码。这个“探针”就算不打开,对性能的影响也很大。

因为多数情况下,探针是不激活的,那么 if () 就是单纯的跳过,跳到“继续func()函数“处,这种向下的跳跃,很容易分枝预测失败。

解决方法也很简单,加个unlikly:

void func(。。。) { if ( unlikly(探针 == 1) ) { 执行探针任务 } 继续 func() 函数 }

编译器现在知道 "探针==1” 大概率为False,会根据情况编译,比如,转为如下形式:

void func(。。。) { if ( (探针 != 1) ) { 继续 func() 函数 return ; } 执行探针任务 }

"if ( (探针 != 1) )"大概率为真,if 并不跳转,还是顺序往下执行。

不跳转的if,叫 “not taken 跳转”,CPU一周期可以执行两条以上这种中转,还不会影响和其他指令的并行性。

这样的探针,就算多加一些,对性能的影响也微乎其乎。

当然了,现在Linux内核又使用了更高端的探针,关注公众号“IT知识刺客”,有时间详说。

这里只是说明,写一个OS,有难度,但难度就算是适中吧。而要写一个好的OS,难度属于没边,难到天际那种。

再举一个例子,我提一个简单问题,i++,与++i,那种写法更优、如果是你,会会选择那种写法。

是不是有种“茴香豆”四种写法的感觉!

放心,我不是大学老师,我不会来一串奇怪的++(++)++……。

我把条件再限制一下,比如在一个多并发的系统中,变量 i 是一个公共内存变量,要得到 i 的值,并对 i 加一,为了串行化这个操作,要这样写:

lock value = i++ unlock

当然,也可以这样写:

lock value = ++i unlock

那么,i++好,还是++i好呢?

锁这个动作,对CPU而言,是相当于内存屏障的。++ 的前后有了“屏障”类指令,这使得从lock到unlock,成为一个相对独立的程序块。这是首先要了解的。

然后,我们再来说说++,以“value = i++"为例 ,它相当于如下伪码:

lock (1):使用数据(读取 i 的值) (2):数据自增 (i++) unlock

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

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

如果是 ++i 呢,它的伪码如下:

lock (1):数据自增 (i++) (2):使用数据 (读取 i 的值) unlock

和前面相比,(1)、(2)颠倒一下。

由于自增在前,要使用自增后 i 的话,要等待自增完毕,才能读取 i 。也就是说,(2)是依赖(1)的。这就导致IPL失效,(1)和(2)无法并行执行,只能先执行(1),再执行(2),这就最少需要两个周期。锁,要至少持有两个周期。

当然,多一个周期,也不叫啥事。但你不是想写操作系统吗,基础软件,对性能是相当敏感的。还有数据库,都是性能敏感型软件。

如果是写上层应用软件,别说多一个周期了,你多10分钟,我顶多出门抽根烟。

上面说的i++与++i区别,是可以测量的,这是我在x64平台上的测试程序:

为什么我感觉开发操作系统并不是件很难的事?  第1张

图1 先加后用(++i)

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

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

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

然后在循环的前后,使用rdtscp,读取CPU的TSC,这是一个高精度时钟,返回CPU的时钟周期。后面还会介绍。

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

为什么我感觉开发操作系统并不是件很难的事?  第2张

图2 先用后加 (i++)

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

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

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

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

下面,看看效果如何:

为什么我感觉开发操作系统并不是件很难的事?  第3张

图3 效果对比

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

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

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

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

为什么我感觉开发操作系统并不是件很难的事?  第4张

图4 rdtscp()实现

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

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

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

这个结果并不意外。但是,不要说是这是编译器会优化的结果啊,我都上嵌入式汇编了,就是为了排除编译器的影响。

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

看下图:

为什么我感觉开发操作系统并不是件很难的事?  第5张

图 5

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

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

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

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

为什么我感觉开发操作系统并不是件很难的事?  第6张

图 6 乱序执行 第一周期

图右边,第二条指令依赖第一条执行的结果,在第一个周期中,第二条指令不能执行。

没关系,那就跳过它,继续执行后续的指令。

假设CPU每一周期可以执行4条指令。第一周期,跳过第二条,执行第1、3、4、5条指令。

第二周期,如下图:

为什么我感觉开发操作系统并不是件很难的事?  第7张

图 7 乱序执行 第二周期

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

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

你看,这就是乱序执行。

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

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

既然i++,++i都一样,还浪费我们时间干吗?

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

lock (1):数据自增 (2):使用数据 unlock

乱序执行不能穿越“屏障”类指令。而锁,查当于内存屏障。它将++i的指令流封闭为独立的代码块,不能混在一起执行。

这个怎么测试一下?

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

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

为什么我感觉开发操作系统并不是件很难的事?  第8张

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

为什么我感觉开发操作系统并不是件很难的事?  第9张

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

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

好,再来看效果吧:

为什么我感觉开发操作系统并不是件很难的事?  第10张

图 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周期还包括了循环的指令)。

算了,不细算了,就算提升不多,做为基础软件,不过是++放哪儿,顺手做的事,为什么不选择性能更好的。前提是,要有这样的知识储备。

END

免责声明:本文由卡卷网编辑并发布,但不代表本站的观点和立场,只提供分享给大家。

卡卷网

卡卷网 主页 联系他吧

请记住:卡卷网 Www.Kajuan.Net

欢迎 发表评论:

请填写验证码