■ CPU와 RAM
왔다갔다 계속 사용하기엔 시간이 다소 많이 소요되니 이 둘 사이에 저장공간인 레지스터 및 캐시를 이용한다.
[CPU 코어 구성]
(1) ALU (연산장치)
(2) 캐시 장치
- 레지스터
- L1 캐시
- L2 캐시
....
캐시 장치를 구성하는 레지스터, L1캐시, L2캐시는 계층 구조로 생각해봤을 때 아래로 갈수록 (Register->L2) 저장 공간은 많아지지만 속도는 느려진다.
위 캐시 장치들을 모두 훑어봤을 때도 데이터가 없을 경우에 주기억장치로 가서 훑어보게 된다.
■ 캐시 정책
지역성 | 설명 |
시간지역성 (Temporal Locality) |
(레스토랑 비유) 시간적으로 보면, 방금 주문한 테이블에서 추가 주문이 나올 확률이 높다. 방금 주문한걸 메모해 놓으면 편하지 않을까? |
공간지역성 (Spatial Locality) |
(레스토랑 비유) 공간적으로 보면, 방금 주문한 사람 근처에 있는 사람이 추가 주문을 할 확률이 높다. 방금 주문한 사람과 합석하고 있는 사람들의 주문 목록도 메모해 놓으면 편하지 않을까? |
위와 같이 캐싱해놓은 메모리에 또 다시 접근하는 경우가 발생한다면 CPU의 추측이 들어맞는 것이다.
이와 같이 주기억장치 까지 갈 필요없이 성공적으로 데이터를 메모리에서 찾아 반환해준 것을 '캐시 적중(Cache hit)'라고 부른다.
이와 반대로 데이터를 찾을 수 없는 경우를 '캐시 미스 (Cache Miss)'라고 한다.
* 캐시 적중률 (Cache Hit Ratio) = 적중 횟수 / 총 접근 횟수.
(0.95 ~ 0.99가 우수한 편이다)
■ 공간지역성 (Spatial Locality) 실습
위 첫번째 구간의 코드를 보면 i번째 행에 위치한 모든 요소들을 갖고와 j열에 위치한 데이터를 접근하려고 한다면 공간지역성으로 인해 i번째 행에 위치한 모든 데이터 청크를 캐싱해놓은 뒤, 굳이 주기억장치로 갈필요 없이, j요소가 접근하는 것을 볼 수 있다. 그로 인하여 빠른 시간 내에 모든 데이터를 순회할 수 있었다.
두 번째 구간에선 j와 i를 뒤바꿔줬다. j가 매번 바뀌는 와중에 j행에 위치한 모든 요소들을 가져온다고 했을 때, i+1에 위치한 요소에 바로 접근을 하지 않다보니 계속 새로운 행들을 꺼내서 데이터를 체크해줘야 한다.
위 두 구간의 코드는 크게 달라진건 없지만, 공간지역성으로 인해 성능이 확연히 달라지는 것을 확인할 수 있었다.
■ CPU 명령어 파이프라인
CPU가 명령어를 처리하는 과정은 이와 같다.
(1) 메모리의 Code 영역에 실행해야할 명령어들이 기록되어 있을 텐데 이를 먼저 갖고온다. (Fetch)
(2) 명령어가 무엇인지를 해석한다 (Decode)
(3) 명령어를 실행한다 (Execute)
(4) 명령어 실행 후 원래 있던 곳으로 코드 데이터를 반환해준다. (Write-back)
■ 주의할점
싱글 스레드에선 잘 동작하던 것들이 멀티스레딩 환경에선 문제를 일으킬 수 있는데 크게 아래 두 가지 상황을 이유로 발생할 수 있다.
(1) CPU가 명령어를 처리할 때 요청한 순서대로 명령어들을 처리하지 않고 대기 중인 명령어들이 독립적인 명령어라고 판단할 경우 그중 가장 먼저 실행하면 효율적일 것 같은 애들을 실행해준다.
(아래 포스팅에 '비순차적 명령어 처리' 참고 요망)
https://yjhdevelopdiary.tistory.com/237
예로 아래와 같은 코드를 작성했다고 가정해보자.
각 환경마다 count가 달라지겠지만 내 컴퓨터 환경에선 32526번 while문을 반복해야 비로소 빠져나올 수 있었다.
안에서 지지고 볶으면서 빠져나올 수 있었던 이유는 CPU는 대기 중인 명령어들이 독립적인 명령어라 판단할 경우 최적화를 위해 아래와 같이 순서를 바꾸는 경우가 간혹 있기 때문이다.
알게 모르게 CPU가 성능 향상을 위해 그런 작업을 하는데 멀티 스레드 환경에선 치명적인 상황이 발생할 수 있다.
(2) 컴파일러 또한 코드 내용을 바꿀 수 있다.
예로 아래와 같은 코드를 작성했다고 가정해보았을 때
어차피 ready는 false 상태이고 그렇기 때문에 무한 루프를 돌아야 하는데 컴파일러 입장에선
while 반복문의 조건 (ready==false)를 매 번 비교하는 것이 필요없는 작업이라 생각해서 while(false)로 최적화를 위해 바꿀 수 있다는 것이다.
하지만 어디선가 ready변수에 접근하고 있다면 이는 큰 문제가 생긴다.
위와 같은 상황을 미연에 방지하기 위해 변수 선언을 할 때 'volatile'키워드를 붙이는 경우가 종종 있다.
volatile 키워드는 이 변수가 언제든 바뀔 수 있기 때문에 코드 최적화를 하지 말아달라는 뜻이다.
하지만 C++, C# 등 언어에 따라 volatile 키워드의 의미가 약간씩 달라지기 떄문에 이를 사용하는 것을 권장하지 않는다.
대체로 atomic을 사용하는 편이고 위 두 가지 문제 상황에 대해 방어책은 여러 가지가 있고 다음 포스팅들에서 소개할 예정이다.
(★)
요즈음 나오는 현대 CPU인 AMD, Intel 등의 프로세서를 사용한다면 어지간해서는 우리가 생각하는 직관적인 방식대로 작동한다. 하지만, 표준에 명시되어있는 규칙이 아니기 때문에 위와 같은 상황에서 ready를 true로 바꿔줬음에도 불구하고 다른데서 캐시에 의해 인지를 못하는 상황이 발생할 수도 있다. 이와 같이 환경에 따라 동작하는 것이 크게 많이 달라질 수도 있다.
'개발 (언어) > C++' 카테고리의 다른 글
[MultiThread] Mutex, Spinlock, DeadLock (0) | 2024.04.08 |
---|---|
[MultiThread] 공유 자원, Atomic (0) | 2024.04.07 |
[MultiThread] 쓰레드 생성 (1) | 2024.04.07 |
[Modern C++] weak_ptr, unique_ptr (0) | 2024.03.31 |
[Modern C++] shared_ptr (0) | 2024.03.31 |