Skip to content

Commit 80a39e4

Browse files
committed
docs(blog): add unpublished ArrayList vs LinkedList draft with JMH section
1 parent c9b12cd commit 80a39e4

1 file changed

Lines changed: 221 additions & 0 deletions

File tree

  • src/content/posts/2026-02-25-arraylist-vs-linkedlist-cache-locality
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
---
2+
title: "ArrayList vs LinkedList: Big-O는 맞는데, 왜 대부분 ArrayList가 더 빠를까?"
3+
description: "Java 컬렉션 선택을 Big-O가 아닌 CPU 캐시와 메모리 레이아웃 관점에서 정리하고, ArrayDeque 선택 기준과 Project Valhalla의 변화 가능성까지 다룹니다."
4+
date: 2026-02-25
5+
category: "Java > Performance"
6+
tags: ["java", "jvm", "performance", "collections", "arraylist", "linkedlist", "cache", "valhalla"]
7+
draft: false
8+
publish: false
9+
---
10+
11+
Java 컬렉션을 처음 배울 때 보통 이렇게 외웁니다.
12+
13+
- `ArrayList.get(i)`는 O(1)
14+
- `LinkedList`는 중간 삽입/삭제가 O(1)
15+
16+
그래서 직감적으로는 "삽입/삭제가 많으면 LinkedList가 유리하겠네"라고 생각하기 쉽습니다.
17+
그런데 실제 서비스 코드나 벤치마크에서는 대체로 반대 결과가 자주 나옵니다.
18+
19+
이번 글에서는 그 이유를 Big-O가 아니라 **CPU 캐시와 메모리 레이아웃** 관점에서 정리해보겠습니다.
20+
21+
> 이 글에서 말하는 "메모리 모델"은 JMM(Java Memory Model: happens-before, visibility)이 아니라, CPU cache hierarchy + locality 관점입니다.
22+
23+
---
24+
25+
## 1) Big-O는 틀린 게 아니라, 생략이 많다
26+
27+
Big-O는 연산 횟수의 증가율을 설명합니다. 하지만 실제 실행 시간은 여기에 다음 요소가 함께 붙습니다.
28+
29+
- 메모리 접근 패턴(연속/불연속)
30+
- 캐시 미스와 TLB 미스
31+
- 분기 예측 실패
32+
- 객체 할당/GC 비용
33+
34+
즉, 현실 성능은 대략 아래처럼 결정됩니다.
35+
36+
```
37+
실행 시간 = 알고리즘 복잡도 + 메모리 계층 비용 + 런타임/JIT/GC 비용
38+
```
39+
40+
그래서 복잡도 표만 보면 비슷해 보이는 연산이 실제로는 크게 벌어질 수 있습니다.
41+
42+
---
43+
44+
## 2) ArrayList와 LinkedList의 핵심 차이: 레이아웃
45+
46+
`ArrayList`는 내부적으로 `Object[]` 배열을 사용합니다.[^arraylist-javadoc]
47+
반면 `LinkedList`는 노드(prev/item/next)가 연결된 구조입니다.[^linkedlist-javadoc]
48+
49+
```
50+
ArrayList (contiguous references)
51+
[ref][ref][ref][ref][ref]...
52+
53+
LinkedList (pointer chain)
54+
Node <-> Node <-> Node <-> Node ...
55+
```
56+
57+
여기서 중요한 포인트가 하나 있습니다.
58+
59+
- `ArrayList`가 locality 이점을 주는 대상은 "객체 자체"가 아니라 우선 "참조 배열(Object[])"입니다.
60+
- 즉 원소가 참조하는 객체까지 항상 인접하다는 뜻은 아닙니다.
61+
62+
그럼에도 순회/인덱스 접근에서 유리한 이유는, 참조 배열 자체가 연속 메모리이기 때문입니다.
63+
64+
---
65+
66+
## 3) 왜 캐시가 승부를 바꾸는가
67+
68+
CPU는 메모리를 바이트 단위가 아니라 **cache line 단위**로 가져옵니다(대부분 64B).
69+
70+
배열은 연속 메모리라서, `arr[i]`를 읽을 때 주변 원소 참조들도 같이 캐시에 올라옵니다.
71+
그래서 다음 접근이 캐시 히트가 될 가능성이 큽니다(공간 지역성).
72+
73+
반대로 linked list는 다음 노드 주소를 따라가야 하므로,
74+
75+
- 다음 데이터 위치를 미리 예측하기 어렵고
76+
- pointer chasing으로 캐시/TLB 미스가 누적되기 쉽고
77+
- 하드웨어 prefetcher가 잘 먹히지 않는 경우가 많습니다
78+
79+
Oracle dev.java의 공식 비교 자료도 이 점을 명확히 설명합니다.[^devjava-al-vs-ll]
80+
81+
---
82+
83+
## 4) "LinkedList 삽입 O(1)"의 실무 함정
84+
85+
"LinkedList 삽입 O(1)"은 맞는 말입니다. 단, **삽입 위치의 노드를 이미 알고 있을 때**에 한해서입니다.
86+
87+
대부분의 비즈니스 코드는 `add(index, value)` 또는 `get(index)` 형태를 사용합니다.
88+
이 경우 먼저 해당 위치까지 탐색(O(n))이 필요하고, 여기서 pointer chasing 비용이 더해집니다.
89+
90+
결국 실무에서는 아래 패턴이 자주 나타납니다.
91+
92+
- 이론: 삽입 자체는 O(1)
93+
- 현실: 위치 탐색 O(n) + 캐시 미스 + 노드 할당 비용
94+
95+
그래서 일반적인 List 사용에서는 `ArrayList`가 더 빠른 경우가 훨씬 많습니다.
96+
97+
`RandomAccess` 마커 인터페이스가 존재하는 이유도 바로 이 차이를 알고리즘이 구분하기 위함입니다.[^randomaccess-javadoc]
98+
99+
---
100+
101+
## 5) Java 2D 배열: "완전 flat"이 아니라 array-of-arrays
102+
103+
여기서 자주 헷갈리는 포인트가 있습니다.
104+
105+
Java의 `int[][]`는 C 스타일의 단일 연속 2D 블록이 아니라, **배열의 배열**입니다.[^jls-arrays]
106+
107+
그래도 아래 순회는 성능 차이가 납니다.
108+
109+
```java
110+
// row-wise (보통 유리)
111+
for (int i = 0; i < rows; i++) {
112+
for (int j = 0; j < cols; j++) {
113+
sum += a[i][j];
114+
}
115+
}
116+
117+
// column-wise (보통 불리)
118+
for (int j = 0; j < cols; j++) {
119+
for (int i = 0; i < rows; i++) {
120+
sum += a[i][j];
121+
}
122+
}
123+
```
124+
125+
이유는 간단합니다.
126+
127+
- row-wise: 같은 `a[i]` 내부를 연속 접근(좋은 locality)
128+
- column-wise: 서로 다른 행 배열을 계속 점프(나쁜 locality)
129+
130+
즉 Java 2D 배열은 "완전 flat"이라서가 아니라, **행 내부 연속성** 때문에 row-wise가 일반적으로 유리합니다.
131+
132+
---
133+
134+
## 6) Deque는 보통 ArrayDeque를 기본값으로
135+
136+
Deque/Queue/Stack 용도로는 `LinkedList`보다 `ArrayDeque`가 기본 선택에 가깝습니다.
137+
138+
Javadoc도 직접 이렇게 말합니다.
139+
140+
> "ArrayDeque는 queue로 사용할 때 LinkedList보다 faster일 가능성이 높다."[^arraydeque-javadoc]
141+
142+
물론 예외는 있습니다.
143+
144+
- `null` 원소를 꼭 저장해야 한다 (`ArrayDeque`는 null 금지)
145+
- `List` API까지 함께 필요하다 (`LinkedList``List`도 구현)
146+
147+
이런 요구가 없다면 Deque는 `ArrayDeque`를 먼저 고려하는 것이 보통 더 안전합니다.
148+
149+
---
150+
151+
## 7) 앞으로의 변수: Project Valhalla
152+
153+
Project Valhalla(JEP 401)는 value class/value object를 통해 JVM이 더 공격적인 메모리 최적화를 할 여지를 넓히는 방향입니다.[^jep401][^valhalla]
154+
155+
핵심은 "가능성"입니다.
156+
157+
- value object는 identity가 없으므로
158+
- JVM이 flattening/scalarization 같은 표현 최적화를 적용할 수 있고
159+
- 특정 경우 locality/footprint에 유리해질 수 있습니다
160+
161+
하지만 여기서 과장하면 안 됩니다.
162+
163+
- 어떤 레이아웃이 항상 보장되는 것은 아님
164+
- 모든 타입/모든 상황에서 flattening이 일어나는 것도 아님
165+
- 현재는 preview 성격이므로 버전/구현별 차이를 전제로 봐야 함
166+
167+
즉 Valhalla는 "Linked 구조가 자동으로 사라진다"가 아니라, **"JVM 최적화 가능 공간이 커진다"**로 이해하는 것이 정확합니다.
168+
169+
---
170+
171+
## 8) 실무 선택 가이드
172+
173+
제가 실제 코드 리뷰에서 자주 쓰는 기준은 아래입니다.
174+
175+
1. 특별한 이유가 없으면 `List``ArrayList`부터 시작
176+
2. Deque가 필요하면 `ArrayDeque`부터 시작
177+
3. LinkedList를 선택할 때는 "왜 LinkedList여야 하는지"를 코드 코멘트/리뷰에 남김
178+
4. 성능 이슈는 `System.nanoTime()` 루프가 아니라 JMH로 검증[^jmh]
179+
180+
Big-O는 여전히 중요합니다. 다만 **현대 CPU에서 데이터가 어떻게 배치되고 이동하는지**까지 같이 봐야, 실제 서비스 성능을 예측할 수 있습니다.
181+
182+
---
183+
184+
## 9) 재현 가능한 JMH 예시 (로컬 + GitHub Actions)
185+
186+
말로만 "ArrayList가 보통 빠르다"고 끝내지 않기 위해, 이 저장소에 JMH 예시를 같이 두었습니다.
187+
188+
- 벤치마크 코드: `benchmarks/jmh-arraylist-vs-linkedlist/src/main/java/io/clickin/bench/ListAndDequeBenchmark.java`
189+
- 실행 워크플로: `.github/workflows/jmh-collections-benchmark.yml`
190+
191+
로컬에서는 아래처럼 동일한 설정으로 바로 실행할 수 있습니다.
192+
193+
```bash
194+
mvn -B -ntp -f benchmarks/jmh-arraylist-vs-linkedlist/pom.xml clean package
195+
java -jar benchmarks/jmh-arraylist-vs-linkedlist/target/jmh-benchmarks.jar \
196+
io.clickin.bench.ListAndDequeBenchmark \
197+
-wi 5 -i 8 -w 300ms -r 300ms -f 1 -tu ns \
198+
-jvmArgs "-Xms1g -Xmx1g" \
199+
-rf json -rff benchmarks/jmh-arraylist-vs-linkedlist/results/jmh-result.json
200+
```
201+
202+
GitHub Actions 실행 시에는 결과(JSON/TXT)와 실행 환경 정보(Java version, CPU 정보)를 artifact로 올려서,
203+
"어떤 머신/JDK에서 나온 숫자인지"를 추적 가능하게 했습니다.
204+
205+
결론은 단순합니다.
206+
207+
- 이론 복잡도만 보면 LinkedList가 좋아 보일 때가 있어도
208+
- 일반적인 Java 애플리케이션의 대부분 시나리오에서는
209+
- `ArrayList`(그리고 Deque라면 `ArrayDeque`)가 더 좋은 기본값입니다.
210+
211+
---
212+
213+
[^arraylist-javadoc]: ArrayList Javadoc, Oracle Java SE 25 — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ArrayList.html
214+
[^linkedlist-javadoc]: LinkedList Javadoc, Oracle Java SE 25 — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/LinkedList.html
215+
[^randomaccess-javadoc]: RandomAccess Javadoc, Oracle Java SE 17 — https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/RandomAccess.html
216+
[^devjava-al-vs-ll]: ArrayList vs LinkedList, dev.java (Oracle) — https://dev.java/learn/api/collections-framework/arraylist-vs-linkedlist/
217+
[^arraydeque-javadoc]: ArrayDeque Javadoc, Oracle Java SE 25 — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/ArrayDeque.html
218+
[^jls-arrays]: JLS Chapter 10 (Arrays), multidimensional arrays are arrays of arrays — https://docs.oracle.com/javase/specs/jls/se24/html/jls-10.html
219+
[^jep401]: JEP 401: Value Classes and Objects (Preview) — https://openjdk.org/jeps/401
220+
[^valhalla]: OpenJDK Project Valhalla — https://openjdk.org/projects/valhalla/
221+
[^jmh]: OpenJDK JMH — https://github.com/openjdk/jmh

0 commit comments

Comments
 (0)