성능 최적화 완벽 가이드 | C++, Python, Java, JavaScript 언어별 전략
이 글의 핵심
성능 최적화 완벽 가이드입니다. 언어별 최적화 기법과 프로파일링 도구, 실무 전략을 상세히 설명합니다.
들어가며: 성능 최적화의 원칙
”내 코드가 왜 느릴까?”
성능 최적화는 측정 → 분석 → 개선 → 검증의 반복입니다. 추측이 아닌 데이터 기반으로 접근해야 합니다.
이 글에서 다루는 것:
- 언어별 프로파일링 도구
- 알고리즘 최적화
- 메모리 최적화
- 캐싱 전략
- 실무 최적화 사례
목차
1. 최적화 원칙
최적화의 3대 원칙
flowchart LR
A[최적화 시작] --> B[1. 측정]
B --> C[2. 병목 찾기]
C --> D[3. 최적화]
D --> E[4. 검증]
E --> B
1. 측정 먼저 (Measure First)
❌ "이 코드가 느릴 것 같아" (추측)
✅ "프로파일러로 측정한 결과 이 함수가 80% 시간 소요" (데이터)
2. 병목 찾기 (Find Bottleneck)
전체 실행 시간: 10초
├─ 함수 A: 0.1초 (1%)
├─ 함수 B: 8초 (80%) ← 병목!
└─ 함수 C: 1.9초 (19%)
→ 함수 B를 최적화하면 가장 큰 효과
3. 80/20 법칙
코드의 20%가 실행 시간의 80%를 차지
→ 그 20%만 최적화하면 충분
최적화 우선순위
graph TB
A[최적화 우선순위] --> B[1. 알고리즘]
A --> C[2. 자료구조]
A --> D[3. 캐싱]
A --> E[4. 병렬화]
A --> F[5. 언어/컴파일러]
B --> B1[On² → On]
C --> C1[배열 → 해시맵]
D --> D1[중복 계산 제거]
E --> E1[멀티스레드]
F --> F1[컴파일러 옵션]
예제:
# ❌ O(n²) 알고리즘
def has_duplicate(arr):
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j]:
return True
return False
# ✅ O(n) 알고리즘 (해시셋 사용)
def has_duplicate(arr):
seen = set()
for x in arr:
if x in seen:
return True
seen.add(x)
return False
# 성능 차이: 100만 개 배열
# O(n²): 몇 시간
# O(n): 0.1초
2. 프로파일링
언어별 프로파일링 도구
| 언어 | 도구 | 사용법 |
|---|---|---|
| C++ | gprof, Valgrind, perf | g++ -pg, valgrind --tool=callgrind |
| Python | cProfile, line_profiler | python -m cProfile script.py |
| Java | VisualVM, JProfiler | JVM 옵션 또는 IDE 통합 |
| JavaScript | Chrome DevTools, Node.js Profiler | node --prof script.js |
C++ 프로파일링
# gprof 사용
g++ -pg -O2 main.cpp -o main
./main
gprof main gmon.out > analysis.txt
# Valgrind Callgrind
valgrind --tool=callgrind ./main
kcachegrind callgrind.out.*
# perf (Linux)
perf record ./main
perf report
출력 예제:
Flat profile:
Each sample counts as 0.01 seconds.
% cumulative self self total
time seconds seconds calls ms/call ms/call name
80.00 0.80 0.80 1 800.00 800.00 slow_function
15.00 0.95 0.15 100000 0.00 0.00 fast_function
5.00 1.00 0.05 1 50.00 50.00 main
Python 프로파일링
import cProfile
import pstats
def slow_function():
total = 0
for i in range(1000000):
total += i
return total
def fast_function():
return sum(range(1000000))
# 프로파일링
cProfile.run('slow_function()', 'profile_stats')
# 결과 분석
p = pstats.Stats('profile_stats')
p.sort_stats('cumulative')
p.print_stats(10)
line_profiler (줄 단위 프로파일링):
# pip install line_profiler
@profile
def my_function():
total = 0
for i in range(1000000): # 이 줄이 느림
total += i
return total
# 실행
# kernprof -l -v script.py
JavaScript 프로파일링
Chrome DevTools:
// 1. Chrome DevTools 열기 (F12)
// 2. Performance 탭
// 3. Record 버튼 클릭
// 4. 작업 수행
// 5. Stop 버튼 클릭
// 6. Flame Chart 분석
function slowFunction() {
let total = 0;
for (let i = 0; i < 1000000; i++) {
total += i;
}
return total;
}
console.time('slowFunction');
slowFunction();
console.timeEnd('slowFunction');
// slowFunction: 5.234ms
Node.js 프로파일링:
# V8 프로파일러
node --prof script.js
node --prof-process isolate-*.log > processed.txt
# Clinic.js
npm install -g clinic
clinic doctor -- node script.js
3. 알고리즘 최적화
시간복잡도 개선
예제 1: 중복 찾기
# ❌ O(n²) - 느림
def find_duplicates(arr):
duplicates = []
for i in range(len(arr)):
for j in range(i + 1, len(arr)):
if arr[i] == arr[j] and arr[i] not in duplicates:
duplicates.append(arr[i])
return duplicates
# ✅ O(n) - 빠름
def find_duplicates(arr):
seen = set()
duplicates = set()
for x in arr:
if x in seen:
duplicates.add(x)
seen.add(x)
return list(duplicates)
# 성능 차이: 10만 개 배열
# O(n²): 30초
# O(n): 0.01초
예제 2: 두 수의 합
// ❌ O(n²)
vector<pair<int,int>> twoSum(vector<int>& arr, int target) {
vector<pair<int,int>> result;
for (int i = 0; i < arr.size(); i++) {
for (int j = i + 1; j < arr.size(); j++) {
if (arr[i] + arr[j] == target) {
result.push_back({i, j});
}
}
}
return result;
}
// ✅ O(n) - 해시맵 사용
vector<pair<int,int>> twoSum(vector<int>& arr, int target) {
unordered_map<int, int> seen;
vector<pair<int,int>> result;
for (int i = 0; i < arr.size(); i++) {
int complement = target - arr[i];
if (seen.find(complement) != seen.end()) {
result.push_back({seen[complement], i});
}
seen[arr[i]] = i;
}
return result;
}
캐싱 (메모이제이션)
# ❌ 중복 계산
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# fibonacci(40): 몇 초 소요
# ✅ 메모이제이션
from functools import lru_cache
@lru_cache(maxsize=None)
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)
# fibonacci(40): 0.001초
4. 메모리 최적화
C++ 메모리 최적화
// ❌ 불필요한 복사
void process(vector<int> data) { // 복사 발생
// ...
}
// ✅ 참조 사용
void process(const vector<int>& data) { // 복사 없음
// ...
}
// ❌ 작은 객체를 힙에 할당
for (int i = 0; i < 1000000; i++) {
int* p = new int(i); // 느림
delete p;
}
// ✅ 스택 사용
for (int i = 0; i < 1000000; i++) {
int value = i; // 빠름
}
// ❌ 빈번한 재할당
vector<int> vec;
for (int i = 0; i < 1000000; i++) {
vec.push_back(i); // 재할당 발생
}
// ✅ 미리 예약
vector<int> vec;
vec.reserve(1000000); // 재할당 방지
for (int i = 0; i < 1000000; i++) {
vec.push_back(i);
}
Python 메모리 최적화
# ❌ 리스트 (메모리 많이 사용)
numbers = [i for i in range(1000000)] # 36MB
# ✅ 제너레이터 (메모리 절약)
numbers = (i for i in range(1000000)) # 200 bytes
# ❌ 문자열 연결 (느림)
result = ""
for i in range(10000):
result += str(i) # 매번 새 문자열 생성
# ✅ join 사용 (빠름)
result = "".join(str(i) for i in range(10000))
# ❌ 전역 변수 (느림)
global_var = 0
def increment():
global global_var
global_var += 1
# ✅ 지역 변수 (빠름)
def increment(var):
return var + 1
Java 메모리 최적화
// ❌ 불필요한 객체 생성
for (int i = 0; i < 1000000; i++) {
String s = new String("hello"); // 느림
}
// ✅ 문자열 리터럴 사용
for (int i = 0; i < 1000000; i++) {
String s = "hello"; // 빠름 (String Pool)
}
// ❌ StringBuilder 없이 연결
String result = "";
for (int i = 0; i < 10000; i++) {
result += i; // 느림
}
// ✅ StringBuilder 사용
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
String result = sb.toString();
5. 언어별 최적화
C++ 최적화
컴파일러 최적화:
# 최적화 레벨
g++ -O0 main.cpp # 최적화 없음 (디버그)
g++ -O1 main.cpp # 기본 최적화
g++ -O2 main.cpp # 권장 최적화
g++ -O3 main.cpp # 공격적 최적화
# 추가 옵션
g++ -O3 -march=native -flto main.cpp
# -march=native: CPU 최적화
# -flto: Link Time Optimization
인라인 함수:
// ❌ 함수 호출 오버헤드
int add(int a, int b) {
return a + b;
}
// ✅ 인라인 (함수 호출 제거)
inline int add(int a, int b) {
return a + b;
}
// 또는 람다 (자동 인라인)
auto add = [](int a, int b) { return a + b; };
캐시 친화적 코드:
// ❌ 캐시 미스 많음 (열 우선 접근)
for (int j = 0; j < N; j++) {
for (int i = 0; i < N; i++) {
matrix[i][j] = 0;
}
}
// ✅ 캐시 친화적 (행 우선 접근)
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
matrix[i][j] = 0;
}
}
// 성능 차이: 10000×10000 행렬
// 열 우선: 2.5초
// 행 우선: 0.3초 (8배 빠름)
Python 최적화
리스트 컴프리헨션:
# ❌ 느림
result = []
for i in range(1000000):
result.append(i * 2)
# ✅ 빠름 (2배)
result = [i * 2 for i in range(1000000)]
# ✅ 더 빠름 (제너레이터)
result = (i * 2 for i in range(1000000))
내장 함수 사용:
# ❌ 느림
total = 0
for i in range(1000000):
total += i
# ✅ 빠름 (10배)
total = sum(range(1000000))
NumPy 사용:
import numpy as np
# ❌ Python 루프 (느림)
arr = list(range(1000000))
result = [x * 2 for x in arr]
# 시간: 100ms
# ✅ NumPy (빠름)
arr = np.arange(1000000)
result = arr * 2
# 시간: 2ms (50배 빠름)
Java 최적화
Stream vs 반복문:
List<Integer> numbers = IntStream.range(0, 1000000)
.boxed()
.collect(Collectors.toList());
// ❌ Stream (느림)
long sum = numbers.stream()
.mapToInt(Integer::intValue)
.sum();
// 시간: 50ms
// ✅ 반복문 (빠름)
long sum = 0;
for (int num : numbers) {
sum += num;
}
// 시간: 10ms (5배 빠름)
오토박싱 회피:
// ❌ 오토박싱 (느림)
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(i); // int → Integer 변환
}
// ✅ 원시 타입 배열 (빠름)
int[] arr = new int[1000000];
for (int i = 0; i < 1000000; i++) {
arr[i] = i;
}
JavaScript 최적화
배열 메서드 최적화:
const arr = Array.from({ length: 1000000 }, (_, i) => i);
// ❌ 느림
let sum = 0;
arr.forEach(x => sum += x);
// 시간: 20ms
// ✅ 빠름
let sum = 0;
for (let i = 0; i < arr.length; i++) {
sum += arr[i];
}
// 시간: 5ms (4배 빠름)
// ✅ 더 빠름 (내장 함수)
const sum = arr.reduce((acc, x) => acc + x, 0);
// 시간: 8ms
객체 생성 최적화:
// ❌ 느림
const objects = [];
for (let i = 0; i < 100000; i++) {
objects.push({ id: i, name: `User${i}` });
}
// ✅ 빠름 (미리 할당)
const objects = new Array(100000);
for (let i = 0; i < 100000; i++) {
objects[i] = { id: i, name: `User${i}` };
}
6. 정리
최적화 체크리스트
측정:
- 프로파일러로 병목 확인
- 실행 시간 측정
- 메모리 사용량 측정
알고리즘:
- 시간복잡도 개선 (O(n²) → O(n))
- 적절한 자료구조 선택
- 캐싱/메모이제이션
메모리:
- 불필요한 복사 제거
- 메모리 누수 확인
- 객체 재사용
언어별:
- C++: 컴파일러 최적화, 인라인, 캐시 친화적
- Python: 내장 함수, NumPy, 제너레이터
- Java: 오토박싱 회피, StringBuilder
- JavaScript: 배열 메서드, 객체 풀
핵심 원칙
- 측정 먼저: 추측하지 말고 측정
- 병목 집중: 80/20 법칙
- 알고리즘 우선: 언어보다 알고리즘
- 가독성 유지: 과도한 최적화 금지
다음 단계
각 언어의 자세한 최적화 기법은 아래 글을 참고하세요:
- C++ 성능 최적화
- Python 성능 최적화
- 웹 성능 최적화
관련 주제:
- 알고리즘 시간복잡도
- 캐시 최적화
- 병렬 프로그래밍