深入计算机系统看性能优化

一.引言

"性能优化",从计算机诞生之初就一直伴随着计算机技术的发展,直到现在。将来也必定不会消失。这是因为每个人都会追求性价比,花最少的钱,办最多的事。生活中也一样,就比如说泡茶,但凡有点常识的人都不会先洗茶杯,再去烧水,而是先去烧水,在等水开的过程中,去做洗茶杯等工作。这也是一种优化。

二.访问寄存器代替内存引用

我们先看一个例子:

有这么两个程序:它们的目的就是将数组x中的数,按照下标累加到数组y中,最后在把数组y中的数据累加到一个数dest里面。为了验证效果,我们将这个过程重复10000遍。

Prog 1 Prog2

这两个程序的区别就在Prog2中红框里面的内容。那么哪个程序运行的更快呢?

话不多说,我们看实际的结果:

这里为了说明效果,我们编译的时候,并没有采用优化(编译优化,确实可以提高程序运行的效率,但是过高的编译优化等级会有一定的副作用,另外编译器优化也具有一定的局限性,高效的代码仍然应该是我们追求的目标)。可以看到,Prog2要明显比Prog1快。

要想理解上面的例子,我们必须先介绍一下寄存器和汇编代码的相关知识:

1.寄存器

CPU内部用来存放数据的一些小型存储区域, 注意寄存器是在CPU内部,受限于CPU的物理尺寸,寄存器数量不会太多。我们只需要记住两点:

  1. 寄存器和CPU的L1 cache相比,速度虽然还在一个数量级,但是L1 cache的访问速度还是要慢几倍。具体的数据见下文表2

  2. CPU只能从寄存器直接取数据或者指令,如果取不到,获取的顺序是L1->L2->L3->主存->磁盘。

从下文表2中可以看出,如果cpu的cache访问miss了,性能损失还是很大的。如果内存里面再miss了,那对性能来说不亚于一场灾难了。

计算机访问速度分级:

表1 时间单位

以3.3GHz的CPU为例:

表2 系统的各种延时

正如你所见,CPU周期的时间非常短,这段时间,光的速度大约只能走0.5米。想象一下,是不是非常震撼?

x86-64 CPU的整数寄存器:

我们无需刻意去记住这些寄存器的名称,不同架构的寄存器的数量和名称也不一样,我们只要知道他们是cpu内部的效率极高的存储单元即可。

回到前面的例子,为什么Prog2要比Prog1快,是因为Prog2里面用DEST这个局部变量代替了*dest。DEST是一个局部变量,在汇编指令里是直接访问寄存器,而*dest则需要去访问内存cache。

2.汇编代码简介

说到汇编语言的产生,首先要讲一下机器语言。机器语言是机器指令的集合。机器指令展开来讲就是一台机器可以正确执行的命令。电子计算机的机器指令是一列二进制数字。计算机将之转变为一列高低电平,以使计算机的电子器件受到驱动,进行运算。

上面所说的计算机指的是可以执行机器指令,进行运算的机器。这是早期计算机的概念。在我们常用的PC机中,有一个芯片来完成上面所说的计算机的功能。这个芯片就是我们常说的CPU(Central Processing Unit,中央处理单元)。每一种微处理器,由于硬件设计和内部结构的不同,就需要用不同的电平脉冲来控制,使它工作。所以每一种微处理器都有自己的机器指令集,也就是机器语言。

早期的程序设计均使用机器语言。程序员们将用0, 1数字编成的程序代码打在纸带或卡片上,1打孔,0不打孔,再将程序通过纸带机或卡片机输入计算机,进行运算。这样的机器语言由纯粹的0和1构成,十分复杂,不方便阅读和修改,也容易产生错误。

程序员们很快就发现了使用机器语言带来的麻烦(何止是麻烦,简直令人发狂),它们难于辨别和记忆,给整个产业的发展带来了障碍,于是汇编语言产生了。

汇编语言的主体是汇编指令。汇编指令和机器指令的差别在于指令的表示方法上。汇编指令是机器指令便于记忆的书写格式。

我们举个例子看下:

源代码:

汇编代码和机器码:

可以看到汇编代码,好歹还有几个能猜出意思的单词。

有没有觉得现在的程序员还是挺幸福的。

资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linuxc/c++高级开发【直播公开课】

零声白金VIP体验卡:零声白金VIP体验卡(含基础架构/高性能存储/golang/QT/音视频/Linux内核)

3.汇编指令简介

汇编语言是计算机语言的一种,是一种低级语言。相比高级语言,汇编语言更接近底层硬件,使用更加直接,效率更高。但相对而言,汇编语言更加复杂,语法更加严格。

  • 操作数指示符:

大多数指令有一个或多个操作数,指示出执行一个操作中要使用的源数据值。

操作数一般可以分为三类:

立即数

寄存器

内存引用

  • 数据传送指令:

最频繁使用的指令,负责将数据从一个位置复制到另一个位置。

例如:

mov %rbx, %rax : 将rbx寄存器的值移动到rax寄存器

mov %rbx, (%rax) : 将rbx寄存器的值移动到rax寄存器所表示的内存地址中

以及mov指令的一些扩展指令: movb, movw, movl, movq等等

  • 压入和弹出栈数据

将数据压入程序栈中,以及从程序栈中弹出数据。

push %rbp : 将%rbp寄存器的值压入程序栈指针指向的位置

pop %rbp : 将栈指针指向的数据弹出,放入%rbp寄存器

  • 算数和逻辑操作
  • 跳转指令

导致执行切换到程序中一个全新的位置

jmp %rax 用寄存器%rax中的值作为跳转目标

jmp是无条件跳转,还有一些条件跳转指令,有兴趣的同学可以查一下资料。

  • 比较和测试指令

比较指令CMP,只设置条件码,不更新目的寄存器,其余的行为和SUB指令一致

测试指令TEST,只设置条件码,不更新目的寄存器,其余的行为和ADD指令一致

更多详细的内容,可以通过相关书籍进行系统性的了解,鉴于篇幅,就不多介绍了。

让我们回到刚才的例子,可以看到由于把内存引用替换成了访问寄存器,程序性能就有了明显的提升。

三.帮助提高CPU分支跳转的正确率

我们还是先看一个例子:

看上面的两个函数,它们都是calloc一个全零数组x(这里不能直接用数组赋值,否则编译器会足够聪明进行自动的优化),遍历x中的每个数,如果等于0,执行分支A,否则执行分支B。

唯一的不同就是在分支判断的时候,prog2.c加了likely。我们先看下实际的结果如何:

可以看出,加了likely的prog2,明显用时变短。原因何在?

为了理解上面的例子,我们先介绍CPU流水线相关知识:

3.1. CPU流水线简介

CPU流水线是一种使用多级缓存来提高处理器性能的技术。它是指将CPU操作分为多个阶段,每个阶段单独完成一个操作,然后将结果传递给下一个阶段,以此类推。每个阶段都有一个独立的部件,并且所有部件都能同时处理不同的指令。现代CPU都会采用这种技术来提高CPU的运行效率。

CPU流水线通常包括以下五个阶段:

1)取指令(Instruction fetch):从存储器中读取指令。

2)指令译码(Instruction decode):将指令转换为可执行的指令。

3)执行指令(Instruction execute):执行指令的操作。

4)写回(Write back):将执行指令得到的结果写回内存中。

5)更新程序计数器(Update program counter):将程序计数器加1,使它指向下一个指令。

举个简单的例子:

我们假设每一个步骤执行时间都是一个时钟周期,那么一条指令执行需要3个时钟周期

CPU 执行指令的3个时钟周期里,取值单元只在第一个时钟周期里工作,其余两个时钟周期都处于空闲状态,其它两个执行单元也是如此,效率太低了。

解决方法就是引入流水线,引入流水线工作模式后可以看到,除了刚开始第一个时钟周期大家还可以偷懒外,其余的时间都不会闲着

CPU流水线的优点是可以同时执行多个指令,从而提高了处理器的效率。但它也存在一些问题,例如数据相关性(Data dependency)和控制相关性(Control dependency),这些问题可能导致流水线停滞,降低CPU的性能。

执行的程序指令如果是顺序结构,没有中断或跳转,流水线确实可以提高执行效率。但是当程序指令中存在跳转、分支结构时,下面预取的指令可能就要全部丢掉了,需要到要跳转的地方重新取指令执行。一般来说分支预测错误的处罚大约是19个时钟周期。(具体计算方法这里不做详细介绍了)。

我们看下前面提到的例子汇编出来的结果:

prog2,这里汇编是"jne",意思是如果判断结果不为0,就跳转到地址 800 的地方执行。我们知道这里的判断一直是0。所以,cpu指令顺序向下执行,并不会发生预判错误,预取的指令也不会丢弃。这样就不会遭到分支预测错误的惩罚,效率会提高。

所以有些情况下,当我们根据实际的情况可以判断出哪条分支的可能性更高的时候,我们就可以站在上帝视角给予一定的提示,这样就可以降低分支预测错误,减少CPU的无用功了,从而可以有效的提高性能,同时也节省了功耗。

四.总结

从计算机系统的角度来看,性能优化的几个思考方向:

  1. 尽可能高效的获取数据

  2. 尽可能减少CPU的无用功。

  3. 尽可能在有限的时间内,让CPU干更多的有效的事情。

  4. 完成同样的工作任务,尽可能的让CPU少做事。

原文作者:内核工匠

相关推荐
前端青山6 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青云交12 小时前
大数据新视界 -- 大数据大厂之 Impala 性能优化:应对海量复杂数据的挑战(上)(7/30)
大数据·性能优化·impala·数据分区·查询优化·海量复杂数据·经典案例
chusheng184015 小时前
Python 爬取大量数据如何并发抓取与性能优化
开发语言·python·性能优化
XMYX-01 天前
MySQL 性能优化策略:提升响应速度与系统稳定性
mysql·性能优化
PangPiLoLo2 天前
高可用架构-业务高可用
java·性能优化·架构
尸僵打怪兽2 天前
软考(中级-软件设计师)数据库篇(1101)
数据库·oracle·性能优化·软考
程序猿进阶2 天前
系统上云-流量分析和链路分析
java·后端·阿里云·面试·性能优化·系统架构·云计算
飞腾开发者3 天前
飞腾平台Arm ComputeLibrary编译安装指南
linux·服务器·arm开发·后端·性能优化
新知图书3 天前
MySQL 9从入门到性能优化-系统信息函数
数据库·mysql·性能优化
Databuff4 天前
JVM性能优化实战手册:从监控到调优策略
linux·运维·jvm·性能优化·自动化·devops