[한 권으로 읽는 컴퓨터 구조와 프로그래밍]7장 데이터 구조와 처리
어떻게 해야 프로그램에서 데이터를 잘 구성하고 처리할까
서론
- 데이터 구조(data structure): 데이터를 조직화하는 표준적 방법
- 참조 지역성(locality of reference): 필요한 데이터를 메모리에서 서로 근처에 유지하고, 금방 사용할 데이터는 더 가까운 곳에 저장하는 것
기본 데이터 타입
- 프로그래밍 언어는 기본 데이터 타입(primitive data type)을 제공
- 두 가지 측면
- 크기(size): 비트 수
- 해석(interpretation): 부호 유무, 문자나 포인터 아니면 불리언을 나타내는지 여부
포인터
- 컴퓨터 아키텍처에 따라 결정되는 크기의 부호가 없는 정수
- 정숫값이 아닌 메모리 주소로 해석
- 값이 0, 즉 NULL은 일반적으로 주소로 인정하지 않음
- 일반적으로 컴퓨터의 워드 크기와 같은 경우가 많음
- 한 사이클 안에 컴퓨터 내의 대상에 접근하기 위함
배열
- 일반적인 컴퓨터 개발 규정(프로그래밍 언어 문법)에서 배열 원소의 타입은 모두 같아야 한다고 규정
- 상대 주소 지정으로 보는 배열도 가능
- 기저 주소(base address), 즉 0번째 원소의 주소로부터 떨어져 있는 정도(offset)로 지정
참조 지역성(locality of reference)
- 다른 행, 같은 열 순서로 탐색하는 것보다 같은 행, 다른 열 순서로 탐색하는 것이 지역성이 더 좋다.
- 열 인덱스가 바뀌는 것보다 행 인덱스가 바뀔 때 더 많은 이동 발생
- 열 인덱스가 바뀌면 인접한 메모리 위치로 이동하지만 행 인덱스가 바뀌면 열 개수만큼 이동해야 하기 때문
비트맵
- 비트맵(bitmap): 비트의 배열
- 수행할 수 있는 기본 연산
- 비트 설정하기(set): 1로 만들기
- (원하는 인덱스의 비트) = (원하는 인덱스의 비트) OR 마스크
- 비트 지우기(clear): 0으로 만들기
- (원하는 인덱스의 비트) = (원하는 인덱스의 비트) AND (NOT 마스크)
- 비트가 1인지 검사하기
- (원하는 인덱스의 비트) AND 마스크 ≠ 0
- 비트가 0인지 검사하기
- (원하는 인덱스의 비트) AND 마스크 = 0
- 비트 설정하기(set): 1로 만들기
문자열
- 문자열(string): 여러 문자로 이뤄진 시퀀스
- 문자열은 길이를 알아야 하지만 가변(길이가 변하는) 문자열 데이터의 경우 어렵다.
문자열의 길이를 알아내는 방법
- 문자열 안에 길이를 저장
- 길이가 255자로 제한된다는 단점 존재
- 어느 순간 길이를 저장하기 위한 바이트 수가 문자열 길이를 넘어가는 경우 발생
- 문자열은 바이트라서 메모리 정렬(alignment)가 달라지는데 길이를 저장하려면 이 정보는 반드시 올바른 메모리 정렬 경계에 있어야 함
- C는 문자열 전용 데이터 타입을 제공하지 않고, 1차원 바이트 배열을 사용
- 문자 배열에 들어 있는 문자열 데이터 끝에 바이트를 하나 추가하고 여기에 NUL을 넣음
- NUL: 0, 문자열 터미네이터, 문자열 끝을 표시하는 문자
- 대부분의 기계는 주어진 값이 0인지 검사하는 명령어가 있기 때문에 NUL을 사용하는 게 효율적
- 저장이 쉽고 부가 비용이 들지 않다는 장점 존재
- 문자열 길이를 알기 위해 문자열 터미네이터를 발견할 때까지 문자 수를 세야 한다는 단점 존재
- 문자열 중간에 NUL 문자를 넣을 수 없다는 단점 존재
- 문자 배열에 들어 있는 문자열 데이터 끝에 바이트를 하나 추가하고 여기에 NUL을 넣음
복합 데이터 타입
- 구조체(structure): 원하는 대로 데이터 타입을 만드는 방법
- 멤버(member): 구조체 안의 원소
- 편의 문법(syntactic sugar): 프로그램을 문법적으로 더 알기 쉽고 이해하기 편하도록 표현하는 비필수적인 요소
- 패딩(padding): 멤버 순서와 메모리 정렬을 지키기 위해 추가하는 바이트
- 공용체(union)
- 구조체 안의 모든 멤버는 각기 다른 메모리를 차지하지만 공용체의 멤버는 메모리를 공유 가능
단일 연결 리스트
- 연결 리스트(linked list): 목록에 들어갈 원소 개수를 모르는 경우 배열보다 효율적인 구조
- 구조체를 사용해 만들 수 있다.
- 배열과의 차이
- 배열의 각 원소는 메모리에서 연속적으로 위치하지만 리스트 원소는 아무 데나 위치할 수 있음
- 리스트는 원소를 쉽게 추가할 수 있음
동적 메모리 할당
- 정적으로 할당된 데이터 영역 다음에 프로그램 런타임 라이브러리가 설정해주는 힙 영역 존재
- 메모리 관리 유닛(MMU)이 없다면 이 힙 영역이 프로그램이 쓸 수 있는 모든 데이터 메모리
- MMU가 있다면 런타임 라이브러리가 프로그램에 필요한 메모리를 운영체제에 요청
- 프로그램의 브레이크: 프로그램이 사용할 수 있는 메모리의 끝
- 배열 등의 변수가 사용하는 메모리는 정적이며 할당된 주소는 바뀌지 않음
- 리스트 노드와 같은 구조가 사용하는 메모리는 동적이므로 힙 사용
더 효율적인 메모리 할당
- 문자열이 들어있는 연결 리스트의 경우 노드에 문자열을 가리키는 포인터를 포함하는 것보다 노드와 문자열을 동시에 함께 할당하면 더 효율적이다.
가비지 컬렉션
- 가비지 컬렉션(garbage collection): 자바와 자바스크립트에서 동적 메모리 할당을 위해서 사용하는 방법
- 자바와 자바스크립트는 포인터가 없다.
- 단점
- 프로그래머가 가비지 컬렉션 제어 불가
- 불필요한 참조가 남는 경우 빈번
이중 연결 리스트
- 단일 연결 리스트에서 원소를 지우려면 그 앞 원소를 찾아야 해 매우 느리다.
- 이중 연결 리스트는 노드에 다음 원소와 이전 원소에 대한 포인터도 넣어 비용이 늘어나지만, 원소 삭제를 빠르게 할 수 있다.
- 즉 리스트 전체를 방문하지 않아도 원하는 위치에 노드를 추가하거나 삭제할 수 있다.
계층적인 데이터 구조
- 배열, 연결 리스트는 모두 선형적인 데이터 구조이다.
2진 트리
- 노드가 최대 2개의 다른 노드와 연결 가능
- 트리는 단일 연결 리스트처럼 쭉 늘어선 모습이 아닌 균형 잡힌 모습을 유지해야 효율적
- 연결 리스트에서는 원소를 검색할 때 n번 비교해야 하지만 균형 잡힌 트리에서는 log n번만 하면 된다.
- 트리의 균형을 맞추는 데 시간이 좀 걸리지만 log n이 n보다 훨씬 작기 때문에 괜찮다.
대용량 저장장치
- 블록: 디스크의 기본 단위
- 클러스터: 연속적인 블록
문제
데이터를 한 클러스터에 저장하기엔 무리가 있으므로, 고정된 크기의 블록을 여럿 확보해 데이터를 이 블록에 나눠 담아야 한다.
연결 리스트를 사용한다면?
순회에 시간이 너무 오래 걸리기 때문에 사용할 수 있는 디스크 블록, 이미 사용 중인 디스크 블록을 탐색하는데 적절하지 못하다.
파일 이름의 필요성
- 디스크의 데이터는 장기적으로 저장되기에 데이터를 읽기 위한 영구적인 존재, 즉 파일 이름이 필요
- 파일 이름을 사용하기 위해 다음이 필요하며 이를 아이노드가 처리한다.
- 파일 이름을 디스크에 저장
- 파일 이름과 파일의 데이터가 저장된 디스크 블록을 연결
아이노드(inode)란
- 유닉스(UNIX)에서 개발
- 인덱스(index) + 노드(node), 즉 아이노드는 인덱스 노드
- 디스크 블록 중 일부를 아이노드로 지정해 사용
- 파일에 대한 정보를 갖는다.
- 파일의 데이터가 들어 있는 블록에 대한 인덱스
- 파일 이름, 파일 소유자, 파일 크기, 파일에 대한 허가 내역 등
아이노드의 직접 블록과 간접 블록
- 직접 블록
- 보통 직접 블록 포인터가 12개 존재
- 실제로는 포인터가 아니라 블록의 인덱스이다.
- 직접 블록을 통해 데이터를 4096*12=49152바이트까지 보관 가능하다.
- 보통 직접 블록 포인터가 12개 존재
- 간접 블록
- 파일이 크면 사용
- 32비트 인덱스를 사용하면 한 블록에 4바이트짜리 인덱스가 1024개 존재하므로 최대 4MiB까지 지원
- 한 블록은 4KiB이다.
- 더 큰 파일에 대해서는 2중 간접 블록을 통해 4GiB, 3중 간접 블록을 통해 4PiB까지 지원 가능
아이노드와 디렉터리
- 아이노드 정보 중에는 블록에 데이터가 있는지 디렉터리 정보가 있는지에 대한 표시도 포함
- 디렉터리: 파일 이름과 파일 데이터를 가리키는 아이노드를 연결
- 여러 파일 유형 중 하나이므로 디렉터리는 다른 디렉터리 참고 가능 ⇒ 트리 구조의 계층적 파일 시스템 탄생
- 심볼릭 링크: 디렉터리에 대해 링크
- 링크: 아이노드가 블록을 참조하는 것
- 심볼릭 링크를 허용하면 파일 시스템 그래프에 루프가 생길 수 있으므로 무한 루프를 감지하기 위한 코드 필요
가용 공간(free space) 추적
- 비트맵 방식
- 각 블록을 1비트로 표현해서 가용 공간을 나타내는 방식
- 간단하면서 효율적이다.
- 1은 사용 중인 블록, 0은 사용 가능한 블록을 가리킨다면 모든 비트가 1이 아닌 워드를 탐색
- 파일 시스템 그래프와 비트맵 사이 동기화가 깨질 수 있음
- 디스크에 데이터를 기록하는 도중 전원이 나갈 수 있음
데이터베이스
- 트리는 크기가 작은 경향이 있어 디스크 섹터에 잘 들어맞지 않는다.
- 데이터베이스: 정해진 방식으로 조직화한 데이터 모음
- 데이터베이스 관리 시스템(DBMS): 데이터베이스에 정보를 저장하고 읽어올 수 있게 해주는 프로그램
- 일반적으로 맨 아래의 데이터 저장 메커니즘을 감싼 여러 계층의 인터페이스로 구성
B 트리
- 데이터베이스가 일반적으로 이용하는 데이터 구조
- 균형 트리
- 균형 2진 트리보다 공간 효율성은 떨어지지만 성능이 더 낫다.
- 가지의 수는 디스크 블록 하나를 정확히 채울 수 있는 숫자로 결정
인덱스
- 하나의 인덱스로 구성된 노드를 주 인덱스(primary index), 여럿인 경우 다중 인덱스라 한다.
- 인덱스는 데이터가 바뀔 때마다 갱신되어야 한다.
- 데이터 변경보다 검색을 더 많이 하므로 감당할만한 손해이다.
데이터 이동
프로그램은 한 지점에서 다른 지점으로 데이터를 이동시킬 때 많은 시간이 들기 때문에 이를 효율적으로 처리해야 한다. 메모리 블록을 0으로 설정하는 과정을 가정하자.
- 루프 언롤링(loop unrolling): 인덱스를 유지하고 길이를 갱신하기 위한 시간을 최소화하는 방법
- 메모리를 0으로 만드는데 더 많은 시간을 쓸 수 있다.
- 더프의 장치(Duff’s Device): 루프를 여덟 번 펼치고 남은 바이트 수에 따라 적당한 위치부터 펼친 루프의 단계를 시작하는 방법
- 메모리를 0으로 설정하는 시간이 더 개선됨
- 여덟 번은 최대한의 성능을 위해 설정된 횟수
벡터를 이용한 I/O
- 데이터의 효율적인 복사는 시스템 성능에 중요
방법
- 크기와 데이터에 대한 포인터로 이루어진 벡터를 운영체제에 넘김
- 운영체제는 벡터에 저장된 데이터를 이용해 순서대로 조합
- 수집(gathering): 벡터를 활용해 데이터를 쓰는 행위
- 여러 위치에서 데이터를 모아서 쓴다는 의미
- 분산(scattering): 벡터를 사용해 데이터를 읽는 행위
- 여러 위치로 데이터를 분산시킨다는 의미
객체 지향의 함정
- 객체에는 메서드(함수)와 프로퍼티(데이터)가 있다.
- 객체는 전역적으로 알려진 함수 대신 메서드에 대한 포인터를 가지고 다녀야 한다.
- 따라서 객체 내의 데이터가 데이터만 저장하는 데이터 구조처럼 꽉 짜여 있지 않다.
- 성능이 중요할 때는 전통적인 배열을 활용하는 것이 낫다.
정렬
- 정렬 대상이 포인터 크기보다 크다면 데이터를 가리키는 포인터를 정렬해야 한다.
- 데이터 자체가 움직이지 않도록 해야 한다.
- 정렬은 어떤 수가 다른 수보다 큰지, 같은지, 작은지에 따라 결정된다.
해시
- 일반적으로 검색에 사용할 키에 대해 이들을 균일하게 분포시키는 해시 함수를 적용한다.
- 키에 대응하는 메모리를 해시 함수의 결괏값을 이용해 저장한다.
- 해시 함수의 결괏값은 메모리 크기보다 작으면서 데이터를 적당히 분산시켜야 한다.
- 해시 테이블: 해시 함수의 결과를 배열 인덱스로 활용하는 방법
- 이때 배열의 각 원소를 버킷이라 한다.
- 충돌(collision): 해시 함수 값이 같아 같은 버킷에 데이터가 들어가는 경우
- 해시 체인 등의 방법으로 해결할 수 있다.
- 해시 체인: 충돌이 일어나면 버킷에 들어가려는 데이터들을 체인과 같이 연결해서 버킷에 추가한다.
- 해시 체인 등의 방법으로 해결할 수 있다.
- 완전 해시: 각 키를 유일한 버킷에 연결하는 해시 함수
효율성과 성능
- 데이터베이스 샤딩(sharding): 데이터베이스를 각각 다른 기계에서 실행되는 여러 샤드로 나누는 방식
- 수평 파티셔닝이라고도 한다.
- 인터페이스를 통해 요청이 들어온 데이터베이스 연산을 모든 샤드에 전달하고, 컨트롤러가 결과를 하나로 모은다.
- 작업을 여러 작업자가 병렬적으로 수행하기에 성능이 향상된다.
This post is licensed under CC BY 4.0 by the author.