深入理解计算机基础第六章

说明

读书笔记

物理介质

高速缓存模型


当CPU发送了一个内存访问请求时(地址记为s),发生了
1.硬件首先计算s的组id,并到该组检查s是否在某个块中
2.把s映射为缓存地址 : 把缓存也看成一个大号数组,那么需要通过组号、块号、块内偏移,计算出s在缓存内的地址
3.返回缓存被访问的东西
这些步骤需要大量的位运算(与、位移、或),被固化到硬件里面

直接映射高速缓存(若干组,每组一行)

比较简单,注意冷不命中和抖动
2的幂的数组通常引起不命中

组相连高速缓存(若干组,若干行)

命中时的缓存地址计算同直接映射高速缓存
主要是不命中时间的组内块的替换策略

0.最优替换 OPT

可以O(nlogm)的时间内求出,方法是通过两个set维护缓存块的信息,每次删除的时候,把下一次出现最远的块删除就行
注意每次循环,都需要刷新该set,具体操作的时候需要在set的begin和end都进行一些操作

/*
	From XDU'mzb
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
int main () {
	ll n,m;
	cin >> n >> m;
	static ll date[100000 + 110];
	static ll nex[100000 + 110];
	for (ll i = 1;i <= n;++i) {
		cin >> date[i];
	}
	{
		map<ll,ll> has;
		nex[n + 1] = nex[n + 1];
		for (ll i = n;i >= 1;--i) {
			if (has.find(date[i]) == has.end()) {
				has[date[i]] = i;
				nex[i] = n + 1;
			}
			else {
				nex[i] = has[date[i]];
				has[date[i]] = i;
			}
		}
	}
	set<pair<ll,ll>> q; 
	set<ll> has;
	ll ret = 0;
	for (ll i = 1;i <= n;++i) {
		while (q.size() and q.begin() -> first < i) {
			auto now = *q.begin();q.erase(q.begin());
			q.emplace(nex[now.first],now.second);
		}
		if (has.find(date[i]) != has.end()) {
		}
		else if (has.size() < m) {
			has.insert(date[i]);
			q.emplace(nex[i],date[i]);
			ret++;
		}
		else {
			auto now = *--q.end();q.erase(--q.end());
			has.erase(now.second);
			has.insert(date[i]);
			q.emplace(nex[i],date[i]);
			ret++;
		}	
	}
	cout << ret;

	return 0;
}


每次把不再使用的块或者0块替换掉,否则把cache中下一次出现位置最远的换掉
这个信息是强制离线的,所以无法实现
复杂度O(n * sizeof(cache))

/*
---- From XDU's mzb
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
pair<ll,vector<vector<ll>>> opt(vector<ll> v,ll num_of_cache)
  {
  	ll hits = 0;
  	vector<vector<ll>> ret = vector<vector<ll>>(v.size(),vector<ll>(num_of_cache));
	vector<map<ll,ll>> nex_pos = vector<map<ll,ll>>(v.size(),map<ll,ll>());
  	for (ll i = v.size() - 1;i >= 0;i--)
  	  {
  	  	if (i == v.size() - 1)
  	  	  {
  	  	  	nex_pos[i][v[i]] = i;
			  }
		else
		  {
		  	nex_pos[i] = nex_pos[i + 1];
		  	nex_pos[i][v[i]] = i;
		  }
		}
	ret[0][0] = v[0];
  	for (ll i = 1;i < v.size();i++)
  	  {
  	  	ret[i] = ret[i - 1];
  	  	auto &now = ret[i];
  	  	if (count(now.begin(),now.end(),v[i]))
		  {
		  	hits++;
		  }
		else if (count(now.begin(),now.end(),0ll))
  	  	  {
  	  	  	for (auto &it : now)
  	  	  	  if (it == 0)
  	  	  	    {
  	  	  	    	it = v[i];
  	  	  	    	break;
					  }
			  }
		else 
		  {
		  	ll pos = 0;
		  	for (ll k = 0;k < now.size();k++)
		  	  {
		  	  	if (nex_pos[i][now[k]] == 0)
		  	  	  {
		  	  	  	pos = k;
		  	  	  	break;
					  }
		  	  	if (nex_pos[i][now[k]] > nex_pos[i][now[pos]])
		  	  	  {
		  	  	    pos = k;
					  }
				}
			now[pos] = v[i];
		  }
		}
  	return make_pair(hits,ret);
  }
int main()
  {
  	ll hits;
  	vector<vector<ll>> ret;
  	auto v = vector<ll>({2,3,2,1,5,2,4,5,3,2,5,2});
  	tie(hits,ret) = opt(v,3);
  	cout << "hits = " << hits << "/" << v.size() << " = " << 1.0 * hits / v.size() * 100 << "%\n";
  	for (auto v : ret)
  	  {
  	  	for (auto it : v)
  	  	  cout << it << " ";cout << "\n";
		}
	return 0;
  }

1.先进先出,FIFO

复杂度O(n * sizeof(cache)),和opt的区别在于离线在线

/*
---- From XDU's mzb
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
pair<ll,vector<vector<ll>>> fifo(vector<ll> v,ll num_of_cache)
  {
  	ll hits = 0;
  	vector<vector<ll>> ret = vector<vector<ll>>(v.size(),vector<ll>(num_of_cache));
  	ll pre = 0;
  	ret[0][(pre++) % num_of_cache] = v[0];
  	for (ll i = 1;i < v.size();i++)
  	  {
  	  	if (find(ret[i - 1].begin(),ret[i - 1].end(),v[i]) == ret[i - 1].end())
  	  	  {
  	  	  	ret[i] = ret[i - 1];
  	  	  	ret[i][(pre++) % num_of_cache] = v[i];
			  }
		else
		  {
		  	hits++;
		  	ret[i] = ret[i - 1];
		  }
		}
  	return make_pair(hits,ret);
  }
int main()
  {
  	ll hits;
  	vector<vector<ll>> ret;
  	auto v = vector<ll>({2,3,2,1,5,2,4,5,3,2,5,2});
  	tie(hits,ret) = fifo(v,3);
  	cout << "hits = " << hits << "/" << v.size() << " = " << 1.0 * hits / v.size() * 100 << "%\n";
  	for (auto v : ret)
  	  {
  	  	for (auto it : v)
  	  	  cout << it << " ";cout << "\n";
		}
	return 0;
  }

2.最不常使用 LFU

就是每次命中、换入一个块,该快的计数清空,然后给其他块计数 +1
替换的时候把计数最大的块换出去
O(n * sizeof(cache))的算法很容易

/*
---- From XDU's mzb
*/
#include <bits/stdc++.h>
using namespace std;
using ll = long long int;
pair<ll,vector<vector<ll>>> lru(vector<ll> v,ll num_of_cache)
  {
  	ll hits = 0;
  	vector<vector<ll>> ret = vector<vector<ll>>(v.size(),vector<ll>(num_of_cache));
  	vector<ll> cnt(num_of_cache),now(num_of_cache);
	ll level = 0;
	ret[0][0] = now[0] = v[0];
	cnt[0] = --level;
  	for (ll i = 1;i < v.size();i++)
  	  {
  	  	if (count(now.begin(),now.end(),v[i]))
  	  	  {
  	  	  	hits++;
  	  	  	for (ll k = 0;k < num_of_cache;k++)
  	  	  	  {
  	  	  	  	if (now[k] == v[i])
  	  	  	  	  {
  	  	  	  	  	cnt[k] = --level;
  	  	  	  	  	break;
						  }
					}
			  }
		else
		  {
		  	ll maxv = *max_element(cnt.begin(),cnt.end());
		  	for (ll k = 0;k < num_of_cache;k++)
		  	  {
		  	  	if (maxv == cnt[k])
		  	  	  {
		  	  	  	now[k] = v[i];
		  	  	  	cnt[k] = --level;
		  	  	  	break;
					  }
				}
		  }
		ret[i] = now;	
		}
  	return make_pair(hits,ret);
  }
int main()
  {
  	ll hits;
  	vector<vector<ll>> ret;
  	auto v = vector<ll>({2,3,2,1,5,2,4,5,3,2,5,2});
  	tie(hits,ret) = lru(v,3);
  	cout << "hits = " << hits << "/" << v.size() << " = " << 1.0 * hits / v.size() * 100 << "%\n";
  	for (auto v : ret)
  	  {
  	  	for (auto it : v)
  	  	  cout << it << " ";cout << "\n";
		}
	return 0;
  }

3.最近最少使用 LRU

这个办法非常暴力,每访问一次,被访问的块的计数器 + 1
需要替换的时候,就把计数值最小的块替换出去,然后所有计数器清零

全相连高速缓存(一组,若干行)

值得注意的是,可以由电路并行搜索块
只在TLB中有应用

有关写内存的注意点

直写 : 立即将w的高速缓存块写回下一层
写回 : 在包含w的高速缓存块被驱逐时才写回下一层

如何处理写不命中

写分配(回写高速缓存一般是这样的) : 加载相应的低一层的块到高速缓存中,然后更新这个高速缓存块
非写分配(直写高速缓存一般是这样的): 避开高速缓存,直接把这个块写到低一层
一般都是写回高速缓存 + 写分配,因为复杂的硬件电路越来越不是问题

真实的高速缓存剖析

衡量性能的指标

1.不命中率 = \(\frac{不命中数量}{引用数量}\)
2.命中时间 : 从高速缓存传一个字到CPU的时间
3.不命中处罚

特点概括

1.一方面,较大的缓存会提高命中率,但是常数会大一些
2.块越小命中率越小,同时常数更小
3.较高的相连度(也就是E的值比较大),会降低抖动的可能性、同时提高硬件电路复杂性,提高常数
4.写策略的影响

posted @ 2021-10-01 17:13  XDU18清欢  阅读(46)  评论(0)    收藏  举报