c语言中内循环和外循环的位置可能产生性能上的区别

∇ \nabla 联系方式:

e-mail: FesianXu@gmail.com

QQ: 973926198

github: https://github.com/FesianXu

知乎专栏: 计算机视觉/计算机图形理论与应用

微信公众号
qrcode


在图像处理相关的代码中,我们经常有类似于以下的代码,去遍历多维数组(张量)的每一个元素:

#define LENGTH 10000
void proc(){
	uint8 datas[LENGTH][LENGTH];
	int i, j;
	long long sum = 0;
	for (i = 0; i < LENGTH; i++){
		for (j = 0; j < LENGTH; j++){
			sum += datas[i][j];
		}
	}
}

其中的sum += datas[i][j];从语义上是和sum += datas[j][i];效果一致的,然而从性能上来说是否是一致的呢?答案是不是的,要看程序的编译优化程度。我们会发现,当循环变量处于内循环和外循环时,其性能是不一样的,即便是运算结果一致。


我们知道不管是一维数组还是多维数组,在内存中都是线性排列的,以二维数组为例子,为了能将二维的数组拉成一维的,一般需要考虑编译器在编译代码时,其在内存中是行优先(row-major)排列还是列优先(colum-major)排列的。如下图所示,如果一个数组是行优先排列的,那么其在连续内存上的排列顺序如红色线的顺序。举个例子,比如现在有个数组int vars[3][3],如果是行优先排列的,那么有:(===的意思是等价, (vars+i)表示对以vars作为数组指针的前提下,偏移i个元素的地址,而*(vars+i)是对其进行取内容。)

vars[0, 0] === *(vars+0);
vars[0, 1] === *(vars+1);
vars[1, 0] === *(vars+3);
==>
vars[i, j] === *(vars+3*i+j)

如果是列优先排列呢,则是

vars[0, 0] === *(vars+0);
vars[0, 1] === *(vars+1*3+1);
vars[1, 0] === *(vars+0*3+1);
==>
vars[i, j] === *(vars+3*j+i)

在这里插入图片描述
到这里为止都好理解,不过后续我们需要理解的一点是,我们现在跑得程序很多都不是在裸机上跑的。这里指的裸机就是没有操作系统的计算机,比如单片机等,或者是没有任何操作系统的其他CPU。在操作系统上跑程序,那么我们的程序的内存空间其实都是虚拟内存空间,是一种逻辑地址,其和物理的内存位置是没有必然关联的,需要受到操作系统的控制。采用虚拟内存有很多好处,其中最明显的就是:

  1. 不需要考虑不同程序之间的相对地址偏移,每个程序都有其独自的内存空间,其地址范围都是从0到系统定义的最大值max_mem
  2. 虚拟内存可以看成是一个巨大的线性内存空间,不需要考虑内存不足的情况,因为当内存不足的情况或者需要访问的数据不在内存上时,就发生了缺页错误(page fault),操作系统会自动进行“换页”(paging),将物理内存暂时不用了的内存页保存到存储空间巨大的硬盘上,然后把需要读取的内存加载到内存上。在这种情况下,可以把整个硬盘都看成是一个可以换入和切出的内存(虽然速度很慢,没法和真正的内存比)

虚拟内存的细节太多,是操作系统设计的一个主要概念,其他细节需要参考其他书籍,比如[1,2]。我们在这里需要知道的是,我们程序中随时可能遇到数据在内存中找不到的事情发生,这个时候就会发生换页的操作,从硬盘中加载数据到内存(IO操作)是非常花时间的,因此很多程序的瓶颈都会在此。我们这里的关键点也正是在此。

在一个以行优先排序的编译器上,每一行的数据在内存地址上都是比较接近的,而列数据的地址总是相差着sizeof(data_type) * N,其中 N N N是每一行的元素数量。这就导致每一行的数据可能是处于同一个内存页帧(page frame)上的,而每一个列的数据处于不同的页帧上。在以行优先排序的编译器上,如果外循环是列索引,内循环是行索引,有可能会发生频繁地进行缺页,换页的操作(准确地说是n*m次),严重影响程序的性能。当然,这里只是可能,具体情况和你的数组大小,系统的页大小设置等有关。(这里的缺页特别在物理内存比较小的时候更为严重)

这里举个代码例子,说明性能上的差别。


考虑代码:

# code 1
#define LENGTH 20000
int main(){
	float vector[LENGTH][LENGTH];
	int i, j;
	float sum = 0.0f;
	for (i = 0; i < LENGTH; i++){
		for (j = 0; j < LENGTH; j++){
			sum += datas[i][j];
		}
	}
	return 0;
}
# code 2
#define LENGTH 20000
int main(){
	float vector[LENGTH][LENGTH];
	int i, j;
	float sum = 0.0f;
	for (i = 0; i < LENGTH; i++){
		for (j = 0; j < LENGTH; j++){
			sum += datas[j][i];
		}
	}
	return 0;
}

这两个代码除了索引的顺序不同(相当于内外循环调换)之外,其他别无差别。我们为了防止编译器对代码进行优化,影响分析,我们采用-O0编译参数以去掉任何gcc的优化。(其实用其他优化等级效果也是类似的,读者可以自行尝试,并且用指令gcc -O0 -S code1.c观察分析其汇编代码。)

gcc -O0 code1.c
/usr/bin/time -v ./a.out

我们用/usr/bin/time分析程序的运行时间,我们有这两者的运行时间分别为:

在这里插入图片描述
code 1的运行时间为2.05s
在这里插入图片描述
而code 2的时间则变成了5.68s,性能明显差code 1很多。

但是如果我们把数组的大小从20000改成200会怎么样呢?我们发现其性能将不会有明显区别了,就是因为尺寸大小的不同影响了页帧的换页过程。


Reference

[1]. Bryant R E, David Richard O H, David Richard O H. Computer systems: a programmer’s perspective[M]. Upper Saddle River: Prentice Hall, 2003.
[2]. Tanenbaum A S. Structured computer organization[M]. Pearson Education India, 2016.

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
目 录 第1章 C语言 8 1.1 什么是局部程序块(local block)? 8 1.2 可以把变量保存在局部程序块中吗? 9 1.3 什么时候用一条switch语句比用多条if语句更好? 9 1.4 switch语句必须包含default分支吗? 10 1.5 switch语句的最后一个分支可以不要break语句吗? 11 1.6 除了在for语句中之,在哪些情况下还要使用逗号运算符? 11 1.7 怎样才能知道循环是否提前结束了? 13 1.8 goto,longjmp()和setjmp()之间有什么区别? 13 1.9 什么是左值(lvaule)? 15 1.10 数组(array)可以是左值吗? 15 1.11 什么是右值(rvaule)? 16 1.12 运算符的优先级总能保证是“自左至右”或“自右至左”的顺序吗? 17 1.13 ++var和var++有什么区别? 17 1.14 取模运算符(modulus operator)“%”的作用是什么? 17 第2章 变量和数据存储 18 2.1. 变量存储在内存(memory)中的什么地方? 18 2.2. 变量必须初始化吗? 19 2.3. 什么是页抖动(pagethrashing)? 19 2.4. 什么是const指针? 20 2.5. 什么时候应该使用register修饰符?它真的有用吗? 21 2.6. 什么时候应该使用volatile修饰符? 21 2.7. 一个变量可以同时被说明为const和volatile吗? 22 2.8. 什么时候应该使用const修饰符? 23 2.9. 浮点数比较(floating-point comparisons)的可靠性如何? 23 2.10. 怎样判断一个数字型变量可以容纳的最大值? 24 2.11. 对不同类型的变量进行算术运算会有问题吗? 25 2.12. 什么是运算符升级(operatorpromotion)? 25 2.13. 什么时候应该使用类型强制转换(typecast)? 26 2.14. 什么时候不应该使用类型强制转换(typecast)? 27 2.15. 可以在头文件中说明或定义变量吗? 27 2.16. 说明一个变量和定义一个变量有什么区别? 27 2.17. 可以在头文件中说明static变量吗? 28 2.18. 用const说明常量有什么好处? 28 第3章 排序与查找 28 排序 28 查找 29 排序或查找性能? 30 3.1. 哪一种排序方法最方便? 32 3.2. 哪一种排序方法最快? 33 3.3. 对存(磁盘或磁带)中而不是内存中的数据进行排序称为部排序。 39 3.4. 1哪一种查找方法最方便? 44 3.5. 1哪一种查找方法最快? 46 3.6. 1什么是哈希查找? 51 3.7. 1怎样对链表进行排序? 53 3.8. 1怎样查找链表中的数据? 53 第4章 数据文件 59 4.1. 当errno为一个非零值时,是否有错误发生? 59 4.2. 什么是流(stream)? 59 4.3. 怎样重定向一个标准流? 60 4.4. 怎样恢复一个重定向了的标准流? 60 4.5. stdout能被强制打印到非屏幕设备上吗? 61 4.6. 文本模式(textmode)和二进制模式(binarymode)有什么区别? 61 4.7. 怎样判断是使用流函数还是使用低级函数? 62 4.8. 怎样列出某个目录下的文件? 62 4.9. 怎样列出一个文件的日期和时间? 63 4.10. 怎样对某个目录下的文件名进行排序? 66 4.11. 怎样判断一个文件的属性? 67 4.12. 怎样查看PATH环境变量? 69 4.13. 怎样打开一个同时能被其它程序修改的文件? 69 4.14. 怎样确保只有你的程序能存取一个文件? 71 4.15. 怎样防止其它程序修改你正在修改的那部分文件内容? 71 4.16. 怎样一次打开20个以上的文件? 72 4.17. 怎样避开"Abort,Retry,Fail”消息? 72 4.18. 怎样读以逗号分界的本? 74 第5章 编译预处理 76 5.1. 什么是宏(macro)?怎样使用宏? 76 5.2. 预处理程序(preprocessor)有什么作用? 77 5.3. 怎样避免多次包含同一个头文件? 79 5.4. 可以用#include指令包含类型名不是".h"的文件吗? 80 5.5. 用#define指令说明常量有什么好处? 80 5.6. 用enum关键字说明常量有什么好处? 81 5.7. 与用#define指令说明常量相比,用enum关键字说明常量有什么好处? 81 5.8. 如何使部分程序在

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FesianXu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值