参考教材:计算机系统基础 第二版 袁春风 机械工业出版社
参考慕课:计算机系统基础(四):编程与调试实践 https://www.icourse163.org/learn/NJU-1449521162
计算机系统实验导航
实验一:环境安装 https://blog.csdn.net/weixin_46291251/article/details/122477054
实验二:数据的存储与运算 https://blog.csdn.net/weixin_46291251/article/details/122478255
实验三:程序的机器级表示 https://blog.csdn.net/weixin_46291251/article/details/122478979
实验四:二进制程序逆向工程 https://blog.csdn.net/weixin_46291251/article/details/122479554
实验五:缓冲区溢出攻击 https://blog.csdn.net/weixin_46291251/article/details/122479798
实验六:程序的链接 https://blog.csdn.net/weixin_46291251/article/details/122480049
实验源码: xxx
准备
实验内容:
1 课程实验平台环境的安装,基本实验工具的使用;
2 从高级语言的角度展示和解释位运算、浮点数运算的精度、cache对程序性能的影响。
实验目标:
1 完成课程实验平台环境的搭建与设置;掌握常用实验工具的基本使用方法;
2 掌握C语言中位操作语句的使用;了解浮点数表示精度在浮点数运算中的影响;了解cache、数据存储与访问模式对程序性能的影响,掌握编写cache友好代码的基本原则。
实验任务:
1 学习MOOC内容
https://www.icourse163.org/learn/NJU-1449521162
- 第一周 实验与开发环境的安装和使用
第2讲 虚拟机、Linux及其上实验环境的安装
第3讲 基本实验工具的使用 - 第二周 C语言编程实践
第1讲 数据的位运算操作
第2讲 浮点数的精度问题
第3讲 Cache友好代码
2 在自己的电脑上安装实验环境
安装虚拟机软件:VirtualBox 6.0.8开源软件
安装Linux系统:Linux Debian 32位
熟悉软件工具:gcc,gdb,objdump
题目一:不使用中间变量,交换变量a和b的值
编写C语言程序,不使用中间变量,交换变量a和b的值,已知变量的初始值为a=2021,b=191,分析程序的反汇编代码,说明算法的基本原理。
程序代码和注释说明
1.#include <stdio.h>
2.#include <stdlib.h>
3.
4.int main()
5.{
6. int a=2021,b=191;
7. printf ("before swapping: a=%d, b=%d\n",a,b);
8.
9. a = a^b;
10. b = b^a;//b=b^(a^b)=a
11. a = a^b;//a=(a^b)^a=b
12. printf("after swapping: a=%d, b=%d\n",a,b);
13.}
第一步:b=b(ab)=a
第二步:a=(ab)a=b
然后就完成了数值交换并且没有使用其他中间变量。
实验结果记录
可见实现了不使用中间变量,交换变量a和b的值
题目二:编写C语言程序,说明浮点数运算误差问题,并给出解决方案。
注:可以参考kahan累加算法的例子,MOOC内容(第二周 第2讲 浮点数的精度问题);也可以采用其他算例,分析运行效果,说明算法的基本原理。
程序代码和注释说明
1.#include<stdio.h>
2.void main()
3.{
4. float sum = 0;
5. float sum1 =0;
6. float c = 0;
7. float y, t;
8. int i;
9. for (i=0;i<4000000;i++)
10. sum1+=0.1;
11.
12. for( i=0;i<4000000;i++)
13. {
14. y=0.1-c;
15. t=sum+y;
16. c = (t-sum) -y;
17. sum = t;
18. }
19.
20. printf ("sum1=%f\n" , sum1) ;
21. printf ("sum =%f\n" , sum) ;
22.}
上述代码利用两种方法对0.1累加四百万次,并分别输出累加后的结果。
实验结果记录
第一个值为直接累加得到的,第二个值为kahan算法累加得到的。这里的c是累计产生的误差,y经过误差修正后的加数,t是经过本次累加后的和,t-sum为本次累加实际加上的加数。
通过上述运行结果可知浮点数相加会带来一定的误差。浮点数的运算中有对阶、舍入、溢出等问题,导致运算结果会出现大数吃小数、精度误差、结果异常等问题。
截断误差大的原因:
1.据浮点数的定义,浮点数的值越大,在实数轴上越靠右,相邻的两个能表示的浮点数的间距也越大,这就造成截断误差越大。
2.浮点数进行加减法时需要对阶,而且是小阶码向大阶码对齐,尾数右移,移出的比特舍弃。这样两个加数的值相差的越多,较小的数的尾数舍弃的比特也就越多,造成的误差越大。
所有这些作用在400w次加法上,也就造成了15475.21875的误差,所以一定要重视浮点数累加的精度问题。
解决方案:
代码如上:
其中c表示累积误差,初始值为0,y是下一个加数经过调整后的结果,然后用调整过后的y加上sum,对其舍入后得到10003.1。显然此时已经产生了舍入误差,如何计算该误差呢?可以用10003.1减去加数之前的和也就是10000,然后减去添加的加数也就是3.14159,这样就得到了这次加法的一个累积误差,循环往复即可得到正确值。
该算法的主要思想是:设法计算出每次累加所带来的的舍入误差,并将其添加在下一次的加数上,这样就可以获得更为准确的结果。使用前提是尽量在浮点数数值相近时进行加减计算,才能充分利用有效位数。
题目三:编写C语言程序,实现两个1024*1024的浮点数矩阵相乘,采用不同的循环顺序,比较运行效果,并分析导致差异的原因。
程序代码和注释说明
如果我们需要在某个任务中多次使用一个数据时,应尽可能地一次性使用完,这主要是利用了数据的时间局部性。在访问数组时应依次地访问元素,这利用了数据的空间局部性。因为当我们访问一个内存地址单元时会将同一块的数据,同时从内存读入Cache,这样如果我们继续访问附近的数据,那它已经位于Cache中,访问速度就会很快。
1.#include <stdio.h>
2.#include <stdlib.h>
3.#include <sys/time.h>
4.#include <time.h>
5.
6.void multMat1( int n, float *A,float *B, float *C ) {
7. int i,j,k;
8. /* This is ijk 1oop order. */
9. for(i=0;i<n;i++)
10. for(j=0;j<n;j++)
11. for(k=0;k<n;k++)
12. C[i+j*n] += A[i+k*n] *B[k+j*n] ;
13.}
14.
15.
16.void multMat2( int n,float *A,float *B, float *C ) {
17. int i,j,k;
18. /* This is ikj loop order. */
19. for(i=0;i< n; i++ )
20. for(k=0;k<n;k++)
21. for(j=0;j<n;j++)
22. C[i+j*n] += A[i+k*n] *B[k+j*n] ;
23.}
24.
25.void multMat3( int n,float *A,float *B, float *C ) {
26. int i,j,k;
27. /* This is jik loop order. */
28. for(j=0;j<n;j++)
29. for(i=0;i< n; i++ )
30. for(k=0;k<n;k++)
31. C[i+j*n] += A[i+k*n] *B[k+j*n] ;
32.}
33.
34.void multMat4( int n,float *A,float *B,float *C ) {
35. int i,j,k;
36. /* This is ikj loop order. */
37. for(i=0;i< n; i++ )
38. for(k=0;k<n;k++)
39. for(j=0;j<n;j++)
40. C[i+j*n] += A[i+k*n] *B[k+j*n] ;
41.}
42.
43.void multMat5( int n,float *A,float *B,float *C ) {
44. int i,j,k;
45. /* This is kij loop order. */
46. for(k=0;k<n;k++)
47. for(i=0;i< n; i++ )
48. for(j=0;j<n;j++)
49. C[i+j*n] += A[i+k*n] *B[k+j*n] ;
50.}
51.
52.void multMat6( int n,float *A,float *B,float *C ) {
53. int i,j,k;
54. /* This is kji loop order. */
55. for(k=0;k<n;k++)
56. for(j=0;j<n;j++)
57. for(i=0;i< n; i++ )
58. C[i+j*n] += A[i+k*n] *B[k+j*n] ;
59.}
60.
61.
62.
63.
64.int main( int argc, char **argv ) {
65. int nmax = 1024,i,n;
66.
67. void (*orderings[]) (int,float *,float *,float *)= { &multMat1 , &multMat2, &multMat3,&multMat4 , &multMat5 , &multMat6} ;
68. char *names[] = {"ijk","ikj", "jik" ,"jki", "kij", "kji"};
69.
70. float *A = (float *)malloc( nmax*nmax * sizeof (float)) ;
71. float *B = (float *)malloc( nmax*nmax * sizeof (float)) ;
72. float *C = (float *)malloc( nmax*nmax * sizeof (float)) ;
73.
74. struct timeval start, end;
75.
76. /* fill matrices with random numbers */
77. for(i=0;i<nmax*nmax;i++) A[i]=drand48() *2-1;
78. for(i=0;i<nmax*nmax;i++) B[i]=drand48() *2-1;
79. for(i=0;i<nmax*nmax;i++) C[i]=0;
80.
81.
82. for(i=0;i<6;i++)
83. {
84. /* multiply matrices and measure the time */
85. gettimeofday( &start,NULL ) ;
86. (*orderings[i]) ( nmax, A, B, C ) ;
87. gettimeofday( &end, NULL ) ;
88.
89. /* convert time to Gflop/s */
90. double seconds =( end.tv_sec - start.tv_sec)+ 1.0e-6*(end.tv_usec - start.tv_usec) ;
91. printf( "%s:\tn = %d, %.3f s\n", names[i] ,nmax, seconds ) ;
92. }
93.}
实验结果记录
执行程序,结果如下:
可见运行用时有较大差异
不管多少维的矩阵,在计算机内部都是顺序存放的,在C语言中,二维矩阵采用的是行优先存储,也就是依次顺序地存储每一行的数据。当我们访问相邻数据时,应当尽可能地遵从空间局部性和时间局部性。
当按照ijk顺序循环时,矩阵B中第j行与矩阵A中第i列对应的元素相乘并累加,得到矩阵C中的第j行第i列元素的值,可以看到矩阵A的访问不是顺序的,而是跨越了一行的数据,因此如果矩阵过大,Cache放不下整个矩阵的数据,矩阵A的访问就会很慢。
与ijk类似,按照jik顺序循环时,矩阵B中第j行,与A中的第i列对应的元素相乘并累加,得到C中的第j行第i列元素的值。矩阵A的访问不是顺序的,而是跨越了一行的数据,因而它的访问速度也不快。
按照jki循环时,矩阵B中第j行第k列元素分别与矩阵A的第k行元素相乘,得到C中的第j行元素的部分值。此时对A和C的访问是顺序的,对Cache的利用最好,因此jki的运行速度最快。
上面的乘法考虑的都是空间局部性,矩阵相乘还有时间局部性问题,以ijk顺序计算为例,矩阵B中每一行依次与A中的每一列相乘并累加,得到C中的一个元素值。矩阵B的时间局部性很好,每一行读取后,与矩阵A中所有的列依次相乘,当它计算完以后,不会再去使用改行的数据,但矩阵A的时间局部性很差,A中的某一列会在不同的时间被多次读取使用。
一个常用的算法是分块矩阵相乘,即分块算法,可以部分减缓该问题。不再以行和列为单位进行计算,将矩阵分成同样大小的若干块,以块为单位进行计算,提升矩阵A的时间局部性。