计算机内存调用优化,深入理解计算机系统——优化程序性能学习笔记

本文探讨了编译器如何在确保代码行为安全的前提下,通过优化循环结构、减少内存引用、循环展开和并行计算等手段,提升程序性能。还涉及内存性能、寄存器溢出、分支预测以及性能瓶颈识别技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

文章目录

优化编译器的能力和局限性

表示程序性能

消除循环的低效率

减少过程调用

消除不必要的内存引用

理解现代处理器

整体操作

循环展开

提高并行性

多个累计变量

一些限制因素

寄存器溢出

分支预测和预测错误处罚

理解内存性能

应用:性能提高技术

确认和消除性能瓶颈

程序剖析

优化编译器的能力和局限性

编译器只对程序进行安全的优化,尽量不改变程序的行为。

例如有以下两个函数: void twiddle1(long *xp, long *yp)

{

*xp += *yp;

*xp += *yp;

}

void twiddle2(long *xp, long *yp)

{

*xp += 2* *yp;

}

当指针xp和yp不相等的时候,函数1的效率比函数2的效率高:函数2要求3次内存引用(读*xp, 读 *yp,读*xp),而函数1需要6次(2次读*xp, 2次读 *yp,2次读*xp)。

但是当指针xp等于指针yp时,两个函数的结果就不相同,所以当编译器优化的时候就会进行安全的优化,不会使用函数2的形式。

两个指针指向同一个位置的情况称为内存别名使用,编译器在优化的时候必须考虑到这种情况。

表示程序性能

度量标准每元素的周期数(Cycles Per Element, CPE),作为一种表示程序性能的并指导我们改进代码的方法。

时钟周期的时间是时钟频率的倒数,以纳秒或皮秒为单位,例如一个4GHz的时钟其周期为0.25纳秒。

程序性能一般用一个时钟周期执行多少条指令来衡量。

消除循环的低效率

循环条件中使用求长度的函数改为将该函数的值赋值给一个局部变量,这种优化称为代码移动。

9eab78d2976b99f9d7df59317ebc3c87.png

比如说见一个字符串中的所有字母转换为小写字母:

9d9cf8ebdac7339273a00c3b0203a004.png

当字符串越长lower1与lower2的效率差距越大:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xf4CEu3t-1596628543178)(4e39aae6f70fc94208b5353ecee73ff1.png

)]

减少过程调用

过程调用会带来开销,减少循环过程中的函数调用,使用替代的方法。

将图5-6中的获取数组元素的函数使用替代方法:假设get_vec_element方法来获取下一个元素。

25393383caff32fb39542ff30dc6412a.png

消除不必要的内存引用

上边减少了过程调用但是并没起到显著的优化,因为是循环中对内存的引用占用程序大部分的性能。

08e03325a153b8a123b1aaa772729dda.png

理解现代处理器

代码级上看上去是一次执行一条指令,但是在处理器中是同时对多条指令求值,称为指令级并行。

两种下界描述了程序的最大性能:

当一系列操作必须按照严格顺序执行时,就会遇到延迟界限——咋下一条指令开始之前,这条指令必须结束。

当代码中的数据相关限制了处理器利用指令级并行的能力时,延迟界限能够限制程序性能。

吞吐量界限——处理器功能单元的原始计算能力。这个界限是程序性能的终极界限。

整体操作

超标量:处理器可以在每个时钟周期执行多个操作,而且是乱序的——指令执行的顺序不一定要与它们在机器级程序中的顺序一致。

这种处理器有两个主要部分:

指令控制单元:(ICU):负责从内存中读出指令序列,并根据这些指令序列生成一组针对程序数据的基本操作。

执行单元:(EU):执行操作。

e1c1812c2af4e6d2ccef04fc58f244af.png

ICU从指令高速缓存中读取指令进行指令译码,此时可能会遇到指令分支,现代处理器使用分支预测技术,猜测是否会执行分支并预测分支的目标地址。

退役单元记录正在进行的处理,指令译码时,关于指令的信息被放置在一个队列中,当该指令的操作完成并且分支预测结果正确,该指令就可以退役了,退役单元中的寄存器文件的更新也会被执行。

如果分支预测错误,那么这条指令会被清空,丢弃所有计算出来的结果,取值控制会选取另一个方向上的指令。

读写内存是由加载和存储单元实现的,通过数据高速缓存来访问内存。

操作结果负责执行单元中各个单元间的数据交互。

循环展开

通过增加每次迭代计算元素的数量,减少循环的迭代次数。

使用“2X1”循环展开之前的代码:

第一个循环每次处理数组的两个元素

第二个循环处理最后几个元素(剩余元素向量长度不为2的倍数),每次处理一个元素

8e2b542e67d0a27bf292deaa2fe23c24.png

提高并行性

多个累计变量

通过将一组合并运算分割成两个或更多的部分,并在最后合并结果来提高性能。

如求a0到an的乘积拆分成求索引值为偶数的元素的乘积乘上索引值为奇数的元素的乘积。

索引值为奇数的元素累积在变量acc0中,索引值为奇数的元素累积在变量acc1中,因此称为“2X2循环展开”。

1b4224561e4507bc0ebf0d1784e1035f.png

一些限制因素

寄存器溢出

并行度超过了可用寄存器数量,编译器会诉诸溢出,将某些临时值存放到内存中,通常是在运行时堆栈上分配空间。

但是x86-64有足够多的寄存器,大多数循环在出现寄存器溢出直线就将达到吞吐量限制。

分支预测和预测错误处罚

不要过分关心可预测的分支

​ 错误的分支预测影响可能会非常大,但是现代处理器中的分支预测逻辑非常善于辨别不同的分支指令的有规律的模式和长期的趋势。

书写适合用条件传送实现的代码

​ 分支预测只对有规律的模式可行。对于依赖于数据的任意特性的预测是不可行的,比如说判断一个数是正数还是负数。

​ 对于这种情况如果编译器能够产生使用条件数据传送而不是使用条件控制转移的代码,将会极大地提高程序性能。

​ 如对于两个整数数组a,b将数组a[i]设置为a[i]和b[i]中较小的那一个,而将b[i]设置为两者中较大的那一个。

条件控制转移的代码:

492f47f32e8e41d434fab4b598bc9eda.png

条件数据传送的代码:

56af46ee33ae870cb1fc80040511bf0e.png

理解内存性能

加载:从内存读到寄存器

存储:从寄存器写到内存

现代处理器有专门的功能单元来执行加载和存储操作,这些单元有内部的缓冲区来保存未完成的内存操作请求集合。

应用:性能提高技术

高级设计。为遇到的问题选择适当的算法和数据结构

基本编码原则。避免限制优化的因素

消除连续的函数调用。在可能时,将计算移到循环外。

消除不必要的内存引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。

低级优化。结构化代码以利用硬件功能。

展开循环,降低开销,并且使得进一步的优化成为可能。

通过使用例如多个累积量变量和重新结合等技术,找到方法提高指令级并行。

用功能性的风格重写条件操作,使得编译采用条件数据传送。

确认和消除性能瓶颈

在处理大程序的时候很难定位需要优化的地方,就需要使用到代码剖析程序。

程序剖析

程序剖析可以在现实的基准数据上运行实际程序的同时,进行剖析。

剖析程序GPROF可以确定每个函数花费多少CPU时间。计算每个函数被调用的次数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值