개발 커뮤니티에서는 오래전부터 **"n++가 n = n + 1보다 빠르다"**는 속설이 회자됩니다. 그러나 현대 컴파일러와 런타임(JIT)은 두 표현을 거의 동일한 기계어(예: x86의 add reg, 1)로 낮추므로, 기본 정수형에서는 체감 가능한 성능 차이가 사실상 없습니다. 그럼에도 이 믿음이 남아 있는 이유는 과거 ISA 인코딩 차이, 플래그 처리, 그리고 언어별 의미론 차이에서 비롯된 오해가 적지 않기 때문입니다.
이 글에서는 속설의 배경(CPU/ISA 관점의 inc vs add, 컴파일러/JIT 최적화), 언어별 주의사항(C++ 반복자에서 ++i 권장 이유, 멀티스레드 원자성), 올바른 마이크로벤치마크 방법, 그리고 실무에서의 성능 우선순위(알고리즘·캐시·메모리 레이아웃)를 정리합니다. 핵심 요지는 하나입니다. 기본 정수형 증가에서는 연산자 형태가 성능을 좌우하지 않으며, 가독성과 코드 의미가 먼저이고, 진짜 성능은 더 큰 구조적 결정이 좌우합니다.
요약
- 결론: 기본 정수형에서는
n++와n = n + 1(또는n += 1)이 현대 컴파일러/JIT에서 동일한 기계어로 최적화되어 성능 차이가 없다. - 이유: x86 등에서 모두
add reg, 1로 생성되는 경우가 일반적이며, 과거의inc/dec미세 차이는 플래그 처리 차이 때문에 오늘날엔 오히려 잘 쓰이지 않는다. - 예외: C++ 반복자·사용자 정의 타입에서는
i++(후위)가 이전 값 보존을 위해 불필요한 복사 비용이 생길 수 있어 **++i(전위)**를 권장한다.
소스 표현에서 기계어까지: 동일 최적화 흐름
같은 의미의 증가 연산은 현대 툴체인에서 하나의 기계어로 수렴합니다. 개념적으로는 아래와 같습니다.
flowchart LR
subgraph source["소스 코드"]
A["n++"]
B["n += 1"]
C["n = n + 1"]
end
Compiler["컴파일러또는 JIT"]
SameAsm["동일 기계어add 한 번"]
source --> Compiler
Compiler --> SameAsm
- 노드 ID:
source,A,B,C,Compiler,SameAsm— 공백 없음, 예약어 미사용. - 라벨: 수식·등호가 있는 경우 큰따옴표로 감쌌고, 줄바꿈은
</br>사용.
오래된 속설과 현대 최적화
과거에는 “n++가 더 빠르다"는 주장이 있었지만, 현대의 C/C++/Java/C#/Go/Rust 컴파일러와 런타임 JIT는 간단한 정수 증가를 동일한 수준의 기계어로 축약합니다. 특히 x86에서는 상태 플래그 처리 특성 때문에 inc보다 add가 더 일관된 선택이며, 대부분의 컴파일러가 **add reg, 1**을 선호합니다.
같은 의미면 같은 코드가 나온다
아래 C/C++ 예시는 최적화를 켠 경우 보통 동일한 코드로 컴파일됩니다.
| |
둘 다 대개 레지스터에 대해 add 한 번으로 표현됩니다. Java/C# 같은 JIT 환경에서도 JIT 워밍업 후 핫 패스에 동일한 증분이 배치됩니다. 생성된 어셈블리는 Compiler Explorer에서 쉽게 확인할 수 있습니다.
언제 차이가 날 수 있나: C++ 반복자와 사용자 정의 타입
기본 정수형과 달리, 반복자나 사용자 정의 타입에서는 후위 증가(i++)가 이전 값을 반환해야 하므로 임시 객체 생성·복사 비용이 발생할 수 있습니다. 이 때문에 C++ 커뮤니티에서는 전위 증가(++i) 습관을 권장합니다. 단, 기본 정수형에서는 i++와 ++i가 같은 코드가 되어 성능 차이는 없습니다.
flowchart TD
IsFundamental{"기본 정수형인가?"}
UseEither["아무 것이나 사용가독성 우선"]
PreferPreInc["전위 증가 권장++i"]
IsFundamental -->|"예"| UseEither
IsFundamental -->|"아니오"| PreferPreInc
| |
마이크로벤치마크 시 주의 사항
미세 차이를 직접 재려고 하면, 측정 자체가 더 큰 오차를 만듭니다. 올바른 방법은 다음과 같습니다.
| 항목 | 권장 사항 |
|---|---|
| 최적화 옵션 | C/C++는 -O2/-O3 사용, 릴리즈 빌드에서 측정 |
| 워밍업 | JIT 언어(Java/C#)는 워밍업 후 steady-state 측정 |
| 프레임워크 | Java: JMH, .NET: BenchmarkDotNet, C++: Google Benchmark |
| 소거 방지 | 결과를 관측하거나 DoNotOptimize/ClobberMemory 등으로 DCE(dead code elimination) 방지 |
이 주제는 알고리즘 선택이나 메모리 접근 패턴 같은 큰 요인에 비해 영향이 미미합니다.
실무 추천 가이드
- 기본 정수형: 가독성에 맞게 아무 것이나 사용해도 무방.
- C++ 반복자/사용자 정의 타입: 관례적으로
++i사용. - 성능 최적화 우선순위: 자료구조 선택, 캐시 적중률, 분기 예측, 메모리 레이아웃 개선이 훨씬 중요.
CPU/ISA 관점: INC vs ADD, 왜 ADD가 보편적인가
현대 x86에서 정수 1 증가는 대개 **add reg, 1**로 표현됩니다. 과거에는 inc reg가 더 짧은 인코딩을 제공해 미세하게 유리하다는 인식이 있었지만, 최근 컴파일러는 아래 이유로 add를 선호합니다.
- 플래그(FLAGS) 일관성:
inc/dec는 **CF(Carry Flag)**를 변경하지 않지만,add/sub는 변경합니다. 많은 최적화와 패턴 매칭이 CF를 포함한 전체 플래그를 전제로 하므로add가 더 예측 가능합니다. - 마이크로아키텍처: 일부 마이크로아키텍처에서
inc/dec는 부분 플래그 업데이트로 인해 플래그 의존성 추적이 까다로울 수 있습니다.add는 일반적으로 플래그 처리 경로가 잘 최적화되어 있습니다. - 다른 ISA: AArch64(ARM64)에는 별도
inc가 없고, 즉치수 더하기(add xN, xN, #1)로 표현합니다. 결국 “증가 = 더하기"가 보편적입니다.
정리하면, 오늘날 대부분의 컴파일러/어셈블러가 간결성과 일관성을 위해 add를 선택하고, 그 결과 n++, n += 1, n = n + 1이 같은 기계어로 수렴합니다.
컴파일러/JIT 관점: 어떤 코드가 생성되나
- GCC/Clang/MSVC: 최적화(
-O2/-O3)에서 단순 정수 증가를add한 번으로 내보냅니다. 인라이닝·레지스터 할당에 따라 메모리 대신 레지스터에서 수행됩니다. - HotSpot(Java), RyuJIT(.NET): JIT 워밍업 후 핫 루프에서 동일하게 “더하기 1"로 낮아집니다. 초기 인터프리터·tiered JIT 단계에서는 다소 다른 형태가 보일 수 있으나, steady-state에서는 동일해집니다.
- 확인 방법: Compiler Explorer에서 생성 어셈블리를 바로 확인할 수 있습니다.
언어별 주의사항
- C/C++: 서브식에서의 중복 수정/접근은 정의되지 않은 동작이 될 수 있습니다. 예:
a = a++는 피해야 합니다. 반복자·사용자 정의 타입은i++가 이전 값 보존을 위해 임시를 만들 수 있어++i권장. - Java/C#:
n++는 원자적이지 않습니다. 다중 스레드에서 원자적 증가가 필요하면AtomicInteger.incrementAndGet()(Java),Interlocked.Increment또는Interlocked계열(C#)을 사용하세요. 성능상n++vsn = n + 1차이는 JIT 후 사실상 없습니다. - Go/Rust: Rust는
++연산자가 없고n += 1을 씁니다. Go는n++가 문(statement)이며 표현식에 쓸 수 없습니다. 의미 차이를 이해하고 스타일 가이드를 따릅니다. - Python/JS: 동적·고수준 언어에서는 객체 불변성(Python의 int)과 런타임 오버헤드가 지배적이므로, 미시적 연산자 선택은 의미가 거의 없습니다.
마이크로벤치마크 템플릿
아래 예시들은 “같은 의미면 같은 코드가 나온다"를 직접 확인하는 데 도움을 줍니다. 릴리즈 빌드와 워밍업을 반드시 고려하세요.
| |
| |
| |
FAQ
| 질문 | 답변 |
|---|---|
| 무엇이 더 빠른가? | 기본 정수형에서는 보통 동일하다. 컴파일러/JIT가 같은 기계어로 만든다. |
그럼 왜 ++i가 권장되나? | 반복자/사용자 정의 타입에서 후위 증가가 임시를 만들 수 있기 때문이다. 정수형에는 영향이 거의 없다. |
| 원자적 증가가 필요한가? | 언어별 원자 연산 API(Java AtomicInteger, C# Interlocked 등)를 사용하라. n++ 자체는 원자적이지 않다. |
| 오버플로는? | C/C++에서 부호 있는 정수 오버플로는 정의되지 않은 동작이다. 연산자 형태와 무관하게 주의해야 하며, 필요하면 더 넓은 타입이나 명시적 모듈러 연산을 고려하라. |
핵심 정리
- 같은 의미라면 오늘날 컴파일러/JIT는 같은 기계어로 수렴한다.
- 반복자/사용자 정의 타입에서는 **
++i**를 습관적으로 사용하자. - 마이크로 차이보다 알고리즘·캐시·메모리 레이아웃이 성능을 좌우한다.
참고 문헌
- Stack Overflow: Why does n++ execute faster than n=n+1? — 현대 컴파일러에서는 둘 다 동일한 어셈블리로 컴파일된다는 설명과 GCC 어셈블리 예시.
- Compiler Explorer (godbolt.org) — 다양한 컴파일러·옵션으로 C/C++/Rust 등 소스와 생성 기계어를 비교할 수 있는 도구.
- isocpp.org: Efficiency of postincrement v.s. preincrement in C++ — 기본 타입 vs 복잡 타입, 전위/후위 증가의 이론적·실질적 차이 정리.
![Featured image of post [Programming] n++ vs n = n + 1: 성능과 최적화의 진실](/post/2025-09-16-npp-vs-n-plus-equals-1/wordcloud_hu_27a0d9faf80a4ae0.webp)
![[Rust] Comprehensive Rust 무료 강의 정리 및 코스 구조](/post/2022-12-30-comprehensive-rust/wordcloud_hu_d1420ff38434cdb6.webp)
![[Hardware] LattePanda Alpha에 Ubuntu 16.04 LTS 설치 가이드](/post/2018-12-06-install-ubuntu-16.04-on-lattepanda/wordcloud_hu_fc536f8de2cbd4bf.webp)
![[CleanCode] 코드 최적화: 조건문 올리고 반복문 내리기](/post/2025-05-20-the-secrets-of-code-optimization-pushing-conditionals-up-and-loops-down/index_hu_9d2e58a5aa4a31a9.webp)
![[Data Structure] C# Lock-Free 우선순위 큐 구현과 동시성 기법](/post/2025-02-28-csharp-lock-free-priority-queue/index_hu_fbb7019f46127494.webp)
![[C++] cout 소수점 자릿수·정밀도 제어 (precision, fixed)](/post/2022-03-29-cpp-cout-precision/wordcloud_hu_b376048fefd128.webp)