摘要:本文将以Loop Interchange的场景为例,讲述在编写代码时可以拿到更优性能的书写方式。

本文分享自华为云社区《编译器工程师眼中的好代码(1):Loop Interchange》,作者:毕昇小助手。

编者按:C/C++代码在编译时,编译器将源码翻译成CPU可识别的指令序列并生成可执行代码,而最终代码的运行效率取决于由编译器生成的可执行代码。在大部分情况下,编写源代码时就已经决定了程序可以在何种程度下被编译器优化。即使对源代码做微小改动也可能会对编译器生成的代码运行效率产生重大影响。因此,源代码的优化可以在一定程度上帮助编译器生成更高效的可执行代码。

本文将以Loop Interchange的场景为例,讲述在编写代码时可以拿到更优性能的书写方式。

1、Loop Interchange 相关基本概念

1.1 访问局部性

访问局部性指的是在计算机科学领域中应用程序在访问内存的时候,倾向于访问内存中较为靠近的值。这种局部性是出现在计算机系统中的一种可预测行为,我们可以利用系统的这种强访问局部性来进行性能优化。访问局部性分为三种基本形式,分别为时间局部性、空间局部性、循序局部性。

而本文主要讲述的Loop Interchange主要是利用了空间局部性。空间局部性指的是,最近引用过的内存位置以及其周边的内存位置容易再次被使用。比较常见于循环中,比如在一个数组中,如果第3个元素在上一个循环中被使用,那么本次循环中极有可能会使用第4个元素;如果本次循环确实使用第4个元素,就是命中上一次迭代所prefetch到的cache数据。

所以对于数组循环运算,可以利用空间局部性这一特征,保证两次相邻循环中对数组元素的访问在内存上是更加靠近的,即循环访问数组中的元素时stride越小,相应的性能可能会有所优化。

那么,数组在内存上是如何存储的呢?

1.2 Row-major 和 Column-major

Row-major 和 Column-major 是两种将多维数组存储在线性存储中的方式。数组的元素在内存中是连续的;Row-major ordering代表行的连续元素在内存中彼此相邻,而Column-major ordering则是代表列的连续元素彼此相邻,如下图所示。

虽然Row和Column的名称看起来像是特指二维数组,但是Row-major和Column-major也可以推广适用于任何维度的数组。

那么在C/C++中,数组是以以上哪种方式存储的呢?

举一个小例子,用cachegrind工具来展示C使用两种不同的访问形式的CPU的cache丢失率对比。

按行访问:

  1. #include <stdio.h>
  2. int main(){
  3. size_t i,j;
  4. const size_t dim = 1024 ;
  5. int matrix [dim][dim];
  6. for (i=0;i< dim; i++)
  7. for (j=0;j <dim;j++)
  8. matrix[i][j]= i*j;
  9. return 0;
  10. }

按列访问:

  1. #include <stdio.h>
  2. int main(){
  3. size_t i,j;
  4. const size_t dim = 1024 ;
  5. int matrix [dim][dim];
  6. for (i=0;i< dim; i++){
  7. for (j=0;j <dim;j++){
  8. matrix[j][i]= i*j;
  9. }
  10. }
  11. return 0;
  12. }

根据上述C代码中对相同数组的两种不同访问方式时cache丢失率的对比,可以说明在C/C++代码中,数组是以Row-major形式储存的。也就是说,如果前一步访问了a[i][j],那么对a[i][j+1]的访问会命中cache。这样就不会执行对主存储器的访问,而cache比主内存快,因此遵循相应编程语言的储存形式使其可以命中cache可能会带来优化。

至于其他常用的编程语言,Fortran、MATLAB等则是默认Column-major形式。

1.3 Loop Interchange

Loop Interchange利用系统倾向于访问内存中较为靠近的值的特征以及C/C++ Row-major的特点,通过改变循环嵌套中两个循环之间的执行顺序,增加整体代码空间局部性。此外,它还可以启用其他重要的代码转换,例如,Loop Reordering就是Loop Interchange扩展到两个以上循环被重新排序时的优化。在LLVM中,Loop Interchange需要通过使能-mllvm -enable-loopinterchange选项启用。

2、优化示例

2.1 基础场景

简单看下面一个矩阵运算的示例:

原始代码:

  1. for(int i = 0; i < 2048; i++) {
  2. for(int j = 0; j < 1024; j++) {
  3. for(int k = 0; k < 1024; k++) {
  4. C[i * 1024 + j] += A[i * 1024 + k] * B[k * 1024 + j];
  5. }
  6. }
  7. }

试着把jk两层循环进行Loop Interchange之后的代码:

  1. for(int i = 0; i < 2048; i++) {
  2. for(int k = 0; k < 1024; k++) {
  3. for(int j = 0; j < 1024; j++) {
  4. C[i * 1024 + j] += A[i * 1024 + k] * B[k * 1024 + j];
  5. }
  6. }
  7. }

可以发现,在原始代码中,最内层的k每次迭代,C要访问的数据不变,A每次访问的stride为1,大概率命中cache,但B由于每次访问的stride为1024,几乎每次都会cache miss。

Loop Interchange之后,j位于最内层循环,每次迭代时A每次要访问的数据不变,C和B每次访问的stride为1,都会有很大概率命中cache,cache命中率大大增加。

那么cache命中率是否真的增加,以及两者的性能又如何呢?

原始代码:

  1. $ time -p ./a.out

  1. $ sudo perf stat -r 3 -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-loads ./a.out

  1. Loop Interchange后的结果如下:
  2. $ time -p ./a.out

  1. $ sudo perf stat -r 3 -e cache-misses,cache-references,L1-dcache-load-misses,L1-dcache-loads ./a.out

两者相比:

L1-dcache-loads的数目差不多,因为要访问的数据总量差不多;
L1-dcache-load-misses所占L1-dcache-loads的比例在进行loop interchange代码修改后降低了将近10倍。

同时,性能数据上也能带来接近9.5%的性能提升。

2.2 特殊场景

当然,在实际使用时,并不是所有的场景都是如2.1中所示的那种工整的可Loop Interchange场景。

  1. for ( int i = 0; i < N; ++ i )
  2. {
  3. if( I[i] != 1 ) continue;
  4. for ( int m = 0; m < M; ++ m )
  5. {
  6. Res2 = res[m][i] * res[m][i];
  7. norm[m] += Res2;
  8. }
  9. }

如上述场景,如果N是超大数组,那么Loop Interchange理论上可以带来较大收益;但由于两层循环中间增加一个分支判断,导致原本可以Loop Interchange的场景无法实现。

针对这种场景,可以考虑将中间的分支判断逻辑剥离,可以先保证Loop Interchange使得数组res在连续内存上进行访问;至于中间的判断分支逻辑,可以在Loop Interchange两层循环后再进行回退。

  1. for ( int m = 0; m < M; ++ m )
  2. {
  3. for ( int i = 0; i < N; ++ i)
  4. {
  5. Res2 = res[m][i] * res[m][i];
  6. norm[m] += Res2;
  7. if( I[i] != 1 ) //补充逻辑,保证源代码语义
  8. {
  9. norm[m] -= Res2;
  10. continue;
  11. }
  12. }
  13. }

当然,这样的源码修改也需要考虑cost是否值得,如果该if分支进入频率非常高,那么之后回退带来的cost也会较大,可能就需要重新考虑Loop Interchange是否值得;反之,如果分支进入频率非常低,那么Loop Interchange的实现还是可以带来可观的收益的。

3、毕昇编译器对Loop Interchange pass社区的贡献

毕昇编译器团队在llvm社区中对Loop Interchange pass也做出了不小的贡献。团队从legality、profitability等方面对Loop Interchange pass做了全方位的增强,同时也对该pass所支持的场景做了大量的扩展。在Loop Interchange方面,近两年来团队小伙伴为社区提供了二十余个主要的patch,包含Loop Interchange,以及相关的dependence analysis、loop cache analysis、delinearization等分析和优化的增强。简单举几个例子:

这两个patch将Loop Interchange的应用场景扩展到内层或者外层循环中包含不止一个induction variable的情况:

  1. for (c = 0, e = 1; c + e < 150; c++, e++) {
  2. d = 5;
  3. for (; d; d--)
  4. a |= b[d + e][c + 9];
  5. }
  6. }

这两个patch将Loop Interchange的应用场景扩展到支持浮点类型的reduction计算的场景:

  1. double matrix[dim][dim];
  2. for (i=0;i< dim; i++)
  3. for (j=0;j <dim;j++)
  4. matrix[i][j] += 1.0;

D120386 [LoopInterchange] Try to achieve the most optimal access pattern after interchange
(https://reviews.llvm.org/D120386)

这个patch增强了Interchange的能力使编译器能够将循环体permute成为全局最优的循环顺序:

  1. void f(int e[100][100][100], int f[100][100][100]) {
  2. for (int a = 0; a < 100; a++) {
  3. for (int b = 0; b < 100; b++) {
  4. for (int c = 0; c < 100; c++) {
  5. f[c][b][a] = e[c][b][a];
  6. }
  7. }
  8. }
  9. }

=>

  1. void f(int e[100][100][100], int f[100][100][100]) {
  2. for (int c = 0; c < 100; c++) {
  3. for (int b = 0; b < 100; b++) {
  4. for (int a = 0; a < 100; a++) {
  5. f[c][b][a] = e[c][b][a];
  6. }
  7. }
  8. }
  9. }

D124926 [LoopInterchange] New cost model for loop interchange
(https://reviews.llvm.org/D124926)

这个patch为loop interchange提供了一个全新的,功能更强的cost model,可以更准确地对loop interchange的profitability做出判断。

此外,我们还为社区提供了大量的bugfix的patch:

  • D102300 [LoopInterchange] Check lcssa phis in the inner latch in scenarios of multi-level nested loops
  • D101305 [LoopInterchange] Fix legality for triangular loops
  • D100792 [LoopInterchange] Handle lcssa PHIs with multiple predecessors
  • D98263 [LoopInterchange] fix tightlyNested() in LoopInterchange legality
  • D98475 [LoopInterchange] Fix transformation bugs in loop interchange
  • D102743 [LoopInterchange] Handle movement of reduction phis appropriately during transformation (pr43326 && pr48212)
  • D128877 [LoopCacheAnalysis] Fix a type mismatch bug in cost calculation

以及其他功能的增强:

  • D115238 [LoopInterchange] Remove a limitation in legality
  • D118102 [LoopInterchange] Detect output dependency of a store instruction with itself
  • D123559 [DA] Refactor with a better API
  • D122776 [NFC][LoopCacheAnalysis] Add a motivating test case for improved loop cache analysis cost calculation
  • D124984 [NFC][LoopCacheAnalysis] Update test cases to make sure the outputs follow the right order
  • D124725 [NFC][LoopCacheAnalysis] Use stable_sort() to avoid non-deterministic print output
  • D127342 [TargetTransformInfo] Added an option for the cache line size
  • D124745 [Delinearization] Refactoring of fixed-size array delinearization
  • D122857 [LoopCacheAnalysis] Enable delinearization of fixed sized arrays

结语

如果想要尽可能的利用Loop Interchange优化,那在书写C/C++代码时,请尽可能保证每个迭代之间对数组或数列的访问stride越小越好;stride越接近1,空间局部性就越高,自然cache命中率也会更高,在性能数据上也可以拿到更理想的收益。另外,由于C/C++的存储方式为Row-major ordering,所以在访问多维数组时,请注意内层循环要为Column才能拿到更小的stride。

参考

[1] https://zhuanlan.zhihu.com/p/455263968
[2] https://valgrind.org/info/tools.html#cachegrind
[3] https://blog.csdn.net/gengshenghong/article/details/7225775
[4] https://en.wikipedia.org/wiki/Loop_interchange
[5] https://johnysswlab.com/loop-optimizations-how-does-the-compiler-do-it/#footnote_6_1738
[6] https://blog.csdn.net/PCb4jR/article/details/85241114
[7] https://blog.csdn.net/Darlingqiang/article/details/118913291
[8] https://en.wikipedia.org/wiki/Row-_and_column-major_order
[9] https://en.wikipedia.org/wiki/Locality_of_reference

点击关注,第一时间了解华为云新鲜技术~

编译器工程师眼中的好代码:Loop Interchange的更多相关文章

  1. 全栈工程师眼中的HTTP

    HTTP,是Web工程师每天打交道最多的一个基本协议.很多工作流程.性能优化都围绕HTTP协议来进行,但是我们对HTTP的理解是否全面呢?如果前端工程师和后台工程师坐在一起玩捉鬼游戏,他们对HTTP的 ...

  2. 第一篇:数据工程师眼中的智能电网(Smart Grid)

    前言 想必第一次接触到智能电网这个概念的人,尤其是互联网从业者,都会顾名思义的将之理解为"智能的电网". 然而智能电网中的"智能"是广义上的智能,它就是指更好的 ...

  3. 一、源代码-面向CLR的编译器-托管模块-(元数据&IL代码)

    本文脉络图如下: 1.CLR(Common Language Runtime)公共语言运行时简介 (1).公共语言运行时是一种可由多种编程语言一起使用的"运行时". (2).CLR ...

  4. Sublime Text webstorm等编译器快速编写HTML/CSS代码的技巧

    <!DOCTYPE html> Sublime Text webstorm等编译器快速编写HTML/CSS代码的技巧--summer-rain博客园 xiayuhao 东风夜放花千树. 博 ...

  5. 快速开发MQTT(一)电子工程师眼中的MQTT

    转载:https://zhuanlan.zhihu.com/p/54669124 DigCore 主页http://www.digcore.cn 文章首发于同名微信公众号:DigCore 欢迎关注同名 ...

  6. 【C# 线程】编译器代码优化技术 循环提升:Loop Hoisting

    转载自:https://gandalfliang.github.io/2019/01/15/loop-hoisting/ Loop Hoisting 在上篇文章中,提到 Loop Hoisting , ...

  7. Sublime Text、webstorm等编译器快速编写HTML/CSS代码的技巧

    Sublime Text.webstorm等编译器,如果你从事Web前端开发的话,对这几款软件一定不会陌生.它使用仿CSS选择器的语法来生成代码,大大提高了HTML/CSS代码编写的速度,比如下面的演 ...

  8. 如何利用C# Roslyn编译器写一个简单的代码提示/错误检查?

    OK, 废话不多说,这些天在写C#代码时突然对于IDE提示有了一些想法,之前也有了解过,不过并没有深入. 先看个截图: 一段再简单不过的代码了,大家注意看到 count 字段下面的绿色波浪线了吗,我们 ...

  9. (二)如何利用C# Roslyn编译器写一个简单的代码提示/错误检查?

    上一篇我们讲了如何建立一个简单的Roslyn分析项目如分析检查我们的代码. 今天我们主要介绍各个项目中具体的作用以及可视化分析工具. 还是这种截图,可以看到解决方案下一共有三个项目. Analyzer ...

随机推荐

  1. 169. Majority Element - LeetCode

    Question 169. Majority Element Solution 思路:构造一个map存储每个数字出现的次数,然后遍历map返回出现次数大于数组一半的数字. 还有一种思路是:对这个数组排 ...

  2. 论文解读(gCooL)《Graph Communal Contrastive Learning》

    论文信息 论文标题:Graph Communal Contrastive Learning论文作者:Bolian Li, Baoyu Jing, Hanghang Tong论文来源:2022, WWW ...

  3. Java 多线程共享模型之管程(上)

    主线程与守护线程 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束.有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束. packag ...

  4. Dubbo的基本使用

    Dubbo分为提供者和消费方  并且两者都要注册到ZK上 提供者 注解    @Service   这是dubbo包下的 消费组 注解    @Reference 远程注入 第一步导入依赖 <! ...

  5. 左右手切换工具xmouse v1.2版本发布

    Xmouse 方便的切换鼠标左右键,因为功能非常简单,所以支持.net framework 2.0及以上 windows环境就可以了,目前已测试win7.win10可用. 关于为什么做这么个东西,那是 ...

  6. 手把手教你实现一个Vue无限级联树形表格(增删改)

    前言平时我们可能在做项目时,会遇到一个业务逻辑.实现一个无限级联树形表格,什么叫做无限级联树形表格呢?就是下图所展示的内容,有一个祖元素,然后下面可能有很多子孙元素,你可以实现添加.编辑.删除这样几个 ...

  7. 《阿里云天池大赛赛题解析》——O2O优惠卷预测

    赛事链接:https://tianchi.aliyun.com/competition/entrance/231593/introduction?spm=5176.12281925.0.0.7e157 ...

  8. 安装pystaller

    安装命令 # -i指定下载地址,此处采用清华大学镜像 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple some-package pyin ...

  9. python基础教程:定义类创建实例

    类的定义 在Python中,类通过class关键字定义,类名以大写字母开头 >>>class Person(object): #所有的类都是从object类继承 pass #pass ...

  10. 业务可视化-让你的流程图"Run"起来(2.问题与改进)

    前言 首先,感谢大家对上一篇文章[业务可视化-让你的流程图"Run"起来]的支持. 分享一下近期我对这个项目的一些改进. 问题&改进 问题1: 流程运行开始后,异步执行,无 ...