第3章“程序的机器级表示”:数组分配与访问

news/2024/11/8 22:46:55/

文章目录

  • 概述
  • 3.8.1 基本原则
  • 3.8.2 指针运算
  • 3.8.3 数组与循环
  • 3.8.4 嵌套数组
  • 3.8.4 固定大小的数组
  • 3.8.5 动态分配的数组

概述

C 中数组是一种将标量型数据聚集成更大数据类型的方式。C用来实现数组的方式非常简单,因此很容易翻译成机器代码。C的一个不同寻常的特点是可以对数组中的元素产生指针,并对这些指针进行运算。这些运算会在汇编代码中翻译成地址计算。

优化编译器非常善于简化数组索引所使用的地址计算,弊端是使得 C 代码和它到机器代码的翻译之间的对应关系很难理解。

3.8.1 基本原则

对于数据类型 T T T 和 整常数 N N N,声明:

T A[N];

有两个效果:

首先,它在存储器中分配了 L ∗ N L * N LN 字节的连续区域,此处的 L L L 是数据类型 T T T 的大小(单位为字节)。我们用 x A x_A xA 来表示起始位置。

其次,它引入了标识符 A A A A A A 可以用来作为指向数组开头的指针。这个指针的值就是 x A x_A xA

可以用 0 ~ N − 1 N-1 N1 之间的整数索引来访问数组元素。数组元素 i i i 的存放地址为 x A + L ∗ i x_A + L *i xA+Li

举例说明:

char A[12];
char *B[8];
double C[6];
double *D[5];

这些声明会产生带下列参数的数组:

在这里插入图片描述
数组 A A A 由 12 个单字节(char)元素组成。数组 C C C 由 6 个双精度浮点值组成,每个值需要 8 个字节。 B B B D D D 都是指针数组,因此每个数组元素都是 4 个字节。

IA32 的存储器引用指令被设计用来简化数组访问。例如,假设 E E E 是一个整数数组,而想计算 E [ i ] E[i] E[i],在此, E E E 的地址存放在寄存器 %edx 中,而 i i i 存放在寄存器 %ecx 中。然后,指令

movl (%edx, %ecx, 4), %eax

会执行地址计算 x E + 4 i x_E + 4i xE+4i,在该存储器位置执行读操作,并将结果存放在寄存器 %eax 中。提示:伸缩因子1、2、4、和 8 适用于基本数据类型的大小。

3.8.2 指针运算

C允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行调整。即,如果 p p p 是一个指向类型 T T T 的数据的指针, p p p 的值为 x p x_p xp,表达式 p + i p+i p+i 的值为 x p + L ∗ i x_p + L * i xp+Li,这里 L L L 是数据类型 T T T 的大小。

单操作数的操作符 &* 可以产生指针和间接引用指针。即,对于一个表示某个对象的表达式Expr,&Expr 表示一个地址。对于表示一个地址的表达式 Addr-Expr,*Addr-Expr 表示该地址中的值。因此,表达式 Expr*&Expr 是等价的。可以对数组和指针应用数组下标操作,如数组引用 A [ i ] A[i] A[i] 与表达式 ∗ ( A + i ) *(A+i) (A+i) 是一样的。它计算第 i i i 个数组元素的地址,然后访问这个存储器位置。

扩充一下前面的例子,假设整数数组 E 的起始地址和整数索引 i i i 分别存放在寄存器 %edx 和 %ecx 中。下面是一些与 E 有关的表达式以及每个表达式的汇编代码实现,结果存放在寄存器 %eax 中。

表达式类型汇编代码
E E Eint * x E x_E xEmovl %edx, %eax
E [ 0 E[0 E[0]int M [ x E ] M[x_E] M[xE]movl (%edx), %eax
E [ i ] E[i] E[i]int M [ x E + 4 i ] M[x_E + 4i] M[xE+4i]movl (%edx, %ecx, 4), %eax
& E [ 2 ] E[2] E[2]int * x E + 8 x_E + 8 xE+8leal 8(%edx), %eax
E + i − 1 E+i-1 E+i1int * x E + 4 i − 4 x_E+4i-4 xE+4i4leal -4(%edx, %ecx, 4), %eax
*(& E [ i ] + i ) E[i] + i) E[i]+i)int M [ x E + 4 i + 4 i ] M[x_E + 4i + 4i] M[xE+4i+4i]movl (%edx, %ecx), %eax
& E [ i ] − E E[i] - E E[i]Eint i i imovl %ecx, %eax

这些例子中,leal 指令用来产生地址,而 movl 用来引用存储器(除了第一种情况是拷贝一个地址)。最后一个例子表明可以计算同一个数组结构中的两个指针之差,结果值是除以数据类型大小后的值。

3.8.3 数组与循环

循环代码内,对数组的引用通常有非常规则的模式,优化编译器会使用这些模式。

例如,下面(a) 中的函数 decimal5,计算的是一个由 5 个十进制数字表示的整数。在把这个函数转换成汇编代码的过程中,编译器产生的代码类似于 b 中的 C 函数 decimal5_opt

//(a) 原始C代码
int decimal5(int *x)
{int i;int val = 0;for (i = 0; i < 5; i++)val = (10 * val) + x[i];return val;
}
//(b) 等价的指针代码
int decimal5_opt(int *x)
{int val = 0;int *xend = x + 4;do {val = (10 * val) + *x;x++;} while (x <= xend);return val;
}

首先,它不会使用循环变量 i i i,而是用指针运算来依次遍历数组元素。它计算出最后一个数组元素的地址,并且把与这个地址的比较作为循环测试。最后,它能使用do-while 循环,因为至少要执行一次循环体。
在这里插入图片描述

(c) 中的代码给出了一个进一步的优化,以避免使用整数乘法指令。特别地,它使用 leal(第5行)来计算 5 ∗ v a l 5 *val 5val 作为 v a l + 4 ∗ v a l val + 4 *val val+4val。然后,用伸缩因子值为 2 的 leal (第7行)使之扩展为 10 ∗ v a l 10*val 10val

为什么要避免使用整数乘法?

在较老的 IA32 处理器模型中,整数乘法指令要花费 30 个时钟周期,所以编译器要尽可能地避免使用它。而在大多数新近的处理器模型中,乘法指令只需要 3 个时钟周期,所以不一定会进行这样的优化了。

3.8.4 嵌套数组

即使是创建数组的数组时,数组分配和引用的通用原则也是有效的。

例如,声明

int A[4][3];

等价于声明

typedef int row3_t[3];
row3_t A[4];

数据类型 row3_t 被定义成一个三个整数的数组。数组 A 包含有四个这样的元素,每个都需要 12 个字节来存放三个整数。所以,总的数组大小为 4 * 4 * 3 = 48 字节。

数组 A 还可以看成是一个 4 行 3 列的二维数组,从 A[0][0] 到 A[3][2]。数组元素在存储器中是按照 “行优先” 的顺序排列的,这就意味着先是行 0 的所有元素,后面是行 1 的所有元素,依此类推。

在这里插入图片描述
这种排序方法是嵌套声明的结果。将 A 看成一个四元素数组,每个元素又是一个三个 int 的数组,先有 A[0](也就是行0),后面是 A[1],依此类推。

要访问多维数组中的元素,编译器产生的代码要计算待访问元素的偏移,然后再用 movl 指令,以数组的起始作为基地址,偏移(可能需要乘以伸缩因子)作为索引。通常,对一个声明如下的数组:

T D[R][C]

数组元素 D[i][j] 是位于存储器地址 x D + L ( C ∗ i + j ) x_D + L(C *i + j) xD+L(Ci+j) 的,这里 L L L 是用字节表示的数据类型 T T T 的大小。

如下例子,考虑前面定义的 4 × 3 4 \times 3 4×3 的整数数组。假设寄存器 %eax 包含 x A x_A xA,%edx 保存着 i i i,而 %ecx 保存着 j j j。然后,下面的代码将拷贝数组元素 A [ i ] [ j ] A[i][j] A[i][j] 到寄存器 %eax:

在这里插入图片描述

3.8.4 固定大小的数组

对固定大小的多维数组进行操作的代码,C编译器能够进行多种优化。

例如,假设将数据类型 fix_matrix 声明为 16 × 16 16 \times 16 16×16 的整数数组:

#define N 16
typedef int fix_matrix[N][N];

如下的 (a) 中的代码计算矩阵 A 和 B 的乘积的元素 i i i k k k。C编译器产生的代码类似于 (b)中所示的那样,这段代码包含很多聪明的优化。

//(a) 原始的C代码
#define N 16
typedef int fix_matrix[N][N];/* Compute i,k of fixed matrix product */
int fix_prod_ele(fix_matrix A, fix_matrix B, int i, int k)
{int j;int result = 0;for (j = 0; j < N; j++)result += A[i][j] * B[j][k];return result;
}
//(b) 优化过的C代码
/* Compute i,k of fixed matrix product */
int fix_prod_ele_opt(fix_matrix A, fix_matrix B, int i, int k)
{int *Aptr = &A[i][0];int *Bptr = &B[0][k];int cnt = N - 1;int result = 0;do {result += (*Aptr) * (*Bptr);Aptr += 1;Bptr += N;cnt--;} while (cnt >= 0)return result;
}

编译器认出循环会依次访问数组 A 的元素 A[i][0],A[i][1],…,A[i][15]。这些元素占据的是存储器中从数组元素 A[i][0] 的地址开始的相邻的位置。因此,程序可以用指针变量 Aptr 来访问这些连续的位置。

循环会依次访问数组B 的元素 B[0][k],B[1][k],…,B[15][k]。这些元素占据的是存储器中从数组元素 B[0][k] 的地址开始的位置,分别相距 64 个字节。因此,程序可以用指针变量 Bptr 来访问这些连续的位置。在 C 中,这个指针会增加 16,尽管实际上真实的指针会增加 4 * 16 = 64。最后,代码可以用一个简单的计数器来记录循环的次数。

给出了 fix_prod_ele_opt 的 C 代码,来说明 C 编译器产生汇编时所使用的优化。如下是这个循环的实际的汇编代码:

在这里插入图片描述
注意,上面的汇编代码中,所有的指针增加量均乘以伸缩因子值4。

3.8.5 动态分配的数组

C只支持大小在编译时就能知道的多维数组(第一维可能有些例外)。在许多应用程序中,需要代码能够对动态分配的任意大小的数组进行操作。为此,必须显式地写出从多维数组到一维数组的映射。可以将数据类型 var_matrix 简单地定义为 int *

typedef int *var_matrix;

用 Unix 的库函数 calloc 来为一个 n × n n \times n n×n 的整数数组分配和初始化存储:

var_matrix new_var_matrix(int n)
{return (var_matrix)calloc(sizeof(int), n * n);
}

calloc 函数有两个参数:每个数组元素的大小和所需数组元素的数目。它试着为整个数组分配空间。如果成功,会将整个存储器区域初始化为0,并返回指向第一个字节的指针。如果没有足够的可用空间,它就返回空(null)。

C、C++ 和 Java 中的动态存储器分配和释放

在 C 中,堆(一个可以用来存放数据结构的存储器池)中的存储分配是用的库函数 malloccalloc。它们的效果类似于 C++ 和 Java 中的 new 操作。C 和 C++ 都要求程序显式地用 free 函数来释放已分配的空间。在 Java 中,释放是由运行时系统通过一个称为 garbage collection(垃圾回收)的进程自动完成的。

然后,用行优先顺序的数组下标计算方法确定矩阵元素 i i i j j j 的位置 i ∗ n + j i * n + j in+j

int var_ele(var_matrix A, int i, int j, int n)
{return A[(i * n) + j];
}

翻译成汇编代码是这样的:

在这里插入图片描述

这段代码与用来计算固定大小数组的下标的代码相比,看到动态版本更加复杂。它必须用一条乘法指令来将 i i i 增大 n n n 倍,而不是用一组移位和加法指令。在现代处理器中,这种乘法并不会带来严重的性能损失。

在许多情况中,编译器可以使用相同于已描述的固定大小数组的优化原则,来简化大小可变数组的下标计算。

如下(a) 给出的 C 代码,计算的是两个大小可变矩阵 A 和 B 的乘积的元素 i i i k k k

在(b) 中,给出了一个优化过的版本,它是根据编译原始版本产生的汇编代码逆向生成的。编译器可以利用由循环结构产生的顺序访问模式,消除整数乘法 i ∗ n i * n in j ∗ n j * n jn。在这种情况中,编译器不会产生指针变量Bptr,而是创建一个称为 nTjPk(表示 “ n n n 乘以 j j j 加上 k k k”)的整数变量,因为相对于原始代码,它的值等于 n ∗ j + k n * j + k nj+k。最开始时, n T j P k nTjPk nTjPk 等于 k k k,每次循环时都增加 n n n

//(a)原始C代码
typedef int *var_matrix;/* Compute i,k of variable matrix product */
int var_prod_ele(var_matrix A, var_matrix B, int i, int k, int n)
{int j;int result = 0;for (j = 0; j < n; j++)result += A[i * n + j] * B[j * n + k];return result;
}
//(b)优化过的C代码
/* Compute i,k of variable matrix product */
int var_prod_ele_opt(var_matrix A, var_matrix B, int i, int k, int n)
{int *Aptr = &A[i * n];int nTjPk = n;int cnt = n;int result = 0;if (n <= 0)return result;do {result += (*Aptr) * B[nTjPk];Aptr += 1;nTjPk += n;cnt--;} while (cnt):return result;
}

编译器为循环产生代码,其中寄存器 %edx 保存 cnt,%ebx 保存 Aptr,%ecx 保存 nTjPk,而 %esi
保存结果。这段代码如下:

在这里插入图片描述

注意,每次循环时,变量 B 和 n n n 都必须从存储器中读出。这是一个寄存器溢出(register spilling) 的例子。没有足够的寄存器来保存所有需要的临时数据,因此编译器必须将某些局部变量放在存储器中。此时,编译器会选择溢出变量 B 和 n n n ,因为它们只用读一次——在循环里,它们的值不变。寄存器溢出是 IA32 一个很常见的问题,因为处理器的寄存器数量太少了。


http://www.ppmy.cn/news/234575.html

相关文章

海信电视如何安装应用软件(WPS)

1.下载APK文件到U盘 2.需在应用设置里把电视设置成商场模式; 3.信号源切换到U盘

海信电视通过U盘安装应用

1、U盘格式&#xff1a;FAT32 或者 NTFS 2、下载 .apk 应用文件 3、插入到电视USB接口 4、按步骤安装即可

海信电视怎么删除自带应用

准备步骤 将电视通过无线或者有线连接网络&#xff0c;在设置里查看电视的IP地址&#xff0c;系统设置&#xff0d;网络设置&#xff0d;网络详情&#xff1b;用和电视共用一个网络的电脑下载root工具并解压&#xff0c;下载地址&#xff1a; 电视root工具.rar 二&#xff0…

电视家海信html安装不了,海信电视怎么看电视直播,安装电视家3.0解决方法

海信电视一直致力研发&#xff0c;技术和产品性能一直保持在同行业领先地位&#xff0c;更是凭借“智能大屏AI模式”&#xff0c;高度迎合消费者需求&#xff0c;给海信用户带来了非常不错的视听体验。但是海信电视同其他智能电视一样无法直接观看电视直播节目&#xff0c;那海…

海信计算机怎么开机,海信电视怎么开机

市面上电视品牌众多&#xff0c;其中海信电视作为老品牌&#xff0c;深受广大消费者的喜爱&#xff0c;那么海信电视怎么开机呢&#xff1f; 一、海信电视怎么开机 有两种方式&#xff0c;一种用遥控器&#xff0c;一种用手机。先介绍下遥控器开机&#xff0c;首先将遥控器对准…

计算机找不到海信电视,海信电视到底怎么投屏,看这一篇就够了!

海信电视到底怎么投屏,看这一篇就够了! 发布时间:2019/10/21 00:00 海信电视到底怎么投屏,看这一篇就够了! 海信电视手机电脑投屏汇总。 智能电视最大的特点就是屏幕大, 他第一个核心便是看。有人直接在电视上拿遥控器找内容。也有人使用手机投屏的方式看海信等电视。所以…

蓝光电视再降价 海信42吋液晶不足五千

[中关村在线电视频道原创]作为首款具备蓝光功能的 液晶电视—— 海信42V67PK液晶电视本周价格再次下调&#xff0c;目前售价已经跌破5000元大关&#xff0c;这款电视具备H.264全高清影片解码功能&#xff0c;同时具备SRS虚拟环绕 音箱&#xff0c;对蓝光电视感兴趣的朋友可以关…

SAP VK11税码的理解

背景:销售订单税率获取逻辑 1.工厂&#xff0c;工厂定义维护了国家代码信息 (SO–工厂–国家代码) 2.客户主数据通用数据维护了国家代码 (SO-客户–国家代码) 3.客户主数据销售数据维护了国家代码对应的税分类&#xff08;此国家代码由销售组织对应国家代码维护的国家代码带出来…