C++ 인라인 어셈블리 | 'asm' 키워드 가이드 | 핵심 개념과 실전 활용
이 글의 핵심
인라인 어셈블리(asm)는 C++ 코드 안에 어셈블리를 끼워 넣어 특정 아키텍처 명령을 쓰는 고급 기능입니다. GCC 확장 asm 피연산자·클로버·메모리 순서, SIMD 인트린식과의 선택, 프로덕션 래퍼 패턴까지 예제와 함께 정리합니다.
기본 문법
성능 병목이나 특수 명령이 필요할 때만 인라인 어셈블리를 고려하는 것이 일반적입니다. 이 글에서는 컴파일러별 문법 차이를 알아 두고, 이식성을 잃기 쉬운 부분을 판단하는 데 도움이 되도록 예제를 나열합니다.
GCC/Clang (AT&T 문법)
main 함수의 구현 예제입니다.
int main() {
int x = 10;
int y = 20;
int result;
asm("addl %1, %0"
: "=r" (result) // 출력
: "r" (x), "0" (y) // 입력
);
cout << result << endl; // 30
}
MSVC (Intel 문법)
main 함수의 구현 예제입니다.
int main() {
int x = 10;
int y = 20;
int result;
__asm {
mov eax, x
add eax, y
mov result, eax
}
cout << result << endl; // 30
}
레지스터 제약
add 함수의 구현 예제입니다.
int add(int a, int b) {
int result;
asm("addl %2, %0"
: "=r" (result) // 출력: 아무 레지스터
: "0" (a), "r" (b) // 입력
);
return result;
}
// 제약 문자:
// r: 범용 레지스터
// a: %eax/%rax
// b: %ebx/%rbx
// c: %ecx/%rcx
// d: %edx/%rdx
// m: 메모리
// i: 즉시값
GCC 확장 asm(Extended asm) 문법 심화
GCC·Clang 계열의 인라인 asm은 단순히 문자열을 끼워 넣는 것이 아니라, 템플릿, 출력·입력 피연산자, 클로버(clobber) 목록이 결합된 미니 DSL에 가깝습니다. 컴파일러는 템플릿 안의 %0, %1 …를 C/C++ 식과 레지스터 할당 결과로 치환하며, 여기서 실수하면 “조용히 잘못된 기계어”가 만들어지는 경우가 많습니다.
템플릿과 피연산자 번호
기본 형태는 asm [volatile] ( 템플릿 : 출력 : 입력 : 클로버 ) 입니다. 출력·입력 피연산자는 0부터 번호가 매겨지며, 템플릿에서는 %n으로 참조합니다. AT&T 문법에서는 레지스터 이름 앞에 %가 오므로, 리터럴 %eax처럼 쓰려면 %%eax처럼 퍼센트를 이중으로 적어 템플릿 치환과 구분합니다.
같은 피연산자를 입력과 출력에서 묶고 싶으면 매칭 제약을 씁니다. 예를 들어 출력이 "=r"(out)이고 첫 번째 입력이 "0"(in)이면, 둘은 동일 슬롯으로 묶입니다. 덧셈 누적처럼 “같은 레지스터에서 읽고 덮어쓰기”할 때 유용하지만, 컴파일러가 레지스터를 고르는 방식에 따라 의도와 다른 스케줄이 나올 수 있으므로 읽기·쓰기 모드(+)를 쓰는 편이 명확한 경우가 많습니다.
제약 문자와 수식자: =, +, &, g
=: 쓰기 전용 출력(이전 값을 읽지 않는다고 가정).+: 읽기-쓰기(입력 겸 출력). 예:"=r"대신"+r"로 같은 슬롯에서 증가.&: 조기 손상(early-clobber) 출력. 아래 절에서 별도로 설명합니다.g,m,i등: 컴파일러가 즉시값·메모리·레지스터 중 선택할 수 있게 허용하는 정도의 폭을 조절합니다.
제약을 너무 넓게("g"만 남발) 잡으면 컴파일러가 메모리 피연산자를 고르는 바람에 불필요한 로드/스토어가 생길 수 있고, 반대로 너무 좁게 잡으면 스필(spill)이 늘 수 있습니다. 핫 루프라면 Godbolt 등으로 실제 명령 스트림을 확인하는 것이 안전합니다.
volatile의 의미(그리고 남용)
asm volatile은 “부수 효과가 있으니 이 블록을 최적화로 통째로 없애지 말라”는 힌트입니다. 타이밍 측정, 특수 명령 시퀀스, 하드웨어 레지스터 접근처럼 제거되면 의미가 바뀌는 경우에 필요합니다. 반면 순수 계산을 asm으로 옮겼다면 volatile은 오히려 최적화를 막아 성능을 해칠 수 있습니다. 즉 volatile은 동기화 보장이 아니라 최적화 억제에 가깝습니다.
asm goto(GCC/Clang 확장)
레이블로 분기해야 하는 명령(일부 조건부 시스템 콜, 플랫폼별 분기 패치)에서는 asm goto가 쓰입니다. 출력 피연산자 제약이 까다롭고, 가능하면 표준 제어 흐름으로 대체하는 것이 유지보수에 유리합니다. 프로젝트에서 이 확장을 쓸 경우, 빌드 매트릭스(클랭 버전·-std=·다른 TU와의 ABI)를 문서화해 두는 것이 좋습니다.
레지스터 할당과 클로버(clobber)
인라인 asm의 핵심 난이도는 “컴파일러가 무엇을 맡고, 개발자가 무엇을 보증하는가”를 분리하는 데 있습니다.
클로버 목록: 레지스터·"memory"·플래그
네 번째 인자(클로버)에는 이 asm 블록이 덮어쓰는 자원을 나열합니다.
- 명명된 레지스터:
"%rax","%xmm5"처럼 직접 손댄 레지스터. "cc": 플래그 레지스터(조건부 분기·비교에 영향)."memory": 이 블록이 메모리에 대한 가정을 깨뜨릴 수 있음을 알림(아래 메모리 절 참고).
출력·입력 피연산자로 이미 연결된 레지스터는 클로버에 중복 나열할 필요는 없지만, 템플릿 안에서 추가로 건드린 레지스터는 반드시 적어야 합니다. 빠뜨리면 컴파일러가 그 레지스터에 살아 있는 C++ 변수를 유지한 채로 asm을 끼워 넣어 데이터가 조용히 깨집니다.
조기 손상(early-clobber)과 &
출력 피연산자가 입력보다 먼저 덮어써져서는 안 되는 패턴(예: 한 레지스터에서 여러 입력을 읽기 전에 출력이 그 레지스터를 사용)에서는 출력에 & 제약을 붙입니다. 예: "=&r"(dst) 형태. 이렇게 하면 컴파일러는 출력과 입력이 동일 할당되지 않도록 스케줄합니다. & 없이 잘못 묶으면 “같은 번호/슬롯” 최적화가 터져 미묘한 버그가 납니다.
레지스터 변수와 ABI
GCC 확장인 레지스터 변수(특정 레지스터에 고정)는 드물게 쓰이며, 호출 규약·Callee-saved 레지스터와 충돌하기 쉽습니다. 인라인 asm을 “특정 레지스터에 고정”하려는 욕구가 있다면, 먼저 제약 문자("a", "D" 등)와 컴파일러가 허용하는 피연산자 조합으로 해결할 수 있는지 검토하는 편이 낫습니다.
메모리 배리어와 순서 보장
동시성·드라이버·MMIO 코드에서 asm은 종종 순서 문제와 엮입니다. 여기서는 세 층을 구분해야 합니다: C++ 메모리 모델, 컴파일러 재배치, CPU/캐시 일관성.
컴파일러 배리어 vs 하드웨어 펜스
asm volatile("" ::: "memory")형태는 대개 컴파일러에게 “이전 메모리 접근이 이후 접근보다 뒤로 넘어가면 안 된다”는 수준의 순서를 요구합니다(구현·버전에 따라 세부는 다를 수 있으나, 관용적으로 컴파일러 배리어로 쓰입니다).mfence/sfence/lfence등은 실제 명령으로 파이프라인·스토어 버퍼·로드 순서에 영향을 줄 수 있습니다. 아키텍처마다 의미가 다르므로, “배리어 한 줄이면 동기화 끝”이라고 가정하면 위험합니다.
x86은 전통적으로 강한 메모리 모델로 알려져 있으나, 락(lock) 접두사, 원자적 연산, 릴렉스드/획득-해제 같은 C++ std::atomic 수준의 요구와 1:1로 대응하지는 않습니다. 가능하면 동기화는 std::atomic과 표준 메모리 순서로 표현하고, asm은 “부득이한 명령”에 한정하는 것이 프로덕션에서 안전합니다.
"memory" 클로버와 최적화
"memory" 클로버는 “임의의 메모리가 이 asm 전후로 일관되게 관찰되어야 한다”는 보수적 모델을 유도합니다. 범위가 넓어 레지스터에 캐시된 값 재사용까지 막을 수 있어, 핫 루프에 남발하면 성능이 떨어집니다. 정말로 필요한지, 더 좁은 순서 보장(atomic 연산, 타깃팅된 컴파일러 내장)으로 대체할 수 없는지 먼저 판단해야 합니다.
C++ std::atomic과 중복·충돌
이미 memory_order_acquire/release로 표현 가능한 패턴에 mfence를 추가로 끼워 넣으면 과도한 동기화가 될 수 있습니다. 반대로 약한 순서로는 하드웨어 레지스터 쓰기가 관측되지 않는 문제가 생길 수 있어, MMIO는 종종 volatile std::uint32_t*와 컴파일러 배리어의 조합(혹은 플랫폼 문서가 권장하는 순서)이 따로 논의됩니다.
SIMD 인트린식 대 인라인 asm
SIMD는 인트린식 우선이 거의 정답에 가깝습니다. 컴파일러는 레지스터 할당, 명령 스케줄링, 타깃 CPU(-march=, -mtune=)에 맞춘 대체 명령, 벡터화 실패 시 스칼라 폴백 등을 자동으로 조정합니다.
인트린식이 유리한 이유
- 타입 시스템:
__m128i,__m256i등으로 의도가 드러납니다. - 이식성 레이어: 동일 헤더·동일 호출이 여러 백엔드에서 다른 명령으로 펼쳐질 수 있습니다.
- 최적화: 상수 전파, 루프 융합, 자동 벡터화와 상호작용합니다.
아래는 개념 예시입니다(실제 프로젝트에서는 정렬·로드 패턴·도메인에 맞는 내장 함수를 선택합니다).
#include <immintrin.h>
// 128비트 정수 벡터 덧셈(의미 예시)
void add_epi32_example(__m128i a, __m128i b, __m128i* out) {
*out = _mm_add_epi32(a, b);
}
인라인 asm이 남는 경우
- 컴파일러가 아직 노출하지 않는 아주 새로운 명령을 시험할 때(단기).
- 세밀한 인코딩이나 레지스터 압력이 극단적인 커널/크립토 루틴에서, 인트린식이 생성하는 코드를 검증 후 수동으로 고치는 경우.
- 특수 시스템 레벨 명령(일부 플랫폼의 MSR/특권 명령)처럼 인트린식이 아예 없을 때.
이때도 가능하면 한 파일·한 함수에 asm을 모아 두고, 나머지 코드는 인트린식·C++로 유지하는 편이 테스트와 이식에 유리합니다.
프로덕션에서의 어셈블리 패턴
단일 구현 지점과 매크로 경계
- 비즈니스 로직 곳곳에 asm 문자열을 흩뿌리지 않습니다. 플랫폼별
cpu_rdtsc(),cpu_pause()같은 얇은 래퍼로 모읍니다. - 조건부 컴파일(
#if defined(__x86_64__))은 한 곳에 모아, 나머지 TU는 동일한 시그니처만 호출합니다. - ABI/호출 규약을 깨는 asm(스택 조작, callee-saved 레지스터 무단 사용)은 유지보수 비용이 매우 큽니다. 정말 필요하면 어셈블리 별도
.S파일을 고려합니다(MSVC x64는 인라인__asm블록조차 지원하지 않습니다).
검증 루틴
- Compiler Explorer(godbolt.org)로 동일 입력에 대해 인트린식 vs asm의 생성 코드를 비교합니다.
-S/-masm=intel등으로 목록을 덤프해 레지스터 압력과 스필을 확인합니다.- 유닛 테스트는 “결과 값”뿐 아니라, 가능하면 참조 구현(순수 C++)과의 동치성을 비교합니다.
컴파일러 내장(builtin) 우선
많은 플랫폼별 명령은 __builtin_* 형태로 래핑되어 있습니다. 인라인 asm으로 같은 일을 반복 구현하기 전에, 내장 함수 존재 여부를 확인하면 클로버 실수를 줄일 수 있습니다.
실전 예시
예시 1: CPUID
#include <iostream>
using namespace std;
void cpuid(int code, int* a, int* b, int* c, int* d) {
asm volatile("cpuid"
: "=a"(*a), "=b"(*b), "=c"(*c), "=d"(*d)
: "a"(code)
);
}
int main() {
int a, b, c, d;
cpuid(0, &a, &b, &c, &d);
char vendor[13];
*(int*)(vendor) = b;
*(int*)(vendor + 4) = d;
*(int*)(vendor + 8) = c;
vendor[12] = '\0';
cout << "CPU: " << vendor << endl;
}
예시 2: 원자적 연산
int atomicIncrement(int* ptr) {
int result;
asm volatile(
"lock; xaddl %0, %1"
: "=r" (result), "+m" (*ptr)
: "0" (1)
: "memory"
);
return result;
}
int main() {
int counter = 0;
for (int i = 0; i < 10; i++) {
atomicIncrement(&counter);
}
cout << counter << endl; // 10
}
예시 3: 타임스탬프 카운터
uint64_t rdtsc() {
uint32_t lo, hi;
asm volatile("rdtsc"
: "=a"(lo), "=d"(hi)
);
return ((uint64_t)hi << 32) | lo;
}
int main() {
uint64_t start = rdtsc();
// 측정할 코드
for (int i = 0; i < 1000000; i++) {
// ...
}
uint64_t end = rdtsc();
cout << "사이클: " << (end - start) << endl;
}
예시 4: 메모리 배리어
앞의 「메모리 배리어와 순서 보장」 절에서 구분한 것처럼, compilerBarrier()는 주로 컴파일러 재배치를 억제하는 쪽에 가깝고, mfence는 실제 명령입니다. 프로덕션 동기화는 가능하면 std::atomic으로 모델링하는 것이 안전합니다.
memoryBarrier 함수의 구현 예제입니다.
void memoryBarrier() {
asm volatile("mfence" ::: "memory");
}
void compilerBarrier() {
asm volatile("" ::: "memory");
}
// 사용
atomic<bool> ready(false);
int data = 0;
void producer() {
data = 42;
compilerBarrier(); // 재배치 방지
ready.store(true, memory_order_release);
}
volatile
main 함수의 구현 예제입니다.
int main() {
int x = 10;
// volatile: 최적화 방지
asm volatile("nop"); // 제거되지 않음
// 메모리 clobber
asm volatile("" ::: "memory"); // 메모리 재배치 방지
}
플랫폼별 차이
x86-64
// 64비트 레지스터
asm("movq %0, %%rax" : : "r"(value));
ARM
// ARM 문법
asm("mov r0, %0" : : "r"(value));
크로스 플랫폼
#ifdef __x86_64__
asm("rdtsc" : "=a"(lo), "=d"(hi));
#elif __aarch64__
asm("mrs %0, cntvct_el0" : "=r"(cycles));
#else
#error "Unsupported platform"
#endif
자주 발생하는 문제
문제 1: 레지스터 손상
C/C++ 예제 코드입니다.
// ❌ 레지스터 손상
asm("movl $10, %eax"); // eax 손상
// ✅ clobber 명시
asm("movl $10, %%eax"
:
:
: "%eax" // eax가 손상됨을 명시
);
문제 2: 최적화 간섭
// ❌ 최적화로 제거됨
asm("nop");
// ✅ volatile 사용
asm volatile("nop");
문제 3: 플랫폼 의존성
C/C++ 예제 코드입니다.
// ❌ x86 전용
asm("rdtsc" : "=a"(lo), "=d"(hi));
// ✅ 조건부 컴파일
#ifdef __x86_64__
asm("rdtsc" : "=a"(lo), "=d"(hi));
#else
// 대체 구현
#endif
인라인 어셈블리 vs Intrinsics (요약)
스칼라 연산이면 컴파일러가 생성하는 일반 연산이나 내장 함수로 충분한 경우가 많고, SIMD·비트 조작은 인트린식·내장 함수가 기본 선택입니다. 위의 「SIMD 인트린식 대 인라인 asm」 절에서 선택 기준과 예외를 정리했습니다.
// 스칼라: 인라인 asm 예시(교육용)
asm("addl %1, %0" : "=r"(result) : "r"(a), "0"(b));
// SIMD: __m128i 타입으로 의도를 고정(권장 방향)
#include <immintrin.h>
// __m128i va = ..., vb = ...;
// __m128i vr = _mm_add_epi32(va, vb);
Intrinsics·내장 함수 쪽이 유리한 이유: 타입 정보가 살아 있어 최적화기가 레지스터를 다시 배치하기 쉽고, 플랫폼 분기도 컴파일러가 한 번에 처리할 수 있습니다.
디버깅
func 함수의 구현 예제입니다.
// 어셈블리 출력
void func() {
int x = 10;
int y = x * 2;
}
// 컴파일
// g++ -S -O2 program.cpp
// program.s 파일 확인
FAQ
Q1: 인라인 어셈블리는 언제 사용하나요?
A:
- 극한 최적화
- 하드웨어 직접 접근
- 특수 명령어 (CPUID, RDTSC)
Q2: Intrinsics vs 인라인 어셈블리?
A: 가능하면 Intrinsics를 사용하세요. 더 안전하고 이식성이 좋습니다.
Q3: 성능 향상이 보장되나요?
A: 아니요. 컴파일러 최적화가 더 나을 수 있습니다.
Q4: 플랫폼 독립적으로 만들려면?
A:
- 조건부 컴파일
- Intrinsics 사용
- 어셈블리는 최후 수단
Q5: 디버깅은?
A:
- GDB의 disassemble 명령
- -S 옵션으로 어셈블리 출력
- Compiler Explorer (godbolt.org)
Q6: 인라인 어셈블리 학습 리소스는?
A:
- GCC 인라인 어셈블리 문서
- Intel/AMD 매뉴얼
- “PC Assembly Language” (Paul Carter)
Q7: &(early-clobber)는 언제 붙이나요?
A: 출력 피연산자가 입력을 모두 읽기 전에 같은 레지스터/슬롯을 덮어쓰면 안 될 때 출력 제약에 &를 붙여, 컴파일러가 입력과 출력을 동일 레지스터에 할당하지 않도록 합니다. 빠뜨리면 간헐적 오류가 납니다.
Q8: "memory" 클로버와 mfence는 같은가요?
A: 아닙니다. "memory"는 주로 컴파일러에게 메모리 접근 순서를 보수적으로 취하라고 알리는 것이고, mfence 등은 실제 CPU 명령입니다. 동기화 의미는 C++에서는 std::atomic과 메모리 순서로 표현하는 편이 명확합니다.
Q9: 프로덕션에서 asm을 흩뿌리지 말라는 이유는?
A: 클로버 누락·ABI 위반·플랫폼 분기가 코드 전체로 퍼지면 검증 비용이 폭증합니다. 단일 래퍼 함수로 모으고, Godbolt·테스트로 생성 코드를 고정하는 패턴이 안전합니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ inline 함수 | “Inline Function” 가이드
- C++ 메모리 정렬 | “Alignment와 Padding” 가이드
- C++ Expression Templates | “지연 평가” 고급 기법
관련 글
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「C++ 인라인 어셈블리 | ‘asm’ 키워드 가이드」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.
내부 동작과 핵심 메커니즘
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
sequenceDiagram participant C as 클라이언트/호출자 participant B as 경계(런타임·게이트웨이·프로세스) participant D as 의존성(API·DB·큐·파일) C->>B: 요청/이벤트 B->>D: 조회·쓰기·RPC D-->>B: 지연·부분 실패·재시도 가능 B-->>C: 응답 또는 오류(코드·상관 ID)
- 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
- 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.
프로덕션 운영 패턴
| 영역 | 운영 관점 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가 |
| 안전성 | 입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가 |
| 성능 | 캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가 |
| 용량 | 피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가 |
스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.
확장 예시: 엔드투엔드 미니 시나리오
앞선 본문 주제(「C++ 인라인 어셈블리 | ‘asm’ 키워드 가이드」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
ctx = newCorrelationId()
validated = validateSchema(request)
authorize(validated, ctx)
result = domainCore(validated)
persistOrEmit(result, idempotentKey)
recordMetrics(ctx, latency, outcome)
return result
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스, 타임아웃, 외부 의존성, DNS | 최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검 |
| 성능 저하 | N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납 | 상한·TTL·힙/FD 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, assembly, asm, 저수준, 최적화 등으로 검색하시면 이 글이 도움이 됩니다.