常見頁面置換算法圖解


前言

緩存文件置換的原因是電腦存儲器空間固定,不可能將服務器上所有數據都加載在存儲空間中,當需要調用不用的數據時,那么勢必需要將需要的數據進來存儲空間替換原有數據

常見的緩存文件置換方法有:

先進先出算法(FIFO):最先進入的內容作為替換對象

最久未使用算法(LRU):最久沒有訪問的內容作為替換對象

最近最少使用算法(LFU):最近最少使用的內容作為替換對象

非最近使用算法(NMRU):在最近沒有使用的內容中隨機選擇一個作為替換對象

內存的平均引用時間為:

其中

  • T= 內存平均引用時間
  • m= 未命中率 = 1 - (命中率)
  • Tm= 未命中時訪問主內存需要的時間 (或者在多層緩存中對下級緩存的訪問時間)
  • Th= 延遲,即命中時引用緩存的時間
  • E= 各種次級因素, 如多處理器系統中的隊列效應

衡量緩存的指標主要有兩個:延遲和命中率。同時也存在其他一些次級因素影響緩存的性能。

緩存的命中率是指需要的對象在緩存中被找到的頻率。 高效的置換策略會保留較多的實用信息來提升命中率(在緩存大小一定的情況下)。

緩存的延遲是指命中后,從發出請求到緩存返回指定對象所需的時間。 快速的置換策略通常會保留較少的置換信息,甚至不保留信息,來減少維護該信息所需要的時間。

每種置換策略都是在命中率和置換之間妥協。

先進先出算法(FIFO)

如上圖,在一個隊列中,如果隊列未滿,添加資源時添加在末尾,如果隊列資源已經滿了,那么再添加資源時需要先將隊列頭部的資源移除,騰出空間后再將待添加的資源加至隊列尾。

代碼實現:

public class FIFO implements Cacheable {
 
    private int maxLength = 0;
    private Queue<Object> mQueue = null;
    
    public FIFO(int _maxLength) {
        ... ...
    }    
    @Override
    public void offer(Object object) {
        if (mQueue == null) {
            throw new NullPointerException("策略隊列對象為空");
        }      
        // check is need swap or not
        if (mQueue.size() == maxLength) {
            clean();
        }
        mQueue.offer(object);
    }
    @Override
    public void visitting(Object object) {
        System.out.println("Visited " + object);
    }
    private void clean() {
        mQueue.poll();
    }
}

最久未使用算法(LRU)

最久未使用算法圖示:

對比FIFO原理圖和LRU原理圖,可以很明顯地看到只是在被使用的資源部分有一些小的改動。在一般地使用過程中,和FIFO一樣,如果隊列還未滿時,我們可以隨意加入,當隊列緩存的數據滿了的時候,我們一樣地從隊列的尾部加入,從隊列的頭部移除了。當我們要訪問一個資源的時候,就把這個資源移動到隊列的尾部,讓這個資源看上去就像最新添加的一樣。這個就是我們LRU算法的核心了。

代碼:

public class LRU implements Cacheable {
    private int maxLength = 0;
    private Queue<Object> mQueue = null;
    public LRU(int _maxLength) {
        ... ...
    }
    @Override
    public void offer(Object object) {
        if (mQueue == null) {
            throw new NullPointerException("策略隊列對象為空");
        }  
        // check is need swap or not
        if (mQueue.size() == maxLength) {
            clean();
        }   
        mQueue.offer(object);
    }  
    @Override
    public void visitting(Object object) {
        if (mQueue == null || mQueue.isEmpty()) {
            throw new NullPointerException("訪問對象不存在");
        }        
        System.out.println("Visited " + object);
        displace(object);
    }
    // remove the longer no visit
    private void clean() {
        mQueue.poll();
    }
    // cache core code
    private void displace(Object object) {
        for (Object tmp : mQueue) {
            if (object.equals(tmp)) {
                mQueue.remove(tmp);
                break;
            }
        }
        mQueue.offer(object);
    }
}

LRU算法改進版 LRU-K

LRU-K圖示

算法簡介:

主要思想是統計一下對象加載的頻率,達到設定的K值之后才進入緩存,進入緩存之后實行最久未使用(LRU)淘汰策略。

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,沒問題)。如此一來,歷史列隊就可以做得很小了。

代碼實現

添加新元素:

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);
    }

最近最少使用算法(LFU)

LFU的英文全稱Least Frequently Used中就可以看到,此算法是基於資源被訪問的次數來實現的。由於算法很簡單,我們就直接給出思路,而邏輯代碼就不再展示了(因為下面的LRU-KMQ才是本文的關鍵)。

算法步驟:

(1)當有新資源被訪問時,就把這個資源添加到緩存隊列的尾部;

(2)當訪問一個已經存在的資源時,就把這個資源被訪問的次數+1,再上移至合適的位置;

(3)在被訪問次數相同的資源集合中,是按照訪問時間來排序的;

(4)當新資源加入時,檢測到此時隊列已滿,那么就把隊列尾部的資源換出,將新資源添加到隊列的尾部。

算法評價:

此算法並不是一個很好緩存算法,因為它不能很好地反映“用戶”在一個比較短的時間里訪問資源的走向。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM