前文 <一行机器指令感觉下内存操做到底有多慢> 中,咱们体验到了 CPU 流水线阻塞带来的数量级性能差别。当时只是根据机器码,分析推断出来的,此次咱们作一些更小的实验来分析验证。segmentfault
动手以前,咱们先了解一些背景。在 \<CPU 提供了什么> 一文中介绍过,CPU 对外提供了运行机器指令的能力。那 CPU 又是如何执行机器指令的呢?缓存
一条机器指令,在 CPU 内部也分为好多个细分步骤,逻辑上来讲能够划分为这么五个阶段:性能优化
例如连续的 ABCD 四条指令,CPU 并非先完整的执行完 A,才会开始执行 B;而是 A 取指令完成,则开始解析指令 A,同时继续取指令 B,依次类推,造成了流水线做业。并发
理想状况下,充分利用 CPU 硬件资源,也就是让流水线上的每一个器件,一直保持工做。然而实际上,由于各类缘由,CPU 无法完整的跑满流水线。分布式
好比:函数
400553
的指令。je 400553
对于这种分支指令,CPU 有分支预测技术,基于以前的结果预测本次分支的走向,尽可能减小流水线阻塞。性能
r8
依赖于第一条指令的结果。mov r8,QWORD PTR [rdi] add r8,0x1
这种时候,CPU 会利用操做数前推技术,尽可能减小阻塞等待。学习
现代复杂的 CPU 硬件,其实也不仅有一条 CPU 流水线。简单从逻辑上来理解,能够假设是有多条流水线,能够同时执行指令,可是也并非简单的重复整个流水线上的全部硬件。测试
多发射能够理解为 CPU 硬件层面的并发,若是两条指令没有先后的顺序依赖,那么是彻底能够并发执行的。CPU 只须要保证执行的最终结果是符合指望的就能够,其实不少的性能优化,都是这一个原则,经过优化执行过程,可是保持最终结果一致。优化
理论须要结合实践,有实际的体验,才能更清晰的理解原理。
此次咱们用 C 内联汇编来构建了几个用例来体会这其中的差别。
#include <stdio.h> void test(long *a, long *b, long *c, long *d) { __asm__ ( "mov r8, 0x0;" "mov r9, 0x0;" "mov r10, 0x0;" "mov r11, 0x0;" ); for (long i = 0; i <= 0xffffffff; i++) { } __asm__ ( "mov [rdi], r8;" "mov [rsi], r9;" "mov [rdx], r10;" "mov [rcx], r11;" ); } int main(void) { long a = 0; long b = 0; long c = 0; long d = 0; test(&a, &b, &c, &d); printf("a = %ldn", a); printf("b = %ldn", b); printf("c = %ldn", c); printf("d = %ldn", d); return 0; }
咱们用以下命令才执行,只须要 1.38
秒。
注意,须要使用 -O1
编译,由于 -O0
下,基准代码自己的开销也会很大。
$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c $ time ./asm a = 0 b = 0 c = 0 d = 0 real 0m1.380s user 0m1.379s sys 0m0.001s
以上的代码,咱们主要是构建了一个空的 for
循环,能够看下汇编代码来确认下。
一下是 test
函数对应的汇编,确认空的 for
循环代码没有被编译器优化掉。
000000000040052d <test>: 40052d: 49 c7 c0 00 00 00 00 mov r8,0x0 400534: 49 c7 c1 00 00 00 00 mov r9,0x0 40053b: 49 c7 c2 00 00 00 00 mov r10,0x0 400542: 49 c7 c3 00 00 00 00 mov r11,0x0 400549: 48 b8 00 00 00 00 01 movabs rax,0x100000000 400550: 00 00 00 400553: 48 83 e8 01 sub rax,0x1 // 在 -O1 的优化下,变成了 -1 操做 400557: 75 fa jne 400553 <test+0x26> 400559: 4c 89 07 mov QWORD PTR [rdi],r8 40055c: 4c 89 0e mov QWORD PTR [rsi],r9 40055f: 4c 89 12 mov QWORD PTR [rdx],r10 400562: 4c 89 19 mov QWORD PTR [rcx],r11 400565: c3 ret
此次咱们在 for
循环中,加入了 “加一” 和 “写内存” 的两条指令。
for (long i = 0; i <= 0xffffffff; i++) { __asm__ ( "add r8, 0x1;" "mov [rdi], r8;" ); }
本次执行时间,跟基础测试基本无差异。
说明新加入的两条指令,和基准测试用的空 for
循环,被“并发” 执行了,因此并无增长执行时间。
$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c $ time ./asm a = 4294967296 b = 0 c = 0 d = 0 real 0m1.381s user 0m1.381s sys 0m0.000s
这个例子,也就是上一篇中优化 LuaJIT 时碰到的状况。
新加入的内存读,跟原有的内存写,构成了数据依赖。
for (long i = 0; i <= 0xffffffff; i++) { __asm__ ( "mov r8, [rdi];" "add r8, 0x1;" "mov [rdi], r8;" ); }
再来看执行时间,此次明显慢了很是多,是的,流水线阻塞的效果就是这么感人😅
$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c $ time ./asm a = 4294967296 b = 0 c = 0 d = 0 real 0m8.723s user 0m8.720s sys 0m0.003s
此次咱们加入另外三组相似的指令,每一组都构成同样的数据依赖。
for (long i = 0; i <= 0xffffffff; i++) { __asm__ ( "mov r8, [rdi];" "add r8, 0x1;" "mov [rdi], r8;" "mov r9, [rsi];" "add r9, 0x1;" "mov [rsi], r9;" "mov r10, [rdx];" "add r10, 0x1;" "mov [rdx], r10;" "mov r11, [rcx];" "add r11, 0x1;" "mov [rcx], r11;" ); }
咱们再看执行时间,跟上一组几乎无差异。由于 CPU 的乱序执行,并不会只是在那里傻等。
反过来讲,其实流水线阻塞也不是那么可怕,有指令阻塞的时候,CPU 仍是能够干点别的。
$ gcc -masm=intel -std=c99 -o asm -O1 -g3 asm.c $ time ./asm a = 4294967296 b = 4294967296 c = 4294967296 d = 4294967296 real 0m8.675s user 0m8.674s sys 0m0.001s
现代 CPU 几十亿个的晶体管,不是用来摆看的,内部其实有很是复杂的电路。
不少软件层面常见的优化技术,在 CPU 硬件里也是有大量使用的。
流水线,多发射,这两个在我我的看来,是属于很重要的概念,对于软件工程师来讲,也是须要能深刻理解的。不只仅是理解这个机器指令,在 CPU 上是如何执行,也是典型的系统构建思路。
最近有学习一些分布式事务的知识,其实原理上跟 CPU 硬件系统也是很是的相似。
多动手实践,仍是颇有好处的。