如何使用高级语言在机器语言层面提高程序运行效率

  • ==基础编码原则==
    • 利用局部性原理减少过程调用
    • 利用局部性原理消除不必要的内存引用
  • ==低级优化==
    • 指令级并行
      • 寄存器重命名
    • 循环展开
      • 利用多个累计变量
      • 重新结合变换
    • 触发条件传输

下面我将对一个示例程序进行一系列的优化带各位走过利用高级语言【C语言】指导编译器编译高效率代码的路程。
本文分为两章。
第一章《基础编码原则》利用局部性原理编码提高程序效率。
第二章《低级优化》利用编码手段最大化利用处理器资源。
本文没有具体的汇编代码【本来想写,但嫌麻烦】,大家放心食用

基础编码原则

本章我们将利用局部性原理,最大化利用寄存器来编码,使程序减少内存访问读写,以及减少过程【函数】调用来减轻处理器由分配资源和ret指令引起的处理器时钟周期浪费。

利用局部性原理减少过程调用

下面是一段计算向量元素和的代码

对向量内元素求和 利用这个宏定义

#define IDENT 0
#define OP +

对向量内元素求积 利用这个宏定义

#define IDENT 1
#define OP *

下面的代码是对向量的各个操作:【下面示例函数中会调用这两个函数】

/**定义一个向量结构体**/
typedef struct{long len;data_t *data;
} vec_rec, *vec_ptr;/**程序中各函数的定义**/
//获取向量中下标为index的元素,返回结果给dest所指向的内存单元
int get_vec_element(vec_ptr v, long index, data_t* dest)
{if(index < 0 || index >= v->len) return 0;*dest = v->data[index];return 1;
}//返回向量长度
long vec_length(vec_ptr v)
{return v->len;
}

待优化示例代码:

void combine1(vec_ptr v , data_t* dest)
{long i;*dest = IDENT;for(i = 0; i < vec_length( v ); i++){data_t val;get_vec_element( v , i , &val);*dest = *dest OP val;}
}

上面给出的代码中,for循环每次循环都会调用一次vec_length( )函数来访问向量元素个数,而每次调用vec_length( )都会对内存进行一次访存操作,并且还面临对程序栈的各项修改,还有ret带来的处理器三个周期的浪费1。但由于向量长度不会因为访问一次向量元素就发生改变,而每次for循环都要进行一次vec_length( )函数调用,带来大量不必要的处理器周期浪费。

下面给出经过修改的代码:

void combine2(vec_ptr v , data_t* dest)
{long i;*dest = IDENT;long length = vec_length(v);//在此处加入一个局部变量lengthfor(i = 0; i < length; i++){data_t val;get_vec_element( v , i, &val);*dest = *dest OP val;}
}

经过修改的代码中,在combine1函数内增加了一个局部变量length用来记录向量长度,这样就不必每次for循环都调用一次vec_length( )函数,避免了反复调用函数带来的处理器不必要消耗。
局部变量,在处理器执行过程中一般处于程序栈中或者处理器更愿意利用寄存器存储局部变量,这是由编译器决定的,一般当没有空闲的寄存器的时候才会用栈来存储局部变量,因为这个局部变量【length】被反复访问,因此编译器会将其用更加高速的寄存器存储。于此,提高了代码的局部性。

我们发现,combine2中还有一个函数,get_vec_element( v , i, &val),其反复访问内存,给出以下优化代码

//函数返回向量的首地址
data_t* get_vec_start(v)
{return v->data;
}void combine3(vec_ptr v , data_t* dest)
{long i;*dest = IDENT;long length = vec_length(v);data_t* data = get_vec_start(v);//创建一个局部变量data指针指向 向量首地址,利用直接访存操作代替通过函数访问向量元素for(i = 0; i < length; i++){*dest = *dest OP data[i];}
}

如此,我们利用一个指向向量首地址的指针,使用指针直接访存的方法,消除了反复调用get_vec_element( )函数的处理器浪费。

利用局部性原理消除不必要的内存引用

上面优化到combine3,我们利用局部性原理,减少了函数调用带来的处理器浪费。
接下来我们将学习如何利用局部性消除不必要的内存引用。
重点: 我们观察combine3,发现for循环中,dest指针指向的地址被反复读取与写入,我们需要减少内存读写,因为每次内存读写都需要使用非常多的时钟周期,从而降低程序运行效率,哪怕是高速缓存也至少需要4个时钟周期,下图展示了处理器使用内存的顺序。

我们知道,处理器访存顺序为:寄存器 === 》L1缓存 ===》L2缓存 ===》L3缓存 ===》内存 ===》固态硬盘/机械硬盘 ===》网络磁盘
如下图所示:

下一级存储作为上一级存储的缓存。所以当高速缓存不命中的时候缓存会往内存提取一个数据块,因为局部性原理,往往访问的都是刚刚访问的内存附近的内存,所以直接传入数据块可以提高缓存命中率并且降低访问内存的次数。
那么,combine3的代码中,dest指针的内存读写如何消除呢?
示例代码:

data_t* get_vec_start(v)
{return v->data;
}void combine4(vec_ptr v , data_t* dest)
{long i;long length = vec_length(v);data_t* data = get_vec_start(v);data_t acc = IDENT;//创建一个局部变量acc用于累计计算结果for(i = 0; i < length; i++){acc = acc OP data[i];}*dest = acc;//最后将累计的结果赋值给dest指针指向的内存
}

combine4使用一个局部变量acc累积计算结果,跟局部变量length一样,编译器会利用一个寄存器来表示acc,这样就降低了访存次数,只在函数的最后 将累积结果返回给dest指向的内存单元。
那data[i]这个访存次数怎么降低呢?【请看低级优化章】

低级优化

本章我们将从处理器最底层硬件走入最大化利用处理器资源的康庄大道。
主要分为:1.突破延迟界限,2.接近吞吐量界限
我们的目标:使程序接近吞吐量界限
首先,我们预普及一下处理器资源:【参考处理机:Intel Core i7 Haswell】

处理器分为 指令控制单元 和 指令执行单元
指令控制单元: 负责从内存中读出指令序列,并根据指令序列生成一组针对程序数据的基本操作
指令执行单元: 负责执行指令控制单元生成的基本操作
我们这里讨论的是执行单元中的功能单元部分,因为利用的处理器资源主要是功能单元的算数运算单元和加载单元

指令执行单元 = 功能单元 + 缓存

功能单元可以并行的执行多个操作,而不必顺序执行,并且执行结果严格遵循顺序执行的结果
功能单元分为以下八个单元:

    1. 整数运算、浮点乘、整数和浮点除、分支
    1. 整数运算、浮点加、整数乘、浮点乘
    1. 加载、地址计算
    1. 加载、地址计算
    1. 存储
    1. 整数运算
    1. 整数运算、分支
    1. 存储,地址计算

读写内存是由加载、存储单元实现的,加载单元负责读取,存储单元负责写入。
处理器利用分支单元实现:检查分支预测是否正确,若发现分支预测错误,那么该单元则广播给其他单元预测错误的信息,并命令其他正在运算的单元清空已经运算的错误的内容。
整数运算包括:整数加、位级操作、移位运算

我们看到,功能单元的这种组合具有同时执行多同类型操作的能力,如:

  1. 同时执行四个整数加
  2. 同时执行两个浮点乘
  3. 同时加载两个内存数据

有了以上的处理器功能单元基础,我们就可以充分利用处理器资源来编写代码了

指令级并行

我们发现,跟更新寄存器的操作是在执行单元执行结束了之后的操作,那么当连续多次写同一个寄存器的时候就可以在经过多个执行单元的操作的最后再将寄存器更新了,这就是实现指令级并行的方法之一。控制操作数在执行单元间传送的机制叫“寄存器重命名”。

寄存器重命名

当一条更新寄存器 r 指令译码时,产生标记 t ,得到一个指向该操作结果的唯一标识符。
条目( r , t )被加入到一张表中,该表维护每个程序寄存器 r 与会更新该寄存器的操作的标记 t 之间的关联。
当随后以寄存器 r 作为操作数的指令译码时,发送到执行单元的操作包含 t 作为操作数源的值。
当某个执行单元完成第一个操作时,会产生一个结果 (v , t ),其指明标记为 t 的操作产生值 v 。
所有等待 t 作为源的操作都能够使用 v 作为源值,这也是一种形式的数据转发。
通过寄存器重命名这一机制,寄存器值可以从一个操作直接转发到另一个操作,而不是写入寄存器再读出来,这使得第二个对同一寄存器的操作能够再第一个操作完成后马上开始。

循环展开

我们看看如何用循环展开消除对内存多次访问产生的延迟
通过增加每次迭代计算的元素的数量,减少循环的次数。

void combine5(vec_ptr v , data_t* dest)
{long i;long length = vec_length(v);data_t* data = get_vec_start(v);data_t acc = IDENT;//创建一个局部变量acc用于累计计算结果long limit = length - 1 ;for(i = 0; i < limit; i+=2){acc = (acc OP data[i]) OP data[i+1];}//完成剩余元素for(;i<length;i++){acc = acc OP data[i];}*dest = acc;//最后将累计的结果赋值给dest指针指向的内存
}

循环展开从两个方面改进程序性能:

  1. 它减少了不直接有助于程序结果的操作数量,如【循环索引计算】和【条件分支】。
  2. 它提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。

在本次优化中,原本需要两个循环的操作在一个循环内完成,减少了不必要的循环索引计算和条件分支。
到此,程序仅受单运算单元带来的吞吐量限制。

这被称为2X1的循环展开

利用多个累计变量

我们发现代码acc = (acc OP data[i]) OP data[i+1];实际上只有一个运算单元在执行,但是我们拥有多个执行同一操作的硬件,并非是处理器达到上限,是我们的程序没办法调用其他的相同运算单元。
此处我们提出一种新的方法突破 单运算单元限制:使用多累计变量

void combine6(vec_ptr v , data_t* dest)
{long i;long length = vec_length(v);data_t* data = get_vec_start(v);data_t acc1 = IDENT;//累计变量1data_t acc2 = IDENT;//累计变量2long limit = length - 1 ;for(i = 0; i < limit; i+=2){acc1 = acc1 OP data[i];acc2 = acc2 OP data[i+1];}//完成剩余元素for(;i<length;i++){acc1 = acc1 OP data[i];}*dest = acc1 OP acc2;//将两个累计变量通过 OP 运算的结果存入dest指向的内存单元
}

如此,如果 OP 是“ + ”,那么我们调用了两个加法单元执行运算,实现了指令级并行,如果 OP 是 “ * ”, 并且data_t是double ,那么我们调用了两个乘法单元【处理机仅有的两个乘法单元】。

这被称为2X2的循环展开

重新结合变换

上承combine5,我们从根本上改变合并执行的方式,也可以极大提高程序效率.
上面给出的combine5中:acc = (acc OP data[i]) OP data[i+1];
此处给出的combine7中:acc = acc OP (data[i] OP data[i+1]);

void combine7(vec_ptr v , data_t* dest)
{long i;long length = vec_length(v);data_t* data = get_vec_start(v);data_t acc = IDENT;//创建一个局部变量acc用于累计计算结果long limit = length - 1 ;for(i = 0; i < limit; i+=2){acc = acc OP (data[i] OP data[i+1]);}//完成剩余元素for(;i<length;i++){acc = acc OP data[i];}*dest = acc;//最后将累计的结果赋值给dest指针指向的内存
}

这样做的优势在哪里呢?因为括号改变了向量元素与累计值的合并顺序,一次调动两个加载单元,加载自内存的值直接运算,并将运算结果直接与寄存器运算,这样就消除了加载单元限制的延迟界限,靠近了加载单元的吞吐量界限。
combine5中,运算第一个 OP 完成后,运算第二个 OP 之前需要等待加载单元加载的数据才能运算第二个 OP。
combine7中,直接调动两个加载单元加载内存数据,而后直接调动乘法单元运算结果,再将结果与寄存器内的数据运算,最后将运算结果传送至寄存器内部。

触发条件传输

对以下代码:

for(i = 0;i < n; i++)
{if(a[i] > b[i]){long t = a[i];a[i] = b[i];b[i] = t;}
}

我们发现,在运算结果之前要进行分支预测,一旦分支预测则可能会导致分支预测错误,实际上,我们可以通过编写触发条件传送的代码来避免分支预测。改进如下:

for(i = 0; i< n;i++)
{long min = a[i] < b[i]? a[i] : b[i];long max = a[i] < b[i]? b[i] : a[i];a[i] = min;b[i] = max;
}

由此,我们避免了不必要的分支预测错误可能引发的处理器性能浪费。

小白水平有限,有错误的地方还望大佬不吝指正!

[1] 本文示例和代码引用自《深入理解计算机系统》-Randal E.Bryant
[2] 图片1引用自:https://zhuanlan.zhihu.com/p/265776590#:~:text=%E5%AF%84%E5%AD%98%E5%99%A8%E7%9A%84%E8%AE%BF%E9%97%AE%E9%80%9F%E5%BA%A6%E9%9D%9E%E5%B8%B8%E5%BF%AB%EF%BC%8C%E4%B8%80%E8%88%AC%E8%A6%81%E6%B1%82%E5%9C%A8%E5%8D%8A%E4%B8%AA%20CPU%20%E6%97%B6%E9%92%9F%E5%91%A8%E6%9C%9F%E5%86%85%E5%AE%8C%E6%88%90%E8%AF%BB%E5%86%99%EF%BC%8CCPU%20%E6%97%B6%E9%92%9F%E5%91%A8%E6%9C%9F%E8%B7%9F%20CPU%20%E4%B8%BB%E9%A2%91%E6%81%AF%E6%81%AF%E7%9B%B8%E5%85%B3%EF%BC%8C%E6%AF%94%E5%A6%82,2%20GHz%20%E4%B8%BB%E9%A2%91%E7%9A%84%20CPU%EF%BC%8C%E9%82%A3%E4%B9%88%E5%AE%83%E7%9A%84%E6%97%B6%E9%92%9F%E5%91%A8%E6%9C%9F%E5%B0%B1%E6%98%AF%201%2F2G%EF%BC%8C%E4%B9%9F%E5%B0%B1%E6%98%AF%200.5ns%EF%BC%88%E7%BA%B3%E7%A7%92%EF%BC%89%E3%80%82
[3] 图片引用自哈尔滨工业大学计算机系统课程。


  1. 1 ↩︎

【如何使用高级语言在机器语言层面提高程序运行效率】相关推荐

  1. c# 定位内存快速增长_改善C#程序,提高程序运行效率的50种方法

    转自:http://blog.sina.com.cn/s/blog_6f7a7fb501017p8a.html 一.用属性代替可访问的字段 1..NET数据绑定只支持数据绑定,使用属性可以获得数据绑定 ...

  2. php如何提高程序运行效率,如何提高PHP的运行效率 | 萧小寒

    摘要 在网站开发的过程中,如果我们不太注意代码的规范和编写技巧,很容易导致代码运行效率低下的问题.这个问题应该是绝大多数程序员身上的通病.如何编写出高效的代码,成为很多初中级程序员向高级程序员转变的一 ...

  3. 从缓存来看程序局部性提高程序运行效率的原因

    一.什么是程序局部性 一个写的优美的程序往往具有一个良好的局部性,那么什么是程序局部性呢? 程序局部性是指程序在运行的一段时间内,它们更加倾向于引用的数据靠近最近引用过的数据.因此,整个程序的执行会限 ...

  4. C++中提高程序运行效率的方法集合

    引言 由于其体系比较庞大,介于个人也是在不断完善和探索,故而此文是一个长期不定时更新的状态. 要点记录 函数在参数传递时,尽可能根据当时的情况,使用引用传递:(引用是一种别名,不涉及变量的拷贝带来的开 ...

  5. 提高C++程序运行效率的10个简单方法

    本文以C/C++程序为例讲述了程序运行效率的10个简单方法,分享给大家供大家参考之用.具体分析如下: 对于每一个程序员来说,程序的运行效率都是一个值得重视,并为之付出努力的问题.但是程序性能的优化也是 ...

  6. 提升CUDA程序运行效率的几个关键点

    目录 1.明确计算机中GPU卡片的计算资源,决定变量的性质(constant,share还是global)以及Grid,Block的维度,充分并合理利用GPU显卡的资源 2.提高PCI接口与GPU显卡 ...

  7. 如何提高程序员的生产率 (2)

    版权声明:本文由韩伟原创文章,转载请注明出处:  文章原文链接:https://www.qcloud.com/community/article/252 来源:腾云阁 https://www.qclo ...

  8. 提高C++程序运行效率,减少运行时间的方法

    大致方法: 1.优化业务逻辑,尽量少做事情. 2.减少网络访问,IO等对外操作. 3.如果有数据库,优化SQL和数据库结构. 4.优化算法,比如冒泡排序改成快排等. 5.优化代码的编写.这个就很多了. ...

  9. 用计算机机器语言编写的程序可以,用机器语言编写的程序可读性最差

    正确答案: B 用机器语言编写的程序可读性最差 题目:下列叙述中,正确的是( ). 解析:只有机器语言才能被计算机直接识别,但机器语言可读性是最差的.汇编语言是符号化的机器语言,但不是机器语言,其程序 ...

最新文章

  1. ClickHouse 学习
  2. NOIP2008提高组(前三题) -SilverN
  3. Android调试秘钥证书指纹获取方式
  4. python数据科学实践 常象宇_Python数据科学实践
  5. python创建控件,用 IronPython 创建和调用用户控件.doc
  6. android build.gradle的repositories设置
  7. Cannot subclass final class class com.sun.proxy.$Proxy16
  8. Win7下使用VC6.0的注意事项
  9. docker bi工具superset汉化
  10. 第三阶段应用层——1.3 数码相册—英文和汉字的点阵显示
  11. fresco 显示缩略图,不直接使用setImageURI,防止卡顿和显示不全:纯黑色或纯白色
  12. 如何进行产品原型设计
  13. 关键字const的使用总结
  14. esxi迁移linux报错device eth0 does not seem tobe present,delaying
  15. 如何写一个拼写检查器-by Peter Norvig
  16. VS+OpenCV 处理图像的颜色(下)用色调、饱和度和亮度表示颜色、肤色检测
  17. ABP官方文档(四十五)【集成Hangfire】
  18. 0.1+0.2不等于0.3,微信小程序云开发如何解决JavaScript小数计算精度失准的问题
  19. Office2003/2007/2010强力卸载工具下载
  20. vant步进器传值_Vant Uploader 文件上传

热门文章

  1. 交比不变性 matlab,交比 | 迪沙格定理
  2. GO项目性能优化大赏
  3. 21世纪十大热门编程语言大集合,看你适合哪一种?
  4. 哪些是性价比高的骨传导蓝牙耳机,五款骨传导蓝牙耳机推荐
  5. 映射可以多对一吗_【高中数学集合与映射】(一)整数和有理数“一样多”?...
  6. 微信小程序全选,微信小程序checkbox,微信小程序购物车
  7. 如何做到四位验证码更简洁的生成?
  8. 0.4-如何使用DAP LINK 下载Hex程序?
  9. 同花顺股票交易接口定义被类实现
  10. 讯飞星火 VS 文心一言:谁是中文大语言模型的TOP1?