1. 캐시

(1) 정의

CPU 내에서 RAM과 고속으로 통신하기 위한 임시 저장소.

CPU와 RAM의 속도 차이를 극복하기 위해 생겨났다.

레지스터

ㄴ L1 캐시

ㄴ L2 캐시

ㄴ L3 캐시

|

RAM

레지스터에 가까울 수록 속도가 빠르며, 용량이 적어진다.

 

 

(2) 캐시 철학

캐시의 목적과 운용 원칙에 대한 접근 방식

 

- 임시 지역성 ;Temporal Locality

데이터가 한 번 엑세스되면 다시 엑세스 될 확률이 높다.

따라서 한 번 불러온 데이터는 잠시 캐시에 유지된다.

 

- 공간 지역성 ; Spatial Locality

한 번에 엑세스되는 데이터 주변의 데이터가 더 자주 엑세스 될 확률이 높다.

따라서 캐시로 불러올 때 캐시 라인 단위로 불러온다.

 

 

(3) 캐시 동작을 확인하는 코드 실험

#include <windows.h>
using namespace std;

int buffer[10000][10000];


int main()
{
	::memset(buffer, 0, sizeof(buffer));
	{
		auto start = GetTickCount64();

		__int64 sum = 0;

		for (int i = 0; i < 10000; i++)
			for (int j = 0; j < 10000; j++)
				sum += buffer[i][j];

		auto end = GetTickCount64();

		cout << "경과된 틱 " << (end - start) << endl;
	}

	{
		auto start = GetTickCount64();

		__int64 sum = 0;

		for (int i = 0; i < 10000; i++)
			for (int j = 0; j < 10000; j++)
				sum += buffer[j][i];

		auto end = GetTickCount64();

		cout << "경과된 틱 " << (end - start) << endl;
	}
}

알고리즘 적으로는 두 결과 값이 같아야 한다.

하지만 두번째 결과가 첫번째 결과의 3배의 시간이 걸린다.

그 이유는 캐시 라인을 불러오는 특성(공간 지역성) 때문이다.

 

첫번째 실험은 각 가로 줄마다 값을 1부터 10000까지 채워넣기 때문에, 해당 가로줄을 불러오는 방법을 사용하여 계산이 일찍 끝나는 반면에(캐시 히트 ; Cache Hit), 두번째 실험은 각 가로 줄의 첫번째 칸, 두번째 칸 ... 을 채워넣기 때문에, 가로줄을 불러오는 방법이 속도 향상에 의미를 주지 못하기 때문이다(캐시 미스 ; Cache Miss).

 

 

2. CPU 파이프라인

중앙 처리 장치(CPU)가 명령어를 처리하는 과정을 여러 단계로 나누어, 각 단계를 동시에 수행하여 전체적인 처리 성능을 향상시키는 기술이다.

각 단계가 겹치도록 설계되어 있어, 한 번에 여러 명령어를 동시에 처리할 수 있다.

 

(1) 파이프라인

(1-1) Fetch

명령어를 메모리에서 가져옴.

 

(1-2) Decode

명령어가 어떤 연산을 수행하는지 결정하고 레지스터를 선택.

 

(1-3) Execute

명령어의 연산을 수행.

 

(1-4) Write-back

연산의 결과를 레지스터에 저장.

 

(2) 코드 실험(1) - 동시 접근으로 인한 경쟁 조건(Race Condition)

int x = 0;
int y = 0;
int r1 = 0;
int r2 = 0;
bool ready = false;

void Thread_1()
{
	while (ready == false) {}
	y = 1;
	r1 = x;
}

void Thread_2()
{
	while (ready == false) {}
	x = 1;
	r2 = y;
}

int main()
{
	int count = 0;
	while (true)
	{
		ready = false;
		count++;

		x = y = r1 = r2 = 0;

		thread t1(Thread_1);
		thread t2(Thread_2);

		ready = true;

		t1.join();
		t2.join();

		if (r1 == 0 && r2 == 0)
			break;
	}

	cout << count << endl;
}

두 개의 쓰레드가 공유 변수에 동시 접근하여 값을 수정하는 코드이다.

위의 코드에서는 t1과 t2가 실행되면 r1과 r2의 값이 절대 0이 될 수 없으나, 경우에 따라 두 스레드가 실행되는 타이밍이 달라지면서 예상과 다른 결과가 나타날 수 있다(r1과 r2가 0이 되는 등).

 

 

(3) 코드 실험(2) - 코드 최적화 문제

bool ready = false;

void Thread_1()
{
	ready = false;

	while (ready == false)	{}

	cout << "TEST" << endl;
}


int main()
{
	thread t1(Thread_1);

	this_thread::sleep_for(1s);

	ready = true;

	t1.join();
}

이번에는 스레드를 생성한 1초 뒤에 전역 변수를 건드리는 실험이다.

위에서는 무한 반복문이 종료되면 안되지만, 컴파일러의 판단에 의해 최적화가 되면 while (ready == false)에서 ready가 최적화가 되어 while(false)가 되어 무한 반복문이 종료하게 된다.

 

따라서 컴파일러의 코드 최적화를 방지하기 위해 사용하는 문법인 volatile을 사용할 수 있다.

volatile bool ready = false;

 

따라서 멀티 스레드 환경에서 내가 작업한 코드가 다르게 작동할 여지가 있으므로 스레드를 잘 컨트롤 할 수 있어야 한다.