深入理解计算机基础第六章
说明
读书笔记
物理介质
高速缓存模型
当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.写策略的影响
本文来自博客园,作者:XDU18清欢,转载请注明原文链接:https://www.cnblogs.com/XDU-mzb/p/15359705.html