Arrays, Strings & Memory Layout
연속된 메모리 공간에 데이터를 배치하는 가장 기본적인 물리적 구조인 배열과 문자열의 메모리 레이아웃, 그리고 접근 효율성을 다루는 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
data-structures-algorithmsdata-structuresalgorithmsfoundationscomplexityarraysstringsmemory-layout10 min read
1. Overview
배열, 문자열과 메모리 레이아웃(Arrays, Strings & Memory Layout)은 CPU 캐시의 극한 효율과 연속 메모리의 물리학적 연금술을 통해, 단순한 '박스 집합'처럼 보이는 배열이 실제로는 메모리 계층(Memory Hierarchy)의 가장 예민한 하드웨어 최적화 전장임을 해부하는 기초 공학입니다.
학습자는 arr[10] 선언 하나가 스택(Stack)의 어느 주소에 640비트 연속 블록으로 딱 박혀 있는지, 그리고 그 연속성이 CPU L1 캐시 라인(64Byte)과 어떻게 찰떡처럼 맞아떨어져 for-루프 속도를 수십 배 뻥튀기하는지 **캐시 지역성(Cache Locality)**의 물리학을 뜯어봅니다. 나아가 파이썬/자바의 문자열 불변성(Immutability)이 왜 str + str를 루프 돌릴 때 O(n²)의 메모리 폭탄을 만드는지, 그리고 이중 포인터(C의 char**)와 UTF-8 인코딩의 가변 바이트 지뢰밭까지 해부하여 메모리 수준에서 사고하는 역량을 확보합니다.
2. Scope & Boundaries
In-Scope
- 메모리 연속성과 인덱싱 (Memory Contiguity): 1차원/다차원 배열의 Row-major/Column-major 레이아웃, 랜덤 접근 O(1)의 포인터 산술.
- 캐시 지역성 (Cache Locality): 공간 지역성(Spatial Locality, Prefetch), 시간 지역성(Temporal Locality), 캐시 미스(Cache Miss)의 성능 재앙.
- 문자열 구현 물리학 (String Internals): C-style(null-terminated), 파이썬 intern, Java
StringBuilder, UTF-8 가변 바이트 인코딩. - 동적 배열 (Dynamic Array): C++
std::vector나 파이썬list의 내부 재할당(Amortized O(1) append), 성장 계수(Growth Factor).
Out-of-Scope
- 힙(Heap)의 연결 리스트 기반 자료구조: 배열이 아닌 포인터로 연결된 자료구조들 04-01-02. Linked Lists & Pointer Logic 영역.
- B-트리 기반 인덱스: 데이터베이스의 디스크 기반 인덱스 구조 06-01. Database Index Internals 영역.
Boundaries
- Array vs. Linked List (04-01-02): 배열은 "메모리 덩어리를 미리 연속으로 예약해두는 연속 할당"이라
arr[k]랜덤 접근이 O(1)이지만 중간 삽입이 O(n)(뒤로 다 밀기)인 반면, 연결 리스트는 "메모리 여기저기서 조각을 잡아 포인터로 꿰매는 산발 할당"이라 중간 삽입이 O(1)(포인터만 바꾸기)이지만list[k]접근은 O(k)(k번 포인터 따라가기)이며 CPU 캐시가 빵빵하게 죽는 관계입니다.
3. Counterexample
- 이차원 배열 열 탐색과 캐시 폭발 (Column-Major Cache Miss): C언어 2차원 배열
int A[1000][1000]을for(j) for(i) A[i][j]++처럼 열(Column) 방향으로 순회하는 초보 코드. C/C++은 Row-major 저장(한 행을 연속 메모리에)을 합니다. 열 순회는 매번 1000 * 4byte = 4000바이트를 건너뛰어 완전히 새 캐시 라인을 로드해야 하므로, Row 순회 대비 캐시 미스가 10배 이상 폭발하여 같은 연산인데도 실행 시간이 5~10배 느려지는 처참한 결과를 냅니다. - 문자열 반복 연결의 O(n²) 폭탄 (String Concatenation Trap): 파이썬/자바에서 10만 개의 단어를 하나의 문자열로 만들기 위해
s = s + word를 루프 도는 신참 코드. 파이썬 문자열은 불변(Immutable)이라+연산마다 새 문자열 객체를 메모리에 할당하고 기존 문자열 전체를 복사합니다. 단어 1개짜리 복사, 2개짜리 복사, ... n개짜리 복사를 n번 반복하므로 총 복사량이1+2+...+n = O(n²)메모리 폭탄이 터져, 1만 단어에서 1초이던 작업이 10만 단어에서 100초가 걸리는 지수 폭발 재앙이 됩니다.
4. Prerequisites
- 메모리 주소와 포인터 (Basic): 변수가 실제 램(RAM)의 특정 주소에 위치하며, 인덱스가 그 주소에 오프셋을 더하는 포인터 산술 기초가 필요합니다. (03-03-01 Virtual Memory & Paging)
- CPU 캐시 계층 (Recommended): L1 캐시가 64바이트 라인 단위로 데이터를 가져온다는 하드웨어 이해가 있어야 지역성(Locality)의 위력을 실감합니다. (02-01-02 CPU Architecture)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 연속의 마법과 포인터 산술, 배열 레이아웃 (Array Memory Layout)
- Why to Learn: "배열의 100번째 요소를 읽는 건 왜 항상 O(1)인가?"라는 질문의 답이 곧 컴퓨터 아키텍처의 가장 기초적인 진실인 주소(포인터) + 오프셋 산술 수학임을 꿰기 위함입니다.
- What to Learn:
- Concepts: 연속 메모리(Contiguous Memory), 기저 주소(Base Address), 오프셋(Offset,
i * sizeof(T)), Row-major vs Column-major 레이아웃. - Skills: 2D 배열 선형화(Linearization), 스택/힙 배열 선언의 차이.
- Tools: C언어
sizeof(),&arr[i]주소 출력,gdb메모리 덤프. - Trade-offs: 연속 메모리 배열은 임의 접근(Random Access) O(1)과 캐시 히트율 극강이라는 완벽한 성능이지만, 크기를 사전에 반드시 선언해야 해서 너무 크게 잡으면 메모리 낭비, 너무 작게 잡으면 오버플로우(Buffer Overflow)라는 정적 크기의 감옥 안에 갇히는 딜레마.
- Concepts: 연속 메모리(Contiguous Memory), 기저 주소(Base Address), 오프셋(Offset,
- How to Learn:
- 1단계:
int arr[5]를 선언하면 스택에5 * 4Byte = 20Byte연속 블록이 잡히고,arr[0]의 주소가 예컨대0x7FFF1000이면arr[3]은 무조건0x7FFF1000 + 3*4 = 0x7FFF100C가 되어 포인터 덧셈 한 방에 찾아가는 O(1) 접근 기하학을 해부합니다. - 2단계: C 2D 배열
A[3][4]가 Row-major로A[0][0], A[0][1], A[0][2], A[0][3], A[1][0]...순으로 메모리에 일렬 배치되므로A[i][j] = base + (i*4 + j)*sizeof(int)선형화 공식을 뜯어봅니다.
- 1단계:
- Implement: C 언어(혹은 파이썬
ctypes) 스크립트. 1D 배열의 각 원소 주소를&arr[i]로 출력하여4Byte간격 확인. 2D 배열에서A[0][3]과A[1][0]의 주소가 단4Byte차이임을 증명하는 메모리 레이아웃 덤프 로그.
Recommended
Core Topic 02: 캐시 라인의 마법과 저주, 지역성 물리학 (Cache Locality)
- Why to Learn: 똑같이 백만 번을 접근하는 코드인데 하나는 0.1초, 다른 하나는 5초가 걸리는 기괴한 현상이 CPU 캐시 라인(64B)과의 궁합에서 비롯됨을 장악하고, 실무에서 연산 병목을 하드웨어 레벨에서 뿌리뽑기 위함입니다.
- What to Learn:
- Concepts: 캐시 라인(Cache Line, 64Byte), 공간 지역성(Spatial Locality), 시간 지역성(Temporal Locality), 캐시 미스 패널티(~100클럭).
- Skills: 루프 순서 변환(Loop Interchange), 배열 데이터 정렬(Array-of-Structs vs Struct-of-Arrays).
- Tools:
perf stat -e cache-misses,Valgrind --tool=cachegrind. - Trade-offs: Array-of-Structs(AoS,
[{x,y,color}, {x,y,color}...])는 코드 가독성이 좋고 단일 객체의 모든 필드를 함께 처리(Update)할 때 캐시 히트 극강이지만, 1만 개의 X 좌표만 일괄 계산하는 게임 엔진 루프에서는y, color쓰레기 데이터가 캐시 라인에 함께 올라와 캐시 공간을 낭비하는 반면, Struct-of-Arrays(SoA,{xs:[...], ys:[...]})는 같은 타입끼리 연속 저장되어 X 좌표 배열만 쫙 읽을 때 캐시 효율이 폭발적.
- How to Learn:
- 1단계:
arr[0], arr[1], arr[2]...순 Row 탐색 코드를 실행하면, CPU가arr[0]접근 시 64B 캐시 라인 전체(arr[0]~arr[15])를 한 방에 L1에 끌어올려, 다음 15번 접근은 완전히 공짜(캐시 히트)가 되는 Prefetch 효과를 해부합니다. - 2단계:
arr[0], arr[100], arr[200]...처럼 100칸씩 건너뛰면 매번 새 캐시 라인 로드가 필요하여, 100MHz 연산이 100ns 캐시 미스 패널티를 매번 치르다 사실상 100배 느려지는 비용을 뜯어봅니다.
- 1단계:
- Implement: 파이썬
time.perf_counter()성능 비교.[0] * 1000000리스트를 순차(for i in range(N))로 접근하는 시간 vsrandom.shuffle(indices)후 무작위 접근하는 시간을 10회 평균하여, 캐시 패턴에 따른 최대 5x 속도 차이를 터미널 벤치마크 수치로 증명.
Practical
Core Topic 03: 2배씩 팽창하는 고무줄, 동적 배열 내부 (Dynamic Array & Amortized)
- Why to Learn: 파이썬
list.append()를 수백만 번 쳐도 느려지지 않는 이유와, C++std::vector가 용량 초과 시 왜 1칸이 아닌 2배씩 재할당(Realloc)하는지 그 분할 상환 O(1) 수학의 정체를 꿰기 위함입니다. - What to Learn:
- Concepts: 분할 상환 분석(Amortized Analysis), 성장 계수(Growth Factor, 파이썬 ~1.125, C++ ~2), 재할당 비용(Realloc + Copy).
- Skills: 동적 배열의
append/pop성능 분석, 용량 예약(Pre-allocation). - Tools: 파이썬
sys.getsizeof(list)캐파 추적. - Trade-offs: 성장 계수를 2배(Double)로 잡으면 재할당 빈도가 log N회로 줄어 amortized O(1)가 보장되지만, 방금 전 재할당 직후에는 용량의 절반이 빈 공간으로 남아 메모리 낭비가 최대 50%에 달합니다. 성장 계수를 1.1배처럼 작게 잡으면 메모리 낭비가 줄어드는 대신 재할당(복사) 빈도가 폭증해 총 O(n log n) 복사 비용이 터지는 타협.
- How to Learn:
- 1단계:
append()n번의 총 복사 횟수를 계산. 재할당 시1+2+4+8+...+n ≈ 2n(등비수열 합)이므로 n번의append전체 비용이 O(2n) = O(n), 즉 평균(분할 상환) 1회 비용이 O(1)임을 수학으로 증명합니다. - 2단계: 파이썬
list가 원소를 추가할 때 실제 내부 capacity가[0, 4, 8, 16, 25, 35...]처럼 불규칙한 숫자로 팽창하는 이유(오버헤드 최소화 타협)를sys.getsizeof()로 추적하는 궤적을 뜯어봅니다.
- 1단계:
- Implement: 파이썬
DynamicArray클래스를 리스트 내부 메커니즘 그대로 직접 재현.capacity=1에서 시작해append()시 꽉 차면capacity *= 2후 새 배열로 전체 복사(realloc 모사).append를 1000번 호출하는 동안 총 복사 횟수가 O(2n)에 수렴하는 것을 이벤트 로그로 증명.
Advanced
Core Topic 04: 가변 바이트의 지뢰밭과 불변의 대가, 문자열 인코딩 심층 (String Encoding & Internals)
- Why to Learn:
len("안녕")이 2인지 6인지 모르는 채 문자열 처리를 짜면, 한글/이모지를 다루는 국제화(i18n) 서비스에서 문자가 잘리거나 DB 컬럼이 넘치는 처참한 인코딩 버그를 낳으므로, UTF-8 바이트 물리학을 장악하기 위해서입니다. - What to Learn:
- Concepts: ASCII(1B), UTF-8 가변 바이트(1~4B), CPython String Intern, Java String Pool,
StringBuilder. - Skills: 바이트 단위 슬라이싱(byte slicing), 코드포인트(Code Point) vs 바이트 길이 구분.
- Tools: 파이썬
s.encode('utf-8'),len(s)vslen(s.encode()). - Trade-offs: Python 3의 문자열은 불변(Immutable, 한번 만들면 수정 불가)이라 여러 스레드가 공유해도 락(Lock)이 필요 없는 스레드 세이프(Thread-Safe)를 공짜로 얻지만,
s = s + "x"한 줄이 매번 새 String 객체를 힙(Heap)에 할당하고 구 객체를 GC가 청소하는 메모리 회전 지옥을 만드는 불변성의 역습.
- Concepts: ASCII(1B), UTF-8 가변 바이트(1~4B), CPython String Intern, Java String Pool,
- How to Learn:
- 1단계: "A"는 UTF-8로 1바이트(0x41), "한"은 3바이트(0xED 0x95 0x9C)를 차지합니다. C 언어의
char str[] = "한글"은 7바이트(3+3+null)를 잡지만, Python 3은len("한글") == 2(문자 수 기준)로 알아서 가변 처리합니다. 이 레이어 차이를 해부합니다. - 2단계: Java에서
"hello" == "hello"가true인 이유는 JVM의 String Pool(Intern 테이블)이 동일 문자열 리터럴을 같은 힙 객체에 재활용(Cache)하기 때문임을 뜯어봅니다.
- 1단계: "A"는 UTF-8로 1바이트(0x41), "한"은 3바이트(0xED 0x95 0x9C)를 차지합니다. C 언어의
- Implement: 파이썬 반복 연결 성능 비교.
result = ""에 10만 번result += "x"하는 O(n²) 버전과parts = []; parts.append("x"); "".join(parts)하는 O(n) 버전의 실행 시간을time.perf_counter()로 비교하여, 10배 이상의 속도 격차를 불변성의 실증(Empirical Proof) 로그로 출력.
7. Terminology
8. References
Primary
- [P2] SWEBOK v4.0 - Computing Foundations / Data Structure Basics — Array and layout fundamentals.
- [P1] CS2023 - SDF/Fundamental Data Structures (Arrays) — Core requirements.
Secondary
- [Introduction to Algorithms (CLRS)] Cormen — Formal analysis of arrays.
- [The Art of Computer Programming, Vol 1] Knuth — Fundamental algorithms and layouts.
Industry
- [Intel: Optimization Reference Manual (Memory Layout)] — Hardware-specific alignment strategies.
- [CPP Reference: std::vector and contiguous storage] — Real-world dynamic array spec.
9. Final Checklist
Primary
- 배열의 특정 인덱스에 접근할 때 왜 '상수 시간()'의 물리적 연산만 필요한지 수리적으로 설명 가능한가? (P1)
- 문자열이 메모리에 적재될 때 '인코딩(Encoding)' 방식에 따라 각 문자가 차지하는 물리 바이트 수가 왜 달라지는지 입증할 수 있는 가? (P1)
Secondary
- **캐시 미스(Cache Miss)**가 빈번하게 발생하는 배열 접근 패턴이 시스템 전체 성능을 왜 수십 배까지 깎아먹는지 소통 가능한가?
- 정적 배열과 동적 배열 사이에서 '메모리 파편화(Fragmentation)'가 발생하는 물리적 시나리오를 도출할 수 있는 가?
Industry
- 고성능 게임 엔진이나 금융 시스템 설계 시, 왜 'Array of Structures(AOS)'보다 'Structure of Arrays(SOA)'가 CPU 연산 효율 면에서 유리한지 제안할 수 있는 가? (SFIA)
- 임베디드 장치에서 램 공간을 아끼기 위해 구조체 패딩(Padding)을 최소화하는 '비트 필드(Bit field)' 적용의 물리적 손익을 기술할 수 있는 가?