LRU 알고리즘 문제 - LRU algolijeum munje

▷ 문제

캐시메모리는 CPU와 주기억장치(DRAM) 사이의 고속의 임시 메모리로서 CPU가 처리할 작업을 저장해 놓았다가

필요할 바로 사용해서 처리속도를 높이는 장치이다. 워낙 비싸고 용량이 작아 효율적으로 사용해야 한다.

철수의 컴퓨터는 캐시메모리 사용 규칙이 LRU 알고리즘을 따른다.

LRU 알고리즘은 Least Recently Used 의 약자로 직역하자면

가장 최근에 사용되지 않은 것 정도의 의미를 가지고 있습니다.

캐시에서 작업을 제거할 때 가장 오랫동안 사용하지 않은 것을 제거하겠다는 알고리즘입니다.

LRU 알고리즘 문제 - LRU algolijeum munje

캐시의 크기가 주어지고, 캐시가 비어있는 상태에서 N개의 작업을 CPU가 차례로 처리한다면

N개의 작업을 처리한 후 캐시메모리의 상태를 가장 최근 사용된 작업부터 차례대로 출력하는 프로그램을 작성하세요.

* 입력

첫 번째 줄에 캐시의 크기인 S(3<=S<=10)와 작업의 개수 N(5<=N<=1,000)이 입력된다.

두 번째 줄에 N개의 작업번호가 처리순으로 주어진다. 작업번호는 1 ~100 이다.

* 출력

마지막 작업 후 캐시메모리의 상태를 가장 최근 사용된 작업부터 차례로 출력합니다.

▷ 입력 예시

5 9

1 2 3 2 6 2 3 5 7

▷ 출력 예시

7 5 3 2 6

▷ 풀이

import java.util.Scanner;
import java.util.ArrayList;

public class Main {	
    public ArrayList<Integer> solution(int c, int n, int[] arr){
      ArrayList<Integer> cacheList = new ArrayList<>(); // 캐시 매모리
      for(int i=0; i<c; i++) cacheList.add(0);
      
      for(int i=0; i<n; i++){
        String status = "CM";
        int idx = 0; 

        if(cacheList.contains(arr[i])){
          status = "CH";
          for(int w=0; w<cacheList.size(); w++) if(cacheList.get(w) == arr[i]) idx = w;
        }
        
        //캐시미스 : 모든 자료는 한 칸씩 뒤로 밀리고 캐시는 맨 앞으로
        if("CM".equals(status)) for(int j=c-1; j>0; j--) cacheList.set(j, cacheList.get(j-1));
        //캐시히트 : 캐시 앞의 자료들은 한 칸씩 뒤로 밀리고 캐시는 맨 앞으로
        else for(int x=idx; x>0; x--) cacheList.set(x, cacheList.get(x-1));

        cacheList.set(0,arr[i]);
      }

      return cacheList;
    }

    public static void main(String[] args){
        Main main = new Main();
        Scanner kb = new Scanner(System.in);

        int c = kb.nextInt(); // 캐시의 크기
        int n = kb.nextInt(); // 작업의 개수
        int[] arr = new int[n];

        for(int i=0; i<n; i++){
          arr[i] = kb.nextInt();
        }

        kb.close();
        
        for(int x : main.solution(c, n, arr)){
          System.out.print(x + " ");
        }
    }
  }

▷ 핵심 포인트

  • 캐시미스와 캐시히트의 차이를 정확히 이해하고 구현해야 합니다.


문제 풀다가 LRU알고리즘에 대한 문제가 나와서 정리해보려고 한다. LRU는 페이지 교체 알고리즘 중 하나이므로 페이지 교체 알고리즘 먼저 알아보자!

페이지 교체 알고리즘

  • 페이징 기법으로 메모리를 관리하는 운영체제에서, 페이지 부재가 발생하여 새로운 페이지를 할당하기 위해 현재 할당된 페이지 중 어는 것과 교체할지 결정하는 방법.

    FIFO : 페이지가 주기억장치에 적재된 시간을 기준으로 교체될 페이지를 선정하는 기법
    - 단점 : 중요한 페이지가 오래 있었다는 이유만으로 교체되는 불합리. 가장 오래 있었던 페이지는 앞으로 계속 사용될 가능성이 있다.

    LFU : 가장 적은 회수를 참조하는 페이지를 교체
    - 단점 : 참조될 가능성이 많음에도 불구하고 횟수에 의한 방법이므로 최근에 사용된 프로그램을 교체싴킬 가능성이 있고, 해당 횟수를 증가시키므로 오버헤드 발생

    LRU : 가장 오랫동안 참조되지 않은 페이지를 교체
    - 단점 : 프로세스가 주기억장치에 접근할 때마다 참조된 페이지에 대한 시간을 기록해야 함. 큰 오버헤드가 발생


🧐LRU(Least Recently Used) 알고리즘 이란?

  • 캐시가 사용하는 리소스의 양은 제한되어 있고, 캐시는 제한된 리소스 내에서 데이터를 빠르게 저장하고 접근할 수 있어야 한다.
  • 가장 오랫동안 참조되지 않은 페이지를 교체하는 방법

장점
- 빠른 액세스 : 가장 최근에 사용한 아이템부터 가장 적에 사용한 아이템까지 정렬된다.
따라서 두 아이템에 접근할 경우, O(n)의 시간 복잡도를 가진다.
- 빠른 update : 하나의 아이템에 액세스 할때마다 업데이트 되며, O(n)의 시간 복잡도를 가진다.

⚡️단점
- 많은 공간 차지
=> n개의 아이템을 저장하는 LRU는 N의 크기를 가지는 1개의 Linked-list(queue)와 이를 추적하기 위한 n의 크기를 가지는 1개의 hash-map이 필요하다.
이는 O(n)의 복잡도를 가지지만, 2개의 데이터 구조를 사용해야 한다는 단점이 있다.


그림으로 살펴보기

LRU 알고리즘 문제 - LRU algolijeum munje

Input : 123145 인 상황에서
4초를 보면 원래 있었던 1이 한번 더 입력되므로 1을 참조한다. 참조 후 오랫동안 참조하지 않은 순으로 바꾸면 2->3->1이 된다.
6초에는 cache size가 가득차 5가 들어갈 수 없으므로, 가장 오랫동안 참조되지 않은 2를 제거한 후 저장한다.
Output : 5413

코드

#include <list>
#include <unordered_map>
#include <iostream>
using namespace std;

class LRU {
	// 데이터를 저장할 list
	list<int> LRUList;

	// 참조를 저장할 map
	unordered_map<int, list<int>::iterator> LRUMap;

	// 최대 용량
	int LRUMaxSize; 

public:
	LRU(int);
	void refer(int);
	void display();
};

// 클래스가 생성될 때 크기를 선언함.
LRU::LRU(int n)
{
	LRUMaxSize = n;
}

// 요소 x를 참조하는 함수 refer
void LRU::refer(int x)
{
	// 캐시 내에 없을 경우
	if (LRUMap.find(x) == LRUMap.end()) {
		// 캐시가 꽉 찼을 경우
		if (LRUList.size() == LRUMaxSize) {
			int last = LRUList.back();

			// 리스트에서 가장 오래 사용되지 않은 요소 pop
			LRUList.pop_back();

			// 참조도 함께 삭제
			LRUMap.erase(last);
		}
	}

	// 캐시 내에 있을 경우 해당 요소 삭제
	else
		LRUList.erase(LRUMap[x]);

	// 새로운 요소 x를 맨 앞에 push, 참조도 updqte
	LRUList.push_front(x);
	LRUMap[x] = LRUList.begin();
}

// 요소 x를 참조하는 함수 refer
void LRU::display()
{
	for (auto it = LRUList.begin(); it != LRUList.end(); it++) {
		cout << (*it) << " ";
	}

	cout << endl;
}

int main()
{
	LRU ca(4);

	ca.refer(1);
	ca.refer(2);
	ca.refer(3);
	ca.refer(1);
	ca.refer(4);
	ca.refer(5);
	ca.display();

	return 0;
}

Reference
https://rangsub.tistory.com/124?category=891713
https://j2wooooo.tistory.com/121