도서/IT

C/C++ 프로그래머가 몰랐던 프로그램의 동작 원리

MAKGA 2021. 10. 30. 14:38
320x100


1장 I/O

1.1 I/O 처리는 누가 담당하는가
1.2 디스크
1.3 표준 입출력
1.4 네트워크
1.5 동기적 I/O vs 비동기적 I/O
1.6 정리


2장 Cache와 Prefetch

2.1 반복문의 비밀

for (int i=0; i<SIZE; ++i)
    a[i] = i;

for (int i=SIZE-1; i>0; --i)
    a[i] = i;

는 다를 게 없다. 메모리에 값을 저장하기 때문이다. 이를 캐싱이라고 한다.


2.2 반복문과 Cache

CPU는 먼저 캐시를 살펴보고, 캐시에 필요한 데이터가 없으면 메모리에서 그 값을 가져온다.

캐시를 순차적으로 넣고, 빈 공간이 없을 경우 가장 오래된 데이터 부터 갱신하는 캐시 관리하는 기법을 LRU(Least Recently Used)라고 한다.

 

캐시 적중률 = cache hit / (cache hit + cache miss)

 

캐시 적중률 높이도록 코딩하기

for (int i=0; i<20; ++i)
{
    b[i] = a[i];
    c[i] = a[i];
}

for (int i=0; i<20; ++i)
{
    d[i] = a[i] + i;
    e[i] = a[i] * 2;
}

=>

for (int i=0; i<20; ++i)
{
    b[i] = a[i];
    c[i] = a[i];
    d[i] = a[i] + i;
    e[i] = a[i] * 2;
}

 

2.3 Cache의 마술사, Prefetch

지금 사용하는 데이터의 위치와 성격에 따라 사용할 데이터를 예측 가능한 경우가 있다. 대표적으로 음악이나 영상 같은 경우엔 다음 데이터를 찾기에 용이하다. 이렇게 예측되는 데이터를 미리 가져와서 사용하는 기법을 Prefetch라고 한다.

CPU에서는 최초 접근하는 데이터와 주변 메모리를 한꺼번에 가져오는 방식으로 동작한다. 보통 선형의 데이터 이므로, 메모리의 역순으로 참조하는 코딩은 캐시미스를 발생시킬 수 있다.


2.4 Cache와 Prefetch

Prefetch를 고려할 때에는 메모리의 계층 구조와 각 계층 사이에서 데이터가 오가는 양과 속도의 차이를 염두에 두어야 한다.

RAM과 같은 장치는 버스의 대역폭이 제한되어 있어서 대역폭을 초과하는 데이터는 한 번에 가져올 수 없다. 그러므로 대역폭보다 적은 크기의 데이터는 한 번에 가져오는 것이 훨씬 유리하다.


2.5 CPU에서 Cache와 Prefetch의 조합

CPU 제조사는 캐시에 데이터를 적재할 때 데이터를 가져오기 위해 캐싱과 Prefetching을 동시에 사용하고, 이 과정을 특정 데이터 블록 단위로 수행하는 경우가 많다. (=캐시 라인)

각 CPU는 별도의 캐시를 사용하는데, 한 CPU에서 해당 메모리를 변경한 경우, 캐시 무효화를 선언한다.

그러면 다른 CPU는 RAM에서 새로 읽어와야 하는데 캐시미스가 다량으로 발생하게 된다. 이런 현상을 거짓 공유라고 한다.


2.6 좀 더 느린 장치에서 Prefetch

디스크는 데이터를 읽어오는 속도보다 디스크 암이 움직여서 데이터를 찾는 시간이 더 오래 걸린다. 그러므로 디스크와 같은 장치에서는 Prefetch의 효과가 더 커진다.


2.7 데이터 쓰기에서 Cache

캐시는 변경사항이 생기면 바로 메모리에 저장하지 않고, 캐시가 꽉 차서 캐시에서 지우게 되면 이를 메모리에 저장하는 방식으로 동작한다.

이는 운영체제에서 사용되는 페이지(가상 메모리)에서도 그대로 적용된다. 디스크에 내용을 쓰는 것은 비용이 비싸므로 변경 사항이 생기면 메모리에 이 내용이 변경 됐고, 디스크에 기록되어야 한다는 표식만 해 둔다. (다만 이런 방식은 데이터 베이스에서 사용하기엔 취약하다)

메모리와 캐시에서도 캐시에만 변경 내용이 저장되면 메모리를 공유하는 다른 프로그램은 변경된 내용을 볼 수 없다. 따라서 데이터 일관성이 떨어지므로 CPU의 캐시와 메모리는 특별히 Write-through 또는 Write-back이라는 기술을 사용해 캐시와 메모리의 내용을 일치하도록 관리한다.


2.8 명령어 Cache

최근엔 수정된 하버드 구조가 채택되기 시작했다. CPU가 단일 버스를 이용해 메모리에 접근하지만, 명령어용 캐시와 데이터용 캐시에는 동시에 접근할 수 있다.

분기문 / 반복문등에서 분기 예측을 진행하는데 분기문의 결과가 어떤 것일지 예측한 뒤 이에 맞는 코드를 가져와 미리 실행해 둔다.


2.9 정리


3장 Stack과 Heap

3.1 Stack

함수가 호출되면 함수가 사용할 지역 변수를 담을 스택을 할당받게 된다. (=스택 포인터 값 감소)

함수 프롤로그는 함수 스택을 할당받고, 이전 스택을 복구하기 위한 스택 포인터 값을 백업받으며 필요한 경우 레지스터 값을 백업한다.

(EBP: 스택의 가장 아랫 부분이며 주소가 감소하며 커지므로 메모리 상으론 가장 큰 값이다.

ESP: 스택의 가장 윗 부분이며 push나 pop 명령에 의해 계속 변한다.)

foo: // 함수 호출시
    PUSH %EBP // EBP 저장
    MOV  %ESP, %EBP // ESP = EBP 대입
    SUB  스택의 크기, %ESP // ESP에 필요한 만큼의 값을 빼서 스택을 확장


3.2 Heap

동적으로 메모리가 생성되면 brk()라는 시스템 콜이 호출되는데, 이것은 매우 큰 단위의 메모리 덩어리를 프로세스에 할당하는 일이다. alloc 라이브러리는 자신이 관리하는 메모리의 전체 크기를 알고 있고, 요청된 크기만큼 메모리를 할당해 줄 수 없는 경우 brk나 VirtualAlloc 같은 시스템 콜을 호출하여 여유 메모리를 확보하고 그 안에서 사용자에게 요청된 크기만큼의 힙을 할당해 준다.

 

힙 공간은 여러 스레드간의 공유가 자유롭다보니 잠금 뿐 아니라 알 수 없는 성능 저하를 보이는 경우가 있다.

 

- 거짓 공유

여러 스레드에서 주소가 가까운 여러 메모리를 두고 같이 사용하는 경우, 캐시로 인해 성능 저하가 발생할 수 있다.

어느 CPU에서 메모리 값을 수정하는 경우, 다른 CPU의 캐시 라인은 무효화가 되면서 동일 라인의 데이터까지 무효화가 되므로 캐시가 재활용되기 어렵다.

 

- 잠금


3.3 메모리 공간의 연속성
3.4 정리


4장 프로그램 분석

4장 프로그램 분석
4.1 프로그램 디버깅이란
4.2 프로그램의 문제점 알아내기
4.3 디버거를 통한 프로그램 디버깅
4.4 잠재적인 위험성 분석하기
4.5 메모리 누수 점검
4.6 프로그램 성능 분석
4.7 정리


320x100

'도서 > IT' 카테고리의 다른 글

게임 프로그래밍 패턴  (0) 2022.09.17
C++ 최적화  (0) 2022.09.16
실용주의 프로그래머  (0) 2022.02.14
백세코딩  (0) 2022.01.30
[도메인 주도 설계란 무엇인가]  (0) 2021.11.27