➰ Library/Algorithm

LRU 알고리즘 (Least Recentely Used) 개념 및 구현방법

 사과개발자 2021. 12. 18. 19:06

안녕하세요! daily_D 입니다! 👩🏻‍💻
오늘은 페이지 교체 알고리즘 중에서 LRU에 대해서 공부해볼까요?!


LRU 란?

LRU(Least Recently Used)는 가장 오랫동안 참조되지 않은 페이지를 교체하는 방식입니다.
LRU 는 사용된지 가장 오래된 페이지는 앞으로도 사용될 확률이 낮다는 가설에 의해 만들어진 알고리즘입니다.

 

LRU 의 원리

LRU 를 구현하기 위해서는 캐시가 가득 찼을때, 가장 오랫동안 참조되지 않은 페이지를 찾아서 없애는 과정이 필요합니다.

페이지를 새로 참조할 때마다 연결리스트의 맨 앞에 페이지번호를 추가합니다.
그러면 맨 뒤에 있는 페이지번호가 가장 오랫동안 참조되지 않은 페이지번호가 되겠죠?

따라서 LRU의 원리는

캐시의 크기가 3인데 이미 3개의 페이지가 캐시에 들어있다면 맨 뒤에 있는 페이지번호 node 를 지우고 새로운 페이지번호 node 를 앞에 연결해주는 방식입니다.

LRU를 구현할때는 Doubly Linked List 를 사용하고 head 에 가까운 node 일수록 가장 최근에 참조된 페이지, tail 에 가까운 node 일수록 가장 오랫동안 참조되지 않는 페이지입니다. LRU 의 개념에 따라 cache size 를 넘어서게 된다면 tail 에 가까운 페이지가 먼저 삭제되도록 합니다. 

[1, 2, 1, 3, 4, 1, 5, 4] 예시를 그림으로 정리해보면 아래와 같습니다!

💡 Cache Hit & Cache Miss
Cache Hit : CPU 가 참조하고자 하는 메모리가 캐시에 존재하고 있을 경우
Cache Miss : CPU 가 참조하고자 하는 메모리가 캐시에 존재하지 않을 경우
💡 Hit Ratio & Miss Ratio
Hit Ratio : Cache Hit / (Cache Hit + Cache Miss)
Miss Ratio : Cache Miss / (Cache Hit + Cache Miss)
※ 위의 예시 : Hit Ratio = 3/8, Miss Ratio = 5/8 

 

LRU 의 구현하기

1. 아까 말했듯이 LRU는 Doubly Linked List 로 구현되기때문에 두 개의 class 를 만들어야합니다!

첫번째는 Node class 로, 각 node 는 '자신의 정보를 저장할 data' 와 '이전 node 를 저장할 prev', '다음 node 를 저장할 next' 가 필요합니다. 

class Node:
    def __init__(self, data, prev=None, next=None):
        self.data = data
        self.prev = prev
        self.next = next

 

두번째는 DoublyLinkedList class 로, head nodetail node 가 있어야하며 서로 연결되어있어야합니다.

class DoublyLinkedList:
    def __init__(self, cacheSize):
        self.cacheSize = cacheSize
        self.head = Node("")
        self.tail = Node("")
        self.head.next = self.tail
        self.tail.prev = self.head

※ 여기서 매개변수로 들어온 cacheSize는 LRU를 구현하기위해 필요한 변수입니다!

 

2. 이제 두개의 클래스를 모두 만들었다면! LRU 를 구현하는데 필요한 함수들을 만들차례입니다!
우리는 크게 3 가지의 함수가 필요합니다.

1) LRU
2) cacheHit
3) cacheMiss

 

첫번째로 LRU 함수는 새롭게 넣을 data 를 매개변수로 받아
캐시에 존재하는 data 면 cacheHit, 캐시에 존재하지 않는 data 면 cacheMiss 를 실행시킵니다.

def LRU(self, data):
    node = self.head.next
    while(node.data):
        if node.data == data:
            self.cacheHit(node, data)
            return
        node = node.next
    self.cacheMiss(data)

 

두번째로 cacheHit 함수는 캐시에 data 가 존재하는 경우이므로,
원래의 위치에서 node 를 삭제하고 head 바로 뒤에 원소를 추가합니다.

# 원소 맨앞으로 이동
def cacheHit(self, node, data):
    self.removeNode(node)
    self.addFront(data)
 
# node 삭제
def removeNode(self, node):
    node.prev.next, node.next.prev = node.next, node.prev
    
# head 의 바로 뒤에 원소 넣기
def addFront(self, data):
    newNode = Node(data)
    self.head.next.prev = newNode
    newNode.next = self.head.next
    self.head.next = newNode
    newNode.prev = self.head

 

마지막으로 cacheMiss 함수는 캐시에 data 가 존재하지 않는 경우이므로,
무조건 list 의 맨앞에 data 를 추가하고 만약 doublyLinkedList 의 초기매개변수로 들어왔던 cacheSize 보다 원소의 개수가 많아지면 tail 에 가까운 원소를 제거합니다.

# 원소의 맨앞에 추가 (cacheSize 보다 커지면 tail에 가까운 노드 삭제)
def cacheMiss(self, data):
    self.addFront(data)
    if self.totalLen() > self.cacheSize:
        self.removeTail()
        
# linked list 의 총 길이 반환
def totalLen(self):
    answer = 0
    node = self.head.next
    while(node.data):
        answer += 1
        node = node.next
    return answer
        
# tail 에 가장 가까운 원소 삭제
def removeTail(self):
    self.tail.prev.prev.next = self.tail
    self.tail.prev = self.tail.prev.prev

 


최종 코드는 아래와 같습니다.

class Node:
    def __init__(self, data, prev=None, next=None):
        self.data = data
        self.prev = prev
        self.next = next
    
class DoublyLinkedList:
    def __init__(self, cacheSize):
        self.cacheSize = cacheSize
        self.head = Node("")
        self.tail = Node("")
        self.head.next = self.tail
        self.tail.prev = self.head
    
    def LRU(self, data):
        print("[Put " + data + "]", end=" ")
        node = self.head.next
        while(node.data):
            if node.data == data:
                self.cacheHit(node, data)
                return
            node = node.next
        self.cacheMiss(data)
    
    # 원소 맨앞으로 이동
    def cacheHit(self, node, data):
        self.removeNode(node)
        self.addFront(data)
        print("[Hit ]", end=" ")
        self.printAll()
    
    # node 삭제
    def removeNode(self, node):
        node.prev.next, node.next.prev = node.next, node.prev
       
    # head 의 바로 뒤에 원소 넣기
    def addFront(self, data):
        newNode = Node(data)
        self.head.next.prev = newNode
        newNode.next = self.head.next
        self.head.next = newNode
        newNode.prev = self.head
        
    # 원소의 맨앞에 추가 (cacheSize 보다 커지면 tail에 가까운 노드 삭제)
    def cacheMiss(self, data):
        self.addFront(data)
        if self.totalLen() > self.cacheSize:
            self.removeTail()
        print("[Miss]", end=" ")
        self.printAll()
        
    # linked list 의 총 길이 반환
    def totalLen(self):
        answer = 0
        node = self.head.next
        while(node.data):
            answer += 1
            node = node.next
        return answer
        
    # tail 에 가장 가까운 원소 삭제
    def removeTail(self):
        self.tail.prev.prev.next = self.tail
        self.tail.prev = self.tail.prev.prev
      
    # For Debug  
    # head 부터 tail 까지 순환하면서 date 전부 출력
    def printAll(self):
        node = self.head.next
        while(node.data):
            print(node.data, end="")
            node = node.next
            if (node.data):
                print(" -> ", end="")
        print()

 

이제 우리가 만든 코드로 [1, 2, 1, 3, 4, 1, 5, 4] 예시를 확인해볼까요?

test = DoublyLinkedList(3)
inputArray = ["1", "2", "1", "3", "4", "1", "5", "4"]
for input in inputArray:
    test.LRU(input)

 

위의 코드를 추가한 후 print 주석들을 해제하면 아래와 같이 우리가 원하는 결과가 출력됩니다!!

반응형