Part2) CH5. 스택과 힙

songtofu·2022년 11월 27일
0

전문가를 위한 C

목록 보기
5/10

앞서

  • 개발자는 대부분 스택과 힙 세그먼트를 다루기 바쁘다.
  • 데이터 및 BSS 세그먼트는 사용 빈도가 낮고, 개발자가 통제할 권한도 작기 때문. 또한, 컴파일러가 생성하는 데다, 프로세스의 수명동안 프로세스의 전체 메모리에서 차지하는 비율도 낮기 때문. (중요하지 않다는 의미 X)

5장 학습 내용

  • 스택 세그먼트 검사 방법, 검사에 필요한 도구
  • 스택 세그먼트 메모리 자동 관리 수행 방법
  • 스택 세그먼트의 다양한 특징
  • 스택 세그먼트 사용법의 가이드라인 및 모범 사례
  • 힙 세그먼트 검사 방법
  • 힙 메모리 블록 할당 및 해제하는 방법
  • 힙 세그먼트 사용법의 가이드라인 및 모범 사례
  • 메모리가 제한된 환경과 성능이 더 나은 환경에서의 메모리 튜닝

5.1) 스택

  • 스택은 프로세스의 수명에서 주요 부분, 스택 없이는 프로세스가 계속 실행할 수 없음.
  • 스택 세그먼트 이용해야 함수를 호출 할 수 있음.
  • 스택 세그먼트에서 이뤄지는 할당은 빠르고 어떤 특별한 함수 호출도 필요하지 X
  • 메모리 해제 및 모든 메모리 관리 자동
  • 스택은 크지 않으므로 큰 객체 저장 X

5.1.1 스택 검사하기

  • 스택을 읽거나 변경 하려면 스택을 소유하는 프로세스에 속해야함.

    디버거
    디버그하려는 다른 프로세스에 붙여서 사용하는 프로그램.

  • 디버깅 과정에서 할 수 있는 작업 = 프로세스를 디버깅할 때만 전용 메모리 블록을 읽고 수정할 수 있음, 프로그램 명령어의 실행 순서를 제어하는 일
  • 스택 세그먼트는 변수와 배열이 할당되는 기본 장소 (malloc을 사용하지 않을 때)
  • 컴파일러에 -g옵션을 전달하면 최종 실행 가능한 목적 파일에 디버깅 정보가 삽입된다.

gdb에 관한 것

  • gdb는 디버깅 명령어를 전달하는 커맨드 라인 인터페이스를 갖는다.
  • 디버거에 입력값으로 지정된 실행 가능한 목적 파일을 실행하려면 명령어 r(또는 run)을 입력
  • 중단점(gdb가 프로그램의 실행을 멈추고 나중의 명령어를 기다리도록하는 표시)은 b(또는 break) 명령어로 지정
  • n(또는 next): 코드의 다음 행을 실행하는 명령어
  • x/4b @@@: @@@이 가리키는 지역에서 4바이트를 나타냄 @@@는 배열의 첫번째 원소를 가리키는 포인터임.
  • x/8b @@@: @@@이 가리키는 지역에서 8바이트를 나타냄

  • 배열에는 실제 문자열이 아닌! 아스키값이 저장된다.
  • 스택 세그먼트는 큰 주소부터 채워지고 점차 주솟값은 작아진다. (다른 메모리 지역은 작은 주소부터 시작해 주솟값이 더 커지는 방향으로 채워짐)

5.1.2. 스택 메모리 사용 시 주의점

  • 스코프는 스택 변수의 수명을 결정
  • 스택 변수를 메모리에서 자동 할당 해제할 수 있지만, 오직 스택 변수에만 해당. 스택 세그먼트의 고유 특성에서 비롯된다.
  • 스택 변수를 선언할 때. 이 변수는 스택 세그먼트의 가장 윗부분에 할당된다.
  • 할당은 자동. 변수 수명의 시작으로 기록
  • 이후 더 많은 다른 변수와 스택 프레임이 스택의 맨 위에 놓인다.
  • 변수가 스택에 존재, 다른 변수가 그 위에 놓이는한 그 변수는 계속 살아남는다.
  • 그러나, 결국 이 변수는 스택에서 팝아웃된다. 미래의 어느 지점에서 프로그램은 종료될 것이기 때문.
  • 변수가 스택에서 팝아웃될 미래의 어느 지점 존재. 해제 또는 팝아웃은 자동으로 이뤄지고, 이는 변수의 수명의 끝으로 표시

전역 변수는 데이터나 BSS같은 다른 세그먼트에 할당되며 이들 세그먼트는 스택 세그먼트처럼 작동하지 않는다.

  • 포인터는 계속 스코프 내에 존재하는 스택 변수만을 가리켜야 한다.
  • 현재 스코프에 존재하는 변수에 대한 포인터는 다른 함수에 인자로 전달될 수 있다. 단, 호출된 함수에 있는 코드가 포인터를 사용하려고 할 때 현재 스코프가 여전히 그 장소에 있다고 확인한 경우에만 가능. 이 조건은 동시성 로직이 있는 상황에서는 깨진다.

5.2) 힙

  • 스택 메모리의 비슷한 영역보다 힙 메모리 영역에 할당하는 것이 더 느리다.

  • 힙의 특성
    1. 힙은 자동으로 할당되는 메모리 블록을 갖지 않는다!
    2.힙은 메모리 크기가 크다
    3. 힙 메모리 내에서 메모리의 할당과 해제는 개발자가 관리.
    4. 힙에 할당된 변수는 스코프를 전혀 갖지 않는다.
    5. 변수를 언제 해제해야 할지도 모르며, 효율적으로 메모리를 관리하려면 메모리 블록의 스코프와 소유자를 위한 새로운 정의를 생각해야함.
    6. 힙 메모리 블록의 주소를 지정하려면 포인터만 사용할 수 있다.
    7. 힙 세그먼트는 소유자 프로세스 전용이므로 검사하려면 디버거를 사용해야한다.

    5.2.1. 힙 메모리의 할당과 해제

  • 헤더 파일 <stdlib.h>에 정의

  • 힙 메모리 블록 얻기 : malloc(메모리 할당), calloc(메모리 할당 및 초기화), realloc(이미 할당된 메모리 블록의 크기를 조정해 메모리를 재 할당)

  • 힙 메모리 해제: free

  • realloc 함수: 이 전의 블록에 있는 데이터를 변경하지 않으며 이미 할당된 블록을 새로운 블록으로 확장. 단편화 때문에 현재 할당된 블록을 확장할 수 없을 때 다른 충분히 큰 블록을 찾은 뒤 이전 블록에서 새 블록으로 데이터를 복제한다.

  • 해제에 실패하면 메모리 누수가 발생


    valgrind(메모리 프로파일러 중 하나)로 메모리 누수 감지하기

  • -g옵션 으로 예제를 빌드

  • valgrind로 실행해야함.

  • 주어진 실행 가능한 목적 파일을 실행하는 동안 valgrind는 모든 메모리 할당 및 해제를 기록, 마지막으로 실행이 종료되거나 충돌이 발생했을 때, valgrind는 할당 및 해제에 대한 요약과 해제되지 않은 메모리양을 출력

  • --leak-check=full옵션을 valgrind를 실행할 때 전달하면, 힙 메모리의 누수에 관련된 코드가 나타난다.


    5.2.2. 힙 메모리 원칙

  • 힙 메모리 블록은 아무 스코프도 갖지 않는다. -> 수명이 불분명, 다시 정의되어야함.

  • 힙 수명의 복잡성을 극복하기 위해 고안된 전략 중 가장 좋은(그러나 완벽한 해법은 아닌) 것은, 메모리 블록의 소유자를 정의하는 방법.

    
    #include <stdio.h>
    #include <stdlib.h>
    
    typedef struct {
    	int front;
    	int rear;
    	double* arr;
    }	queue_t;
    
    void init(queue_t* q)
    {
    	q->front = q->rear = 0;
       // 여기에서 할당된 힙 메모리 블록은 큐 객체가 소유
       q->arr = (double *)malloc(Q_MAX_SIZE * sizeof(double));
    }
    
    void destroy(queue_t* q)
    {
    	free(q->arr);
    }
    .
    .
    .
    .
    int main(int ac, char **av)
    {
    	// 여기에서 할당된 힙 메모리 블록은 main 함수가 소유
    	queue_t* q = (queue_t *)malloc(sizeof(queue_t));
       .
       .
       .
    	return (0);
    }
    
  • 위 코드는 각각 특정 객체를 소유하는 서로 다른 2개의 소유권을 포함.
    첫 번째 소유권, 포인터 arr가 주소를 지정하는 힙 메모리 블록에 관한 것, 큐 객체가 소유하는 queue_t구조체에 있음. 큐 객체가 존재하는 한, 이 메모리 블록은 할당된 채로 남아있어야 한다.
    두 번째 소유권, main 함수가 가지는 힙 메모리 블록을 큐 객체인 q의 자리 표시자로 간주, 이 큐 객체는 main 함수 자신이 소유하는 것.

  • 큐 객체와 main 함수가 소유하는 힙 메모리 블록을 구분하는 일은 중요. 이 블록 중 하나를 해제하더라도 다른 블록은 해제되지 않기 때문

  • 어떤 개체(객체나 함수)가 힙 메모리 블록을 소유한다면 주석으로 표기한다는 점

  • 같은 힙 메모리 블록을 여러 번 해제하면 이중 해제(더블 프리)가 발생

  • 소유권 전략 이외에도 가비지 컬렉터(프로그램에 내장된 자동 메커니즘, 그 어떤 포인터도 주소를 가리키지 않는 메모리 블록을 수집) 사용가능. C에서는 사용 X(객체가 파괴되었다는 정보를 전달받을 수 없음) C++에서는 소멸자를 이용해 이 기술을 효과적으로 사용

5.3) 제한된 환경에서의 메모리 관리

  • 이용자의 메모리 사용에 대한 하드 리밋, 저용량의 메모리를 제공하는 하드웨어, 더 큰 메모리를 지원하지 않는 운영체제
  • 메모리 사용은 프로젝트에서 중요한 비기능 요구 사항

5.3.1. 메모리 제한된 환경

  • 메모리 복잡도와 시간 복잡도는 일반적으로 Big-O 함수로 나타냄.(트레이드 오프 관계)

패킹된 구조체

  • 메모리 정렬을 폐기하고 필드를 저장하는 더 작은 메모리 레이아웃을 갖는다.
  • 절충안이다. 메모리 정렬을 폐기하기 때문에 메모리를 덜 사용. 결구 꾸조체 변수를 로드하는 동안 메모리를 읽는 시간이 더 든다.

압축

  • 메모리 내부에 저장할 텍스트 데이터가 많은 프로그램에서 효과적인 기법
  • 텍스트 데이터는 이진 파일 데이터에 비해 압축률이 더 높다.
  • 하지만, 압축 알고리듬은 CPU 바운드이자 계산 집약적이므로, 프로그램 성능 저하된다. 자주 필요하지 않은 텍스트 데이터를 갖는 프로그램에게 이상적

외부 데이터 저장소

  • 네트워크 서비스, 클라우드 인프라, 단순히 하드디스크 드라이브 형태
  • 이 기술은 메모리가 메인 저장소가 아닌 캐시 메모리의 역할을 한다고 가정.
  • 외부 데이터 저장소는 메인 메모리에 비하면 언제나 너무 느리다.

5.3.2. 성능이 더 나은 환경

  • ex) 캐시를 사용

캐싱

  • 캐싱이란? 두 데이터 저장소가 서로 읽기/쓰기 속도가 다를 때 컴퓨터 시스템의 많은 부분에서 사용되는 모든 비슷한 기법을 가리키는 일반적인 용어

  • 느린 저장소에서 다른 항목을 로드해야 하는 어떤 한 항목을 처리한다고 가정,
    지금 캐시 내부에 있을, 최근에 가져온 여러 버킷 중에서 필요한 항목을 검색
    -> 캐시에서 항목을 찾았다면 = 적중(hit)
    -> 캐시 저장소에 항목이 없다면 느린 저장소로 가야하고, 캐시 메모리 내에서 다른 항목들에 대한 버킷을 읽어야함 = 실패(miss)

  • 캐시가 더 적중할 때 마다 성능은 더 좋아진다.

캐시 친화적 코드

  • CPU가 명령어르 ㄹ실행할 때, 필요한 데이터를 모두 가져온다. 데이터는 메인 메모리의 특정 주소에 저장. 이 주소는 명령어에 의해 결정
  • 연산하기 전 데이터는 CPU레지스터로 전송. 하지만, CPU는 보통 가져와야 한다고 예상하는 것보다 더 많은 블록을 가져와서 캐시에 넣는다.
  • 어떤 값에서 이전 주소에 대한 근접성이 필요하다면, 이 값은 캐시 내에 존재해야만 하고, 그러면 CPU는 메인 메모리 대신 캐시를 사용할 수 있다.(빠름)
  • CPU는 왜 이웃 주소(근접성)를 가져와야함? 지역성의 원리. 컴퓨터 시스템에서는 보통 같은 이웃에 위치한 데이터가 더 자주 접근된다.
  • 알고리듬이 이러한 지역성의 원리를 잘 활용한다면, 캐시 친화적 알고리듬이라 말한다.
    ex) 행-우선순위에서 코드의 합계 연산이 더 잘 수행되는 것
profile
읽으면 머리에 안들어와서 직접 쓰는 중. 잘못된 부분 지적 대환영

0개의 댓글