操作系统:基于页面置换算法的缓存原理详解(下)

概述:

  在上一篇《操作系统:基于页面置换算法的缓存原理详解(上)》中,我们主要阐述了FIFO、LRU和Clock页面置换算法。接着上一篇说到的,本文也有三个核心算法要讲解。分别是LFU(Least Frequently Used)、LRU-KMQ(Multi Queue)算法。


本文链接:http://blog.csdn.net/lemon_tree12138/article/details/50475240 --Coding-Naga
                                                                 --转载请注明出处

1.LFU

  我们从LFU的英文全称Least Frequently Used中就可以看到,此算法是基于资源被访问的次数来实现的。由于算法很简单,我们就直接给出思路,而逻辑代码就不再展示了(因为下面的LRU-KMQ才是本文的关键)。LFU的原理图可参见图-1.


图-1 LFU置换算法原理图


算法步骤:

(1)当有新资源被访问时,就把这个资源添加到缓存队列的尾部;

(2)当访问一个已经存在的资源时,就把这个资源被访问的次数+1,再上移至合适的位置;

(3)在被访问次数相同的资源集合中,是按照访问时间来排序的;

(4)当新资源加入时,检测到此时队列已满,那么就把队列尾部的资源换出,将新资源添加到队列的尾部。

算法评价:

  个人认为此算法并不是一个很好缓存算法,因为它不能很好地反映“用户”在一个比较短的时间里访问资源的走向。

2.LRU-K

LRU算法中存在的问题:

  LRU-K算法从名字就可以知道这一定是基于LRU算法的,LRU-K算法是在LRU算法上的一次改进。现在我们分两种情况分别来看看LRU算法的效果。

  第一种,假设存在一定量连续的访问,比如说我先访问A资源10次,再访问B资源10次,再访问A资源10次,再访问C资源10次,等等等等。这样我可以大致补上一个画面就是缓存队列是在一个比较小的范围里来回替换,这样就减少换入换出的次数,提高系统的性能,这是第一种情况。

  第二种情况就是我们依次访问资源A、B、C、...、Y、Z,再重复n次。并且我们的缓存列队长度比这个循环周期要小。这样,我们的资源就需要不停地换入换出,增加IO操作,效率自然就下来了。这种情况导致的效果也被称着是缓存污染。

LRU-K的优势:

  针对上面第二种情况产生的缓存污染,我们做了一个相应地调整——加入了一个新的历史记录的队列。前面LRU算法中,我们是只要访问了某一个资源,那么就把这个资源加入缓存,这样的结果是资源浪费,毕竟缓存队列的资源有限。而在LRU-K算法中,我们不再把只访问一次的资源放入缓存,而是当资源被访问了K次之后,才把这个资源加入到缓存队列中去,并且从历史队列中删除。当资源放入缓存中之后,我们就不用再考虑它的访问次数了。在缓存队列中,我们是以LRU算法来进行更新和淘汰的(对于历史队列可以使用FIFO也可以使用LRU)。

  你是不是要问,既然这里加了一个新的历史队列还是要使用LRU算法,那么优势在哪里?而且加入了一个新的队列,也是开销呀,怎么能说还解决了LRU的问题呢?事实上一开始我也有这样的念头,后来一想,我们的历史记录队列只是一个记录数组,我们可以让它的开销很小。这里要怎么做呢?能想到么?

  我们知道我们这里要说的资源,可能是一个进程,可能是一个什么其他比较大的对象。那么,如果直接在历史队列中保存这些对象或是进程,并不是一件很划算的事情,不是吗?所以,我们的突破点就是这个历史记录队列能不能尽可能地小?是可以的。我们可以对对象进行Hash成一个整数,这样就可以不用保存原来的对象了,而后面的次数可以使用byte来保存(因为我们可以默认某一个资源在一定时间内,访问的次数不会大得离谱,当然可以使用int或是long,没问题)。如此一来,历史列队就可以做得很小了。如果你对于Hash这一点还不清楚,那么你需要做的是补补你的基础知识了。

  LRU-K算法原理图解请下面的图-1.


图-2 LRU-K置换算法原理图

代码实现:

  在代码实现的过程,我们有一个理想地假设,那就是我们将添加新资源与访问老资源完全独立开来。添加(offer)资源时默认在两个队列中都不包含,访问的资源总是存在于某一个队列当中。

添加新元素(offer):

public void offer(Object object) {
        if (histories == null) {
            throw new NullPointerException();
        }
        
        if (histories.size() == maxHistoryLength) {
            cleanHistory();
        }
        
        LRUHistory history = new LRUHistory();
        history.setHash(object.hashCode());
        history.setTimes(1);
        
        histories.add(history);
    }

访问一存在的资源(visitting):

public void visitting(Object object) {
        if (histories == null) {
            throw new NullPointerException();
        }
        
        if (caches == null) {
            throw new NullPointerException();
        }
        
        int hashCode = object.hashCode();
        if (inHistory(hashCode)) {
            boolean offerCache = modifyHistory(hashCode);
            if (!offerCache) {
                return;
            }
            
            offerToCache(object);
        } else if (inCache(object)) {
            displace(object);
        } else {
            throw new NullPointerException("对象不存在");
        }
    }

修改历史记录:

boolean offerCache = modifyHistory(hashCode);
if (!offerCache) {
	return;
}
offerToCache(object);
  上面代码的逻辑描述是当我去修改历史记录队列时,发现某一资源可以加入到缓存中去了,就把这个资源添加到缓存中去。

访问缓存队列某元素:

  因为LRU-K中的缓存队列就是一个完完全全的LRU,所以LRU-K中缓存队列的访问与LRU中访问方式一致,如下:

private void displace(Object object) {
        for (Object item : caches) {
            if (item.equals(object)) {
                caches.remove(item);
                break;
            }
        }
        
        caches.add(object);
    }

3.Multi Queue(MQ)

解决的问题:

  在上面的LRU-K算法中,只要被访问资源的访问次数达到一定数量时,就将这个资源添加到缓存中去。当然,这种做法是合理的,不过,这里我们对这种思路进行了一个扩展。我们按资源被访问的次数对资源进行分级缓存。针对上面的LRU-K算法,我们假设当资源访问次数超过3次时,就加入缓存队列。此时有两个资源A和B,A已经被访问了20次,B已经被访问了4次。这时如果再访问一次B(假设不考虑后续的访问操作),那么B的被淘汰的可能性比A被淘汰的可能要小。可是从整体上来看这种情况,我们知道理论上是A应该比B具备更大的优先级。

  上面说到了LRU-K的“缓存污染”(这里并不否认URL-K算法对URL算法的“缓存污染”改善贡献),所以我们就想了一个办法来解决。正是这里要讲解的MQ(Multi Queue)算法。下面请看MQ置换算法的原理图:


图-3 MQ置换算法原理图

算法步骤:

(1)我们需要一个历史记录队列和一个缓存队列的数组。这个数组中保存的就是真正的缓存队列,每个缓存队列和历史队列均按LRU算法进行“淘汰”。而缓存队列与缓存队列之间是按访问次数进行分级;

(2)当我们需要访问一个全新的资源时,就把这个资源加入到最低等级的Q0中,如果有需要淘汰的资源,就把这个淘汰的资源加入到历史队列中;

(3)在某一个缓存队列中资源再次被访问时,就把这个资源加入到队列的头部。如果当前资源被访问的次数达到一定的次数,就把当前资源从当前队列中删除,并加入到更高级的缓存队列的头部;

(4)为了从一定程序上防止(并没有绝对防止)高级别的缓存资源被删除,当一定时间之内,某一资源还未访问,将此资源等级下滑到下一等级;

(5)从上面缓存队列数组中的缓存资源如果被“淘汰”,是被加入到了历史队列。加入历史队列的资源,如果被再次访问可以重新计算资源被访问次数,并添加相应的缓存队列中去;而如果是历史队列中的资源被淘汰了,则是真正意义上的被淘汰。

逻辑实现:

从上面的算法步骤中,我们来编写代码,并展示关键性的代码。如下:

添加新资源:

public void offer(Object object) {
        if (object == null) {
            throw new NullPointerException();
        }
        
        CacheBean cacheBean = new CacheBean(object);
        cacheBean.setTimes(1);
        cacheBean.setLastVisitTime(System.currentTimeMillis());
        
        CacheQueue firstQueue = cacheQueueList.get(0);
        CacheBean pollObject = firstQueue.offer(cacheBean);
        if (pollObject == null) {
            return;
        }
        
        historyQueue.offer(pollObject);
    }

访问某一资源:

public void visitting(Object object) {
        if (object == null) {
            throw new NullPointerException();
        }
        
        CacheBean cacheBean = new CacheBean(object);
        
        // 先在缓存队列里找
        CacheBean tmpBean = null;
        int currentLevel = 0;
        boolean needUp = false;
        for (CacheQueue cacheQueue : cacheQueueList) {
            if (cacheQueue.contains(cacheBean)) {
                tmpBean = cacheQueue.get(cacheBean);
                if (tmpBean.getTimes() < timesDistance * (currentLevel + 1)) {
                    tmpBean.setTimes(tmpBean.getTimes() + 1);
                    cacheQueue.visiting(tmpBean);
                    return;
                } else {
                    tmpBean.setTimes(tmpBean.getTimes() + 1);
                    cacheQueue.remove(tmpBean);
                    needUp = true;
                }
                break;
            }
            
            currentLevel++;
        }
        
        // 是否需要升级
        if (needUp) {
            int times = tmpBean.getTimes();
            times = times > cacheQueueList.size() * timesDistance ? cacheQueueList.size() * timesDistance : times;
            CacheQueue queue = cacheQueueList.get(times / timesDistance);
            queue.offer(tmpBean);
            return;
        }
        
        // 如果数据在history中被重新访问,则重新计算其优先级,移到目标队列的头部
        if (historyQueue.contains(cacheBean)) {
            CacheBean reVisitBean = historyQueue.revisiting(cacheBean);
            reVisitBean.setTimes(reVisitBean.getTimes() + 1);
            System.out.println(reVisitBean);
            
            int times = reVisitBean.getTimes();
            times = times > cacheQueueList.size() * timesDistance ? cacheQueueList.size() * timesDistance : times;
            CacheQueue queue = cacheQueueList.get(times / timesDistance);
            
            CacheBean cb = queue.offer(reVisitBean);
            if (cb != null) {
                historyQueue.offer(cb);
            }
            
            return;
        }
    }

Ref:

http://flychao88.iteye.com/blog/1977653

http://www.cs.cmu.edu/~christos/courses/721-resources/p297-o_neil.pdf

http://flychao88.iteye.com/blog/1977642

GitHub源码下载:

https://github.com/William-Hai/LRU-Cache

https://github.com/William-Hai/MultiQueue-Cache


转载于:https://www.cnblogs.com/fengju/p/6336018.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
页面置换算法操作系统中的一个重要概念,用于解决虚拟内存中的页面置换问题。常见的页面置换算法包括最佳置换算法、先进先出页面置换算法、最近最久未使用置换算法、改进型Clock置换算法和页面缓冲算法等。 最佳置换算法是一种理论上的算法,它总是选择最长时间内不再被访问的页面进行置换,以保证最小化缺页率。但是,由于需要预测未来的页面访问情况,因此在实际应用中很难实现。 先进先出页面置换算法是一种简单的算法,它总是选择最先进入内存的页面进行置换。这种算法容易实现,但是可能会导致“老旧页面”长时间占用内存,从而增加缺页率。 最近最久未使用置换算法是一种基于时间局部性原理的算法,它总是选择最长时间未被访问的页面进行置换。这种算法相对于先进先出算法能够更好地利用时间局部性,但是需要维护一个访问时间戳,因此实现起来比较复杂。 改进型Clock置换算法是一种基于时钟算法的改进算法,它通过维护一个环形链表和一个访问位来实现页面置换。这种算法相对于最近最久未使用算法能够更好地平衡页面的访问频率和时间,但是需要更多的硬件支持。 页面缓冲算法是一种基于缓存的算法,它通过将热点数据缓存到内存中来减少缺页率。这种算法相对于其他算法能够更好地利用空间局部性,但是需要更多的内存空间。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值