C++ 인라인 어셈블리 | "asm" 키워드 가이드
이 글의 핵심
인라인 어셈블리(asm)는 C++ 코드 안에 어셈블리를 끼워 넣어 특정 아키텍처 명령을 쓰는 고급 기능입니다. 이 글에서는 GCC·Clang AT&T 문법과 MSVC Intel 문법 차이, 제약과 대안을 예제로 소개합니다.
기본 문법
성능 병목이나 특수 명령이 필요할 때만 인라인 어셈블리를 고려하는 것이 일반적입니다. 이 글에서는 컴파일러별 문법 차이를 알아 두고, 이식성을 잃기 쉬운 부분을 판단하는 데 도움이 되도록 예제를 나열합니다.
GCC/Clang (AT&T 문법)
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 문법)
int main() {
int x = 10;
int y = 20;
int result;
__asm {
mov eax, x
add eax, y
mov result, eax
}
cout << result << endl; // 30
}
레지스터 제약
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: 즉시값
실전 예시
예시 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: 메모리 배리어
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
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: 레지스터 손상
// ❌ 레지스터 손상
asm("movl $10, %eax"); // eax 손상
// ✅ clobber 명시
asm("movl $10, %%eax"
:
:
: "%eax" // eax가 손상됨을 명시
);
문제 2: 최적화 간섭
// ❌ 최적화로 제거됨
asm("nop");
// ✅ volatile 사용
asm volatile("nop");
문제 3: 플랫폼 의존성
// ❌ x86 전용
asm("rdtsc" : "=a"(lo), "=d"(hi));
// ✅ 조건부 컴파일
#ifdef __x86_64__
asm("rdtsc" : "=a"(lo), "=d"(hi));
#else
// 대체 구현
#endif
인라인 어셈블리 vs Intrinsics
// 인라인 어셈블리
asm("addl %1, %0" : "=r"(result) : "r"(a), "0"(b));
// Intrinsics (권장)
#include <x86intrin.h>
result = _mm_add_epi32(a, b);
Intrinsics 장점:
- 타입 안전
- 최적화 가능
- 플랫폼 독립적 (컴파일러가 변환)
디버깅
// 어셈블리 출력
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)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ inline 함수 | “Inline Function” 가이드
- C++ 메모리 정렬 | “Alignment와 Padding” 가이드
- C++ Expression Templates | “지연 평가” 고급 기법
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |