从C过渡到C++——换一个视角深入数组[真的存在高效吗?](2)

C风格高效的数组遍历

在过渡到C++之前我还是想谈一谈如何书写高效的C的代码,这里的高效指的是C代码的高效,也就是在不开启编译器优化下,C层级的自由化,编译器优化固然很好,但是源代码的高效远远要胜于编译器的优化,因为在大多数情况下,你并不知道优化器到底干了什么。

数组名和指针

考虑如下声明:

int a;
int b[10];

我们把a通常称为一个标量,因为它是一个单一的值,这个变量的类型是一个整数。我们把变量b称为数组,因为它是一些值的集合。下标和数组名一起使用,用于标识该集合中某个特定的值。但是我们使用的时候都是这样的:

b[1]=10;

我们很少考虑一个问题就是b的类型是什么,我们知道b[1]的类型是一个int,这很好理解,你可能会说b是一整个数组,但实际上在c中并不是这样的,b应该是一个指针常量,且这个指针常量指向数组中第一个元素,为什么这样说呢?

例如我们可以实现如下的操作:

//test1
static int sta_var[]={0};
sta_var[0]++;
int *a = NULL;
a = sta_var;
printf("%d",sta_var);

我们在这里声明了一个指针,然后将数组名付给了它,输出结果如下:

同时上述语句更改为如下以后结果也是相同的:

    a = &sta_var[0];

这里需要强调的是,指针和数组名并不是完全相同的,数组具有确定的元素数量,而指针只是一个标量值。编译器用数组名来记住这些属性。只有当数组名在表达式中使用的时候,编译器才会为它产生一个指针常量。

只有在两个场合下,数组名并不用指针常量来表示——当数组作为sizeof操作符或者单目操作符&的操作数的时候,sizeof返回整个数组中的长度,而不是指向数组的指针长度。取一个数组名的地址所产生的是一个指向数组的指针,关于指向数组的指针我们晚点再来讨论。

下标引用与指针

结合上边的讲解我们来看一个比较奇怪的东西:

*(sta_var+3)

首先我们知道sta是一个指针,指向整型,所以3这个值根据整型值的长度进行调整。加法运算的结果是另一个指向整形的指针,它所指向的是数组第一个元素向后移动3个整数长度的位置。其次,间接访问操作符访问这个新位置,或者取得那里的值(整型)。

这个东西由另一个常见的形式:

sta_var[3]

也就是下标引用和间接访问完全相同。

如果二者完全相同那么在运行效率上会有差别吗?我们应该使用哪一个呢?和往常一样,这里没有明确的答案,对于大多数人来说,下标更容易理解,尤其是在多维数组当中。所以在可读性方面,下标具有一定的优势。但这个选择可能会影响运行时的效率。

假设这两种方法都是正确的,下标绝对不会比指针更有效率,但指针有时会比下标更有效率。这也是本篇文章所讨论的内容。

为了理解这个问题,让让我们来研究两个循环:

    //loop1
   int array[10];
   for(int a= 0 ; a< 10 ;a++)
       array[a] = 1;

第二个loop:

    //loop2
   for(int *i=array; i < array+10 ;i++)
           *i = 0;

对于第一个循环,为了求取下标的值,编译器在程序中插入指令,取得a的值,并把它与整形的长度相乘(整形长度是4)这个乘法需要花费一定的时间和空间,对于第二个循环, 尽管这里不存在下标,但是还是存在乘法运算,这个乘法运算在于每次for循环更新的时候就是指针i进行变化的时候,i++的时候这个1必须与整形的长度进行相乘然后再与指针相加,但是这个乘法有很大区别,乘法执行的是两个整数,也就是1和4,这个乘法在编译的时候只执行一次,程序在运行的时候直接使用这个4。

这里是默认情况未经过编译器优化的测试结果,我们让每一个循环跑10000000遍,看一看需要多久。

修改后两个代码如下:

    //loop1 21.8792s
   start = clock();
   for(int j=1;j<10000000;j++)
       for(int a= 0 ; a< 1000 ;a++)
           array[a] = 1;
   stop = clock();
   duration = ((double)(stop- start))/CLK_TCK;
   printf("%.8f\n",duration);

   //loop2
   start = clock();
   for(int j=1;j<10000000;j++)
  {
       for(int *i=array; i < array+1000 ;i++)
       *i = 1;
  }
   stop = clock();
   duration = ((double)(stop- start))/CLK_TCK;
   printf("%.8f\n",duration);

我们在不开启优化的情况下运行时间如下:

我们在开启O优化的情况下:

至于这里为什么下边运行效率会高一半我觉得是优化的问题,相同的操作只花费一般的时间,所有开启优化的结果都不能算数

开启O2优化,就不用说了,他会直接变成0s,因为实际上做的操作是无用功。整个操作都被优化掉了。

我们看看二者的区别在哪里(以下是没有经过优化的代码):

    jmp .L5
.L6:
mov eax, DWORD PTR 3916[rbp]
cdqe
mov DWORD PTR -96[rbp+rax*4], 123
add DWORD PTR 3916[rbp], 1
.L5:
cmp DWORD PTR 3916[rbp], 999
jle .L6

使用指针的汇编:

    jmp .L5
.L6:
mov rax, QWORD PTR 3912[rbp]
mov DWORD PTR [rax], 123
add QWORD PTR 3912[rbp], 4
.L5:
lea rax, -96[rbp]
add rax, 4000
cmp QWORD PTR 3912[rbp], rax
jb .L6

按照上文的分析我们的数组使用指针效率不应该是更高吗?我们可以看到使用指针的汇编中并没有出现过使用乘法计算的内容:[rbp+rax*4],而是直接使用了4来替代:add QWORD PTR 3912[rbp], 4,那为啥实际上计算时间没有太大差异呢?甚至有些时候指针下标会慢于下标访问呢?

笨蛋的编译器,最重要的原因我觉得就在这里:

.L5:
lea rax, -96[rbp]
add rax, 4000

这里是唯一指针下标多出来的代码。我们的指针并没有将i < array+1000优化掉,即使我们这里所比较的内容是相同的,但是运算的时候为了防止array发生变化,这里又重新计算了一遍加法,也就是说这里在每次循环都要计算一遍加法,这是造成时间变化得最主要原因,所以我们可以这样说这样的写法只能保证:

指针效率≈下标访问的效率

这样的结果是编译器决定的,如果需要查看在某一个机器上的指针运行效率,建议还是查看一下具体的汇编代码,对于一般的for循环我们还是使用下标,这样对于别人来看你的代码的可读性会有很大的提升,如果真的需要大量的遍历或者各种情况,建议使用上文的方法来测量一下运行时间是否真的有一定的进步。

 

从C过渡到C++——换一个视角深入数组[真的存在高效吗?](2)的更多相关文章

  1. 从C过渡到C++——换一个视角深入数组[初始化](1)

    从C过渡到C++--换一个视角深入数组[初始化](1) 目录 从C过渡到C++--换一个视角深入数组[初始化](1) 数组的初始化 从C入手 作用域 代码块作用域 文件作用域 原型作用域 函数作用域 ...

  2. 【机器学习基础】——另一个视角解释SVM

    SVM的另一种解释 前面已经较为详细地对SVM进行了推导,前面有提到SVM可以利用梯度下降来进行求解,但并未进行详细的解释,本节主要从另一个视角对SVM进行解释,首先先回顾之前有关SVM的有关内容,然 ...

  3. ytu 1050:写一个函数,使给定的一个二维数组(3×3)转置,即行列互换(水题)

    1050: 写一个函数,使给定的一个二维数组(3×3)转置,即行列互换 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 154  Solved: 112[ ...

  4. [CareerCup] 13.10 Allocate a 2D Array 分配一个二维数组

    13.10 Write a function in C called my2DAlloc which allocates a two-dimensional array. Minimize the n ...

  5. new一个二维数组

    .定义一个二维数组 char **array1 array1 = new char *[x]; for(i=0;i<x;++i) array1[i] = new char[y]; ...用的时候 ...

  6. 写入数据到Plist文件中时,第一次要创建一个空的数组,否则写入文件失败

    #pragma mark - 保存数据到本地Plist文件中 - (void)saveValidateCountWithDate:(NSString *)date count:(NSString *) ...

  7. [原]Java面试题-输入一个整型数组,找出最大值、最小值,并交换。

    [Date]2013-09-19 [Author]wintys (wintys@gmail.com) http://wintys.cnblogs.com [Content]: 1.面试题 输入一个整型 ...

  8. c语言题目:找出一个二维数组的“鞍点”,即该位置上的元素在该行上最大,在该列上最小。也可能没有鞍点

    //题目:找出一个二维数组的“鞍点”,即该位置上的元素在该行上最大,在该列上最小.也可能没有鞍点. // #include "stdio.h" #include <stdli ...

  9. 如何判断一个变量是数组Array类型

    在很多时候,我们都需要对一个变量进行数组类型的判断.JavaScript中如何判断一个变量是数组Array类型呢?我最近研究了一下,并分享给大家,希望能对大家有所帮助. JavaScript中检测对象 ...

随机推荐

  1. Redis集群搭建 三主三从

    Redis集群介绍 Redis 是一个开源的 key-value 存储系统,由于出众的性能,大部分互联网企业都用来做服务器端缓存.Redis在3.0版本之前只支持单实例模式 虽然支持主从模式,哨兵模式 ...

  2. MathType7安装使用及please restart word to load mathtype addin properly的问题

    MathType7安装使用及please restart word to load mathtype addin properly的问题.最近在自己的电脑上安装Mathtype7,把遇到的问题和解决办 ...

  3. NCF 的Azure Cosmos DB 演示案例

    简介 NCF想必看过我之前发的NCF的文章的同学们都已经很熟悉了 今天我们要来聊一聊的是NCF遇到Azure Cosmos DB后会碰撞出什么样的火花,让我们一起往下看 我们先来说说什么是Azure ...

  4. BUUCTF-[BJDCTF2020]藏藏藏

    [BJDCTF2020]藏藏藏 打开图片发现存在压缩包,使用foremost分离一下. 得到压缩包,直接可以解压. 解码一下就可以得到flag

  5. NFS网络文件系统搭建

    1. 简介 NFS, 就是network file system的简称. 可以通过NFS, 来共享不同主机的文件.目录. 2010年,NFS已经发展到v4.1版本. 2. 应用场景 在中小型企业中,N ...

  6. 在.NET 6.0上使用Kestrel配置和自定义HTTPS

    大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进. 本章是<定制ASP NET 6.0框架系列文章>的第四篇.在本章,我们 ...

  7. Python自动化办公:27行代码实现将多个Excel表格内容批量汇总合并到一个表格

    序言 (https://jq.qq.com/?_wv=1027&k=GmeRhIX0) 老板最近越来越过分了,快下班了发给我几百个表格让我把内容合并到一个表格内去.还好我会Python,分分钟 ...

  8. [零基础学IoT Pwn] 环境搭建

    [零基础学IoT Pwn] 环境搭建 0x00 前言 这里指的零基础其实是我们在实战中遇到一些基础问题,再相应的去补充学习理论知识,这样起码不会枯燥. 本系列主要是利用网上已知的IoT设备(路由器)漏 ...

  9. C语言动态输出等腰三角形

    C语言动态输出等腰三角形 题目要求:输入行数 打印出对应行数的等腰三角形,要求使用for循环嵌套. 思路 while语句写外层死循环 用于判断输出的数据: 分析: 最外层for,来控制最外层行数,存储 ...

  10. C#请求HTTPS地址的故障分析和TLS知识点总结

    背景介绍 近期收到同事反馈,在C#程序中通过HTTPClient请求一个HTTPS的地址时,在本地开发环境和测试环境均能正常执行,而部署到生产环境后发生异常且稳定复现,异常提示为:[请求被中止: 未能 ...