程序员应该写TLB‘喜欢’的代码

一、什么是TLB?

TLB(Translation Lookaside Buffer)转换检测缓冲区是一个内存管理单元,用于改进虚拟地址到物理地址转换速度的缓存。

TLB是一个小的,虚拟寻址的缓存,其中每一行都保存着一个由单个PTE(Page Table Entry,页表项)组成的块。如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据。

根据TLB的功能也许"页表cache”会更加符合吧。

二、TLB的工作原理

TLB在设计上非常靠近CPU而目访问速度非常快,有了TLB,MMU再进行地址转換时会首先去TLB中查找页表映射信息,如果TLB中正好有该信息的话那么接下来将无需访问内存,这将极大的加快地址转换速度,TLB的应用使得页式内存管理方法在地址转换速度上达到了实用的程度。

当MMU从虛拟地址中提取出页号后直接去查找TLB,如果TLB中保存了该页号对于的页表项那么TLB直接返回查找结果和页表项,这被称为TLB命令(TLB hit),接下来的过程想必大家就很清楚啦,如果页表项的保护位允许我们访问该页,那么从虛拟地址中提取出页内偏移,拼接上页帧号就得到最终的物理内存地址了,而如果TLB中并没有该页号对应的页表项信息,那么TLB未命中,即TLB miss。这时将不得不访问一次内存从页表中查找对应的页表项信息,如果页表项表明进程尚未使用该页,那么将产生段错误,segmentationault,此后操作系统将接管系统来处理该进程。而如果页表项表明该进程不能访问该页,那么将产生保护错误,protection fault,此后操作系统同样接管系统来处理该进程;

如果丁LB命中那么本次地址转换速度将会很快;但如果并没有命中,我们将不得不访问一次内存,速度将会非常慢,如果你的程序在执行过程中持续出现TLB不能命中的情况,那么程序运行速度将显著减慢,因此作为程序员我们应该写出对TLB友好的程序,这是什么意思呢?接下来看一个例子就明白啦。

为了更加清楚的展示TLB的作用,在这里我们使用一个简单的访问数组的代码来说明丁LB是如何提高系统性能的。假设程序中使用了一个包含有12个整数的二维数组A,其中:
A[0][0]-A[0][2]占据了第5号页,映射到了第第10号页帧;
A[1][0]-A[1][2]占据了第6号页,映射到了第8号页帧;
A[2][0]-A[2][2]占用了第7号页,映射到了第第13号页帧中;
A[3][0]-A[3][2]占用了第8号页,映射到了第第7号页帧中;

现在需要计算数组元素的和,代码如下:

int nsum = 0;
for(int i = 0;i<4;i++)
{
	for(int j = 0;j<3;j++)
	{
		nsum+=A[i][j];
	}
}

该代码中首先访问的是A[0][0], CPU向内存发出读取A[0][0]指令后,MMU提取A[0][0]虛拟地址(&A[0][0])所在的页号,即5,然后去TLB中查找是否有页号5的信息,显然TLB中还没有第5号页的信息(假设开始时TLB为空,这并不会影响我们的讨论),因此此次访问TLB不能命中,这时将不得不访问一次内存,查找页表后将第5号页的信息写入TLB,重新执行此次的机器指令,由于第5号页的映射信息已经写入TLB,那么这时TLB命中,MMU将虚拟地址转换为物理内存地址,该物理内存地址就是A[0][0]所在的
物理内存地址。

接下来访问的是A[0][1],A[0][1]和A[0][0]在同一页中,也就是第5号页,由于在访问A[0][0]时,第5号的映射信息已经装入TLB中,因此本次虛拟地址转换查找TLB将会命中(图书管理员这次不用再跑一趟啦),同样的道理由于A[0][2]和A[0][1]以及A[0][0]在同一页,因此访问A[0][2]时TLB也将命中.

访问完A[0][0]-A[0][2]后,CPU继续执行指令访问A[1][0]-A[1][2],这个和访问A[0][0]-A[0][2]类似就不再说了。

接下来看另外一个反例

int nsum = 0;
for(int j = 0;j<3;j++)
{
	for(int i = 0;i<4;i++)
	{
		nsum+=A[i][j];
	}
}

这段代码和之前的相比,仅仅就是改变了数组的遍历顺序而已,第一段代码是按行遍历,这段代码是按列来遍历。

首先数组中的A[0][0]被访问,A[0][0]位于进程的第5号页中,因此地址转换时需要在TLB中查找第5号页的映射信息,刚开始时TLB为空,因此TLB不会命中,这种情况下将不得不访问一次内存,将页表中第5页的映射信息写入下LB中,此后重新执行访问A[0][0]的机器指令并成功从TLB中查找到相应的信息。

接下来访问的是A[1][0],注意到和按行遍历不同的是,A[1][0]位于第6页中A[1][0]并没有和A[1][0]位于同一页,因此访问A[1][0]时写入TLB的映射信息在此时起不了作用,也就是说访问A[1][0]进行地址转换时依然无法命中TLB,我们依然需要访问内存将第6号页的映射信息写入TLB中,此后重新执行访问A[1][0]的机器指令,该过程和之前一样.

因为TLB的容量很小,所以不足于将A[0][0],A[1][0],A[2][0],A[3][0]的信息都存下,当访问A[0][1]的时候,A[0][0]的信息早已被覆盖,又得走A[0][0]的路子。所以上述代码空间局部性很差。

三、案例对比

下面给个实际的例子解说一下:

int a[100000][1000] = { 0,1,2,3,4 };

void fun1()
{
	a[100][2] = 100;
	a[99][1] = 200;
	a[672][11] = 300;
	
	clock_t t1 = clock();
	int nsum = 0;
	for (int i = 0;i<100000;i++)
	{
		for (int j = 0;j< 1000;j++)
		{
			nsum += a[i][j];
		}
	}
	clock_t t2 = clock();
	cout <<"按行访问时间:"<< t2 - t1 << endl;
	cout << "求和:" << nsum << endl;
}

void fun2()
{
	a[100][2] = 100;
	a[99][1] = 200;
	a[672][11] = 300;

	clock_t t1 = clock();
	int nsum = 0;
	for (int i = 0; i < 1000; i++)
	{
		for (int j = 0; j < 100000; j++)
		{
			nsum += a[j][i];
		}
	}
	clock_t t2 = clock();
	cout << "按列访问时间:" << t2 - t1 << endl;
	cout << "求和:"<<nsum << endl;

}
void main()
{
	fun1();
	fun2();
	
	system("pause");
}

结果:
第一次执行
在这里插入图片描述
第二次执行:
在这里插入图片描述
第三次执行:
在这里插入图片描述
调换顺序执行:

void main()
{
	fun1();
	fun2();
	
	system("pause");
}

第一次
在这里插入图片描述
第二次:
在这里插入图片描述
第三次:
在这里插入图片描述
从结果看出,同样的数据,遍历花费的时间差别很大。因此作为程序员,这给我们的启示就是如果对性能要求较高那么我们就必须写出对下LB友好的程序,也就是说我们的程序要有很好的局部性来提高TLB的cache命中率从而加快程序执行速度。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

发如雪-ty

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

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

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

打赏作者

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

抵扣说明:

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

余额充值