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;
따라서 멀티 스레드 환경에서 내가 작업한 코드가 다르게 작동할 여지가 있으므로 스레드를 잘 컨트롤 할 수 있어야 한다.
'서버 프로그래밍 > 멀티 스레드' 카테고리의 다른 글
1-6. CAS(Compare And Swap), 스핀 락(Spin Lock) (0) | 2023.11.24 |
---|---|
1-5. 락 (Lock), 뮤텍스(mutex), RAII 패턴, lock_guard (0) | 2023.11.24 |
1-4. 공유 자원과 경쟁 조건 (0) | 2023.11.23 |
1-2. 멀티 스레드 (0) | 2023.11.23 |
1-1. 게임 서버란? (0) | 2023.11.23 |