/

핵심 요약

워치독 타이머는 소프트웨어가 정해진 시간 안에 "살아 있다"는 신호(kick, ping)를 보내지 않으면 시스템을 리셋하는 하드웨어 타이머입니다. 프로그램이 멈추거나 무한 루프에 빠져도 사람 없이 시스템을 다시 살리는 것이 목적입니다.

한 줄 답: 일정 시간 안에 kick이 없으면 하드웨어가 시스템을 리셋하는 장치입니다.

워치독 타이머란 무엇이고, 왜 필요한가요?

한 줄 답: 소프트웨어가 "살아 있다"고 알리지 않으면 시스템을 리셋하는 하드웨어 회로이며, 사람이 없는 장치가 멈춤 상태에서 스스로 복구하도록 하기 위해 필요합니다.

Linux 커널 문서는 워치독 타이머(WDT)를 소프트웨어 오류가 생겼을 때 시스템을 리셋할 수 있는 하드웨어 회로라고 설명합니다. 사용자 공간의 데몬이 일정한 간격으로 /dev/watchdog를 통해 드라이버에 알려 주고, 이 알림이 끊기면 정해진 시간 뒤에 하드웨어가 시스템을 리셋합니다.

현장에 설치되어 사람이 직접 전원을 껐다 켤 수 없는 장치에서는 프로그램이 한 번 멈추면 장치가 계속 먹통 상태로 남습니다. 워치독은 이런 멈춤이나 무한 루프를 사람 없이 복구하기 위한 마지막 안전장치입니다.

워치독은 어떻게 동작하나요?

한 줄 답: 카운트다운이 진행되다가 kick을 받으면 초기화되고, kick이 끊겨 타임아웃이 지나면 리셋됩니다.

커널 문서는 워치독이 /dev/watchdog를 여는 순간부터 동작한다고 설명합니다. 정해진 시간(문서에서는 "timeout" 또는 "margin"이라고 부릅니다) 안에 핑을 보내지 않으면 시스템이 재부팅됩니다. 가장 단순한 핑은 장치에 아무 데이터나 쓰는 것입니다.

  1. 워치독이 시작되면 타이머가 카운트다운을 합니다.
  2. 소프트웨어가 timeout 안에 kick(ping)을 보내면 카운트다운이 처음부터 다시 시작됩니다.
  3. 프로그램이 멈춰 kick이 끊기면 카운트다운이 끝까지 진행됩니다.
  4. timeout이 만료되면 하드웨어가 시스템을 리셋합니다.

kick은 어디서 해야 하나요?

한 줄 답: 주요 작업이 정상인지 확인한 뒤에만 kick해야 하며, 조건 없이 kick하면 안 됩니다.

면접에서 자주 나오는 함정은 타이머 ISR 안에서 무조건 kick하는 구성입니다. 이렇게 하면 메인 루프가 멈춰도 타이머 인터럽트는 계속 동작하므로 kick이 계속 들어가고, 워치독이 영원히 작동하지 않습니다.

해결 방법은 각 태스크가 자신이 정상 동작했다는 "alive" 플래그를 세우고, 메인 루프가 모든 플래그를 확인한 뒤에만 kick하고 플래그를 지우는 것입니다. 하나라도 플래그가 서지 않으면 kick하지 않으므로 워치독이 시스템을 리셋합니다.

  • 잘못된 방식: 타이머 ISR에서 무조건 kick → 메인 루프가 멈춰도 계속 kick됩니다.
  • 올바른 방식: 각 태스크가 alive 플래그 설정 → 메인 루프가 전부 확인 → kick 후 플래그 초기화.

Linux에서는 워치독을 어떻게 쓰나요?

한 줄 답: /dev/watchdog를 열어 시작하고, timeout 전에 write 또는 WDIOC_KEEPALIVE로 핑을 보내며, 종료할 때는 Magic Close로 닫습니다.
  • 시작과 핑: 장치를 열면 워치독이 시작됩니다. 아무 데이터나 write하는 것과 WDIOC_KEEPALIVE ioctl은 정확히 같은 동작입니다.
  • Magic Close: 장치를 닫으면 보통 워치독이 멈추지만, Magic Close를 지원하는 드라이버는 닫기 직전에 매직 문자 'V'를 쓴 경우에만 워치독을 끕니다. 그렇지 않으면 데몬이 죽었다고 보고 timeout 뒤에 시스템을 재부팅합니다.
  • NOWAYOUT: CONFIG_WATCHDOG_NOWAYOUT=Y(또는 nowayout 모듈 파라미터)이면 한 번 시작한 워치독을 끌 방법이 없습니다.
  • timeout 설정: WDIOC_SETTIMEOUT은 초 단위로 지정하며, 드라이버는 하드웨어 단위 때문에 요청과 다를 수 있는 실제 timeout 값을 돌려줍니다. 현재 값은 WDIOC_GETTIMEOUT으로 읽습니다.
  • Pretimeout: 일부 워치독은 리셋 전에 NMI/인터럽트를 발생시켜 Linux가 패닉 정보나 코어덤프를 기록하게 합니다. WDIOC_SETPRETIMEOUT은 timeout "이전"의 초 단위 값입니다. 커널 문서의 예시로는 timeout 60, pretimeout 10이면 50초 시점에 발생합니다. 모든 드라이버가 지원하지는 않습니다.
  • 리셋 원인 확인: WDIOC_GETBOOTSTATUS는 마지막 부팅 시점의 상태를 돌려주며, WDIOF_CARDRESET은 직전 재부팅이 워치독 때문이었음을 뜻합니다. 모든 장치가 지원하지는 않습니다.

커널 문서는 이 API가 ad hoc 성격이라 드라이버마다 일부만 구현한다고 밝힙니다. 즉 드라이버마다 지원 범위가 다르다는 점을 기억해야 합니다. 또한 문서 머리말은 Linux 2.4.18 시점의 상태를 설명하며 마지막 검토는 2007년이라고 적혀 있으므로, 실제 사용 전에 사용하는 커널과 드라이버를 확인해야 합니다.

아래는 문서 수준의 단순화한 예시입니다(실행하지 않았으며 출력은 없습니다).

int fd = open("/dev/watchdog", O_WRONLY);

while (1) {
    ioctl(fd, WDIOC_KEEPALIVE, 0);
    sleep(10);
}

/* 정상 종료 시: 닫기 직전에 매직 문자 'V'를 씁니다. */
write(fd, "V", 1);
close(fd);

systemd 서비스는 WatchdogSec=를 쓸 수 있으며, 이 경우 서비스가 sd_notify로 WATCHDOG=1 keep-alive를 보내야 합니다. 서비스 파일 키워드는 이전 글 「systemd Type= 정리|simple·exec·notify와 서비스 파일 핵심 키워드」를 참고해 주시기 바랍니다.

면접에서 자주 나오는 워치독 질문은 무엇인가요?

한 줄 답: 하드웨어와 소프트웨어 워치독의 차이, timeout 선택, 리셋 원인 확인, 디버깅 중 동작이 주로 나옵니다.
하드웨어 워치독과 소프트웨어 워치독은 무엇이 다른가요?

소프트웨어 워치독은 감시 대상과 같은 커널과 CPU 위에서 돌기 때문에 커널이나 CPU가 멈추면 함께 멈춥니다. 하드웨어 타이머는 독립적으로 카운트하므로 시스템이 완전히 멈춰도 리셋할 수 있습니다. 이는 일반적인 구조에 따른 설명입니다.

timeout은 어떻게 정하나요?

정해진 정답 숫자는 없습니다. 정상 상태에서 가장 오래 걸리는 루프나 부팅 경로보다 길어야 하고, 복구 요구 시간보다는 짧아야 합니다. 하드웨어가 허용하는 최대값도 확인해야 하며, WDIOC_SETTIMEOUT이 실제 적용된 값을 돌려준다는 점도 기억해야 합니다.

리셋 원인이 워치독인지 어떻게 확인하나요?

지원하는 장치라면 WDIOC_GETBOOTSTATUS로 마지막 부팅 상태를 읽고 WDIOF_CARDRESET 플래그를 확인합니다. 이 플래그가 있으면 마지막 재부팅이 워치독 때문이었다는 뜻입니다. 모든 장치가 지원하지는 않습니다.

디버깅 중에 워치독은 어떻게 되나요?

브레이크포인트에서 멈추면 kick도 멈추므로 예상하지 못한 리셋이 일어날 수 있습니다. 사용하는 시스템이 지원하는 방법으로 워치독을 끄거나 멈춰야 하지만, NOWAYOUT 설정에서는 끌 수 없습니다. gdb 사용법은 gdb 정리 글을 참고해 주시기 바랍니다.

pretimeout은 언제 유용한가요?

리셋 직전에 패닉 정보나 코어덤프를 남겨 원인을 추적할 수 있습니다. 다만 드라이버가 지원해야 사용할 수 있습니다.

함께 보면 좋은 글은 무엇인가요?

한 줄 답: 디버깅 중 워치독 동작은 gdb 글, 서비스 단위 워치독은 systemd 글이 이어집니다.
  • gdb 정리 글
  • systemd Type= 정리|simple·exec·notify와 서비스 파일 핵심 키워드

출처는 어디인가요?

한 줄 답: Linux 커널 워치독 API 문서와 systemd.service 매뉴얼 페이지를 참고했습니다.

핵심 요약

LLM 추론 프레임워크는 학습된 모델 가중치를 읽어 요청을 받아 토큰을 만들어 주는 실행 엔진입니다. 여러 요청을 묶어 GPU를 효율적으로 쓰는 서버형(vLLM 등)과, 노트북·CPU·작은 GPU에서 양자화 모델을 가볍게 돌리는 로컬형(llama.cpp 등)으로 나눠 보면 고르기 쉽습니다.

한 줄 답: 서버형(vLLM 등)과 로컬형(llama.cpp 등)으로 나누고, 사용자 수·하드웨어·모델 포맷으로 고르면 됩니다.

최근 Hacker News에서도 특정 모델군을 위한 추론 엔진이 큰 화제가 되었습니다. 이 글은 개념을 정리하는 글이라 설치나 실행 명령은 다루지 않습니다.

LLM 추론 프레임워크란 무엇이고, 모델 파일만으로는 왜 안 되나요?

한 줄 답: 가중치 파일은 데이터일 뿐이라, 로딩·토크나이즈·토큰별 생성 루프·API 서버를 맡는 실행 엔진이 필요합니다.

모델 파일에는 학습된 가중치만 들어 있습니다. 이 가중치를 메모리에 올리고, 입력 문장을 토크나이저로 토큰으로 바꾸고, 토큰을 하나씩 이어서 생성하는 반복 루프를 돌리고, 결과를 요청한 쪽에 돌려주는 일은 별도의 소프트웨어가 맡아야 합니다. 이 역할을 하는 것이 추론 프레임워크입니다. vLLM 문서는 스트리밍 출력, OpenAI 호환 API 서버, Hugging Face 모델 연동을 기능으로 소개하고, llama.cpp README는 GGUF 모델 파일 사용과 OpenAI 호환 API 서버 포함을 안내합니다.

토큰과 컨텍스트 개념이 낯설다면 토큰/컨텍스트 정리 글을 먼저 보셔도 좋습니다.

서버형과 로컬형은 무엇이 다른가요?

한 줄 답: 서버형은 동시 요청을 묶어 GPU를 효율적으로 쓰고, 로컬형은 CPU·소형 GPU에서 양자화 모델을 가볍게 돌립니다.
구분서버형 (예: vLLM)로컬형 (예: llama.cpp)
핵심 목표들어오는 여러 요청을 묶어 처리적은 자원에서 가볍게 실행
메모리·배칭PagedAttention, continuous batchingCPU+GPU 하이브리드 추론(VRAM보다 큰 모델 대응)
양자화FP8, INT8, INT4, GPTQ/AWQ, GGUF 등 지원1.5비트~8비트 정수 양자화
하드웨어tensor/pipeline parallelism으로 분산 추론의존성 없는 C/C++, Apple silicon 우선(NEON, Accelerate, Metal)
모델 포맷Hugging Face 모델 연동GGUF 파일
APIOpenAI 호환 서버, 스트리밍 출력OpenAI 호환 서버 포함

양자화가 무엇인지는 양자화 정리 글에서 다뤘습니다. KV 캐시는 여기서 다시 설명하지 않으며, 궁금하시면 AI Day3: LLM 추론·KV 캐시를 참고해 주시기 바랍니다.

어떤 기준으로 고르나요?

한 줄 답: 동시 사용자 수, 하드웨어, 모델 포맷, 멀티 GPU 필요 여부로 정하면 됩니다.
  • 사용자 수: 여러 사용자의 요청을 동시에 받아야 하면 서버형, 혼자 쓰면 로컬형이 자연스럽습니다.
  • 하드웨어: 서버용 GPU를 쓸 수 있으면 서버형, 노트북·CPU·작은 GPU라면 로컬형이 맞습니다.
  • 모델 포맷: safetensors/Hugging Face 형식이면 서버형, GGUF 파일이면 로컬형과 잘 맞습니다.
  • OpenAI 호환 API: 둘 다 제공하므로 결정 요인이 아닙니다.
  • 멀티 GPU: 여러 GPU로 나눠 돌려야 한다면 tensor/pipeline parallelism을 지원하는 서버형을 보시면 됩니다.

요즘 모델사가 자체 추론 엔진을 내는 이유는 무엇인가요?

한 줄 답: 특정 모델 구조에 맞춘 최적화를 넣기 위해서라고 볼 수 있습니다. 다만 일반적인 추론일 뿐 공식 설명은 아닙니다.

2026-10-20 Hacker News에 "DeepInfer: High-performance inference framework for DeepSeek models"라는 글이 올라와 특정 모델군을 위한 추론 엔진이 화제가 되었습니다(HN 스레드). 한 모델 구조에 맞춰 최적화하면 범용 엔진보다 유리한 부분을 넣을 수 있기 때문이라고 생각해 볼 수 있습니다. 다만 이는 일반적인 추론일 뿐, 위 스레드의 내용이나 수치를 근거로 한 설명은 아닙니다.

자주 묻는 질문은 무엇인가요?

로컬형으로 여러 사람이 써도 되나요?

가능은 하지만, 동시 요청을 묶어 처리하는 배칭이 핵심인 쪽은 서버형입니다. 여러 사람이 함께 써야 한다면 서버형이 더 맞는 선택입니다.

양자화하면 품질이 떨어지나요?

떨어질 수 있으며, 모델과 양자화 방식에 따라 다릅니다. 자세한 내용은 양자화 정리 글을 참고해 주시기 바랍니다.

OpenAI 호환 API가 왜 중요한가요?

기존 클라이언트 코드를 거의 그대로 재사용할 수 있기 때문입니다. vLLM과 llama.cpp 모두 OpenAI 호환 API 서버를 제공합니다.

출처는 어디인가요?

한 줄 답: vLLM 문서, llama.cpp GitHub, HN 스레드이며 모두 2026-10-21에 확인했습니다.

함께 보면 좋은 글

외부 출처

핵심 요약

expected unqualified-id before numeric constant 에러는 #define MAX_SIZE 100 같은 매크로가 변수·멤버·함수 이름 자리에 그대로 펼쳐져서, 이름이 들어가야 할 곳에 숫자(100)가 남을 때 납니다. 매크로와 다른 이름을 쓰거나, 매크로를 constexpr 상수로 바꾸거나, 꼭 필요하면 충돌 지점 앞에서 #undef 합니다.

멀쩡해 보이는 멤버 변수 선언에서 expected unqualified-id before numeric constant 에러가 나면 당황스럽기 쉽습니다. 이 글은 2026-10-21(KST)에 g++ (Debian 14.2.0-19) 14.2.0으로 직접 재현한 코드와 실제 컴파일러 출력을 바탕으로 원인과 해결 방법을 정리합니다.

한 줄 답: 매크로(#define MAX_SIZE 100)가 이름 자리에 펼쳐져 숫자가 남을 때 발생합니다. 이름 변경, constexpr, #undef 중 하나로 해결합니다.

expected unqualified-id before numeric constant 에러란 무엇인가요?

한 줄 답: 이름(식별자)이 와야 할 곳에 숫자 상수가 왔다는 뜻입니다. unqualified-id는 한정자(Foo:: 같은 접두)가 붙지 않은 이름을 말합니다.

아래는 bad.cpp입니다. 멤버 이름이 매크로 이름과 같습니다.

#define MAX_SIZE 100

struct Config {
    int MAX_SIZE = 10;   // 매크로 이름과 같은 멤버 이름
};

int main() {
    Config c;
    return c.MAX_SIZE;
}

g++ -std=c++17 -c bad.cpp의 실제 출력입니다(종료 코드 1).

$ g++ -std=c++17 -c bad.cpp
bad.cpp:1:18: error: expected unqualified-id before numeric constant
    1 | #define MAX_SIZE 100
      |                  ^~~
bad.cpp:4:9: note: in expansion of macro ‘MAX_SIZE’
    4 |     int MAX_SIZE = 10;   // 매크로 이름과 같은 멤버 이름
      |         ^~~~~~~~
bad.cpp: In function ‘int main()’:
bad.cpp:1:18: error: expected unqualified-id before numeric constant
    1 | #define MAX_SIZE 100
      |                  ^~~
bad.cpp:9:14: note: in expansion of macro ‘MAX_SIZE’
    9 |     return c.MAX_SIZE;
      |              ^~~~~~~~
bad.cpp:9:14: error: expected ‘;’ before numeric constant
    9 |     return c.MAX_SIZE;
      |              ^
      |              ;

왜 매크로 이름 충돌이 원인이 되나요?

한 줄 답: 전처리기가 컴파일 전에 매크로 이름을 글자 그대로 치환하기 때문입니다.

컴파일러는 MAX_SIZE라는 이름을 보지 못하고, 전처리가 끝난 결과만 봅니다. g++ -E -P bad.cpp로 전처리 결과를 확인하면 다음과 같습니다.

$ g++ -E -P bad.cpp
struct Config {
    int 100 = 10;
};
int main() {
    Config c;
    return c. 100;
}
  • 치환 전: int MAX_SIZE = 10; → 치환 후: int 100 = 10;
  • 치환 전: return c.MAX_SIZE; → 치환 후: return c. 100;
  • 이름이 와야 할 자리에 숫자 100이 있으니 "expected unqualified-id before numeric constant"가 됩니다.

출력은 이렇게 읽습니다.

  • error는 bad.cpp:1:18, 즉 #define MAX_SIZE 100의 100을 가리킵니다. 치환된 숫자가 원래 있던 위치입니다.
  • 바로 아래 note: in expansion of macro ‘MAX_SIZE’가 실제로 매크로를 쓴 자리(4:9, 9:14)를 알려줍니다. 고쳐야 할 곳은 이 note 쪽입니다.
  • 세 번째 expected ‘;’ before numeric constant는 c. 100에서 이어서 나온 후속 에러로 볼 수 있습니다.

어떻게 고치나요?

한 줄 답: 매크로와 다른 이름을 쓰거나, constexpr로 대체하거나, 충돌 지점 앞에서 #undef 합니다.

  1. 이름 변경: 멤버 이름을 max_size처럼 매크로와 다르게 바꿉니다.
  2. constexpr로 대체: 매크로 대신 constexpr int kMaxSize = 100;을 씁니다. 상수는 일반 이름처럼 스코프 규칙을 따르므로 이런 치환이 일어나지 않습니다.
  3. #undef: 충돌하는 선언 앞에서 #undef MAX_SIZE를 씁니다. 이후에 포함하는 헤더가 그 매크로를 필요로 할 수 있으니 순서에 주의해야 합니다.
  4. 이름 규칙: 매크로 이름은 ALL_CAPS로 쓰고, 일반 변수·멤버 이름에는 대문자만으로 된 이름을 쓰지 않습니다.

①과 ②를 적용한 good.cpp입니다.

#define MAX_SIZE 100

struct Config {
    int max_size = 10;   // 매크로와 다른 이름 사용
};

constexpr int kMaxSize = 100;  // 매크로 대신 constexpr

int main() {
    Config c;
    return c.max_size + kMaxSize - MAX_SIZE;
}
$ g++ -std=c++17 good.cpp -o good && ./good; echo $?
10

③을 적용한 undef.cpp입니다.

#define MAX_SIZE 100

#undef MAX_SIZE
struct Config {
    int MAX_SIZE = 10;   // 매크로 이름과 같은 멤버 이름
};

int main() {
    Config c;
    return c.MAX_SIZE;
}
$ g++ -std=c++17 -c undef.cpp
$ echo $?
0

good.cpp는 컴파일되고 종료 코드 10으로 실행되었으며, undef.cpp는 컴파일에 성공했습니다(종료 코드 0).

키워드 explicit처럼 C++의 다른 함정이 궁금하시면 C++ explicit 글도 참고해 주시기 바랍니다.

헤더에서 온 매크로라면 어떻게 찾나요?

한 줄 답: note에 적힌 파일:줄 번호를 보고, g++ -E 출력에서 해당 이름을 검색합니다.

  • 매크로를 직접 만들지 않았다면 note: in expansion of macro가 알려주는 위치를 먼저 확인합니다.
  • g++ -E 출력을 파일로 저장해 의심되는 이름을 검색하면 치환된 결과를 바로 볼 수 있습니다. 변환된 코드를 눈으로 보는 방법으로는 C++ Insights 글도 있습니다.
  • 대표적인 예가 Windows 헤더의 min/max입니다. Microsoft Learn의 "max macro (Minwindef.h)" 문서에는 #define max(a, b) (((a) > (b)) ? (a) : (b))로 정의되어 있습니다(문서 링크). 함수처럼 생긴 매크로라 std::max 같은 이름도 영향을 받을 수 있습니다.
  • <windows.h>를 포함하기 전에 #define NOMINMAX를 두어 이 매크로가 정의되지 않게 하는 방법이 널리 쓰입니다.
  • 다른 방법으로, (std::max)(a, b)처럼 이름을 괄호로 감싸 호출할 수도 있습니다.
  • 디버깅 자체가 필요하다면 gdb 글을 참고해 주시기 바랍니다.

자주 묻는 질문은 무엇인가요?

한 줄 답: 아래 세 가지가 가장 흔한 질문입니다.

Q1. 매크로 없이도 같은 메시지가 나나요?

나올 수 있습니다. 예를 들어 함수 밖에 return 1; 같은 구문을 쓰면 같은 g++ 14.2.0에서 error: expected unqualified-id before ‘return’이 나옵니다. 뒤에 붙는 말이 numeric constant가 아니라는 점이 다릅니다. 이때는 매크로가 아니라 문장의 위치를 확인해야 합니다.

Q2. enum이나 상수 이름에도 생기나요?

생깁니다. 이름이 올 수 있는 모든 식별자 자리(변수, 멤버, 함수, enum 값 등)에서 같은 방식으로 치환되기 때문입니다.

Q3. 매크로를 꼭 써야 할 때는 어떻게 하나요?

MYLIB_MAX_SIZE처럼 프로젝트 접두어를 붙여 다른 이름과 겹칠 가능성을 줄입니다.

함께 보면 좋은 글은 무엇인가요?

한 줄 답: C++ explicit, C++ Insights, gdb 글입니다.

출처는 어디인가요?

한 줄 답: 직접 실행한 GCC 결과와 Microsoft Learn 문서입니다.

핵심 요약

NPU GPU 차이를 한마디로 하면 설계 초점의 차이입니다. GPU는 수많은 코어가 같은 연산을 병렬로 돌리는 범용 병렬 프로세서이고, NPU는 신경망의 곱셈-누산(MAC) 연산에 맞춰 만든 전용 가속기입니다. 이 글에서는 구조, 학습과 추론, 전력과 용도를 초보자와 임베디드 개발자 눈높이로 비교합니다.

한 줄 답: GPU는 유연한 범용 병렬 프로세서이고 NPU는 MAC 배열 중심의 신경망 전용 가속기입니다. 차이는 "GPU는 행렬곱을 못 한다"가 아니라, 유연성 대 고정된 데이터 흐름·온칩 메모리·전력이라는 설계 초점입니다.

NPU와 GPU는 각각 무엇인가요?

한 줄 답: NPU GPU 차이의 출발점은 GPU가 원래 그래픽용으로 만들어진 범용 병렬 프로세서이고, NPU는 행렬곱·합성곱의 곱셈-누산(MAC)에 맞춘 신경망 전용 가속기라는 점입니다.

GPU는 원래 그래픽 처리를 위해 만들어졌습니다. 수많은 코어가 같은 연산을 동시에 병렬로 돌리는 구조이며, CUDA 같은 프로그래밍 모델 덕분에 그래픽 외에도 다양한 연산을 올릴 수 있습니다.

NPU는 신경망 연산에 맞춰 만든 전용 가속기입니다. 신경망의 대부분을 차지하는 행렬곱과 합성곱은 결국 곱하고 더해 누적하는 MAC 연산의 반복이고, NPU는 이 연산을 효율적으로 처리하도록 설계됩니다.

한 칩(SoC) 안에 CPU, GPU, NPU가 함께 들어 있는 경우도 흔합니다. 그래서 "무엇이 더 좋은가"보다 "어떤 일을 어디에 맡기는가"로 보면 이해하기 쉽습니다.

구조는 어떻게 다르고, 행렬곱·MAC 배열은 무엇인가요?

한 줄 답: MAC은 곱하고 더해 누적하는 연산(acc += a×b)이고 행렬곱은 MAC의 반복입니다. NPU는 MAC을 배열로 깔고 온칩 메모리와 묶는 반면, GPU도 이제 행렬 유닛(Tensor Core)을 갖고 있어 차이는 정도와 설계 초점입니다.

MAC(Multiply-Accumulate)은 두 값을 곱해서 누적값에 더하는 연산입니다. 행렬곱은 이 연산을 아주 많이 반복한 것입니다.

// 개념 예시(실행 안 함)
acc += a * b;

TPU v1: Google TPU v1 논문(Jouppi et al., ISCA 2017)은 "the heart of the TPU is a 65,536 8-bit MAC matrix multiply unit"이라고 설명하며, 최대 성능은 92 TOPS, 소프트웨어가 관리하는 온칩 메모리는 28 MiB라고 합니다. 데이터센터에는 2015년부터 배치되었고 추론 단계를 가속하는 장치입니다. 또 CPU와 GPU의 시간에 따라 달라지는 최적화(캐시, 비순차 실행, 멀티스레딩 등)와 달리 결정적(deterministic) 실행 모델을 쓴다고 설명합니다. TPU 자체는 TPU 글에서 따로 다룹니다.

NVIDIA Tensor Core: NVIDIA 기술 블로그(Programming Tensor Cores in CUDA 9)에 따르면 V100의 Tensor Core 하나는 4x4x4 행렬 처리 배열로 D = A * B + C를 수행합니다. A, B는 FP16이고 C, D는 FP16 또는 FP32입니다. 워프(warp)는 이를 묶어 WMMA API로 16x16x16 행렬 곱셈-누산(MMA)에 사용합니다. 또 Turing 튜닝 가이드(NVIDIA Turing Tuning Guide)에 따르면 Turing Tensor Core는 8비트, 4비트, 1비트 입력의 정수 행렬곱을 추가했고 누산은 32비트 정수로 합니다.

Arm Ethos-U55: Arm 기술 참조 설명서(Arm Ethos-U55 NPU TRM 102420)에 따르면 Ethos-U55 microNPU는 클록당 32~256개의 MAC을 구성할 수 있고, 각 MAC은 8비트 x 8비트입니다. MAC 유닛이 합성곱, 벡터 곱, 풀링의 곱셈-누산을 처리하며, Cortex-M과 함께 쓰도록 설계되었습니다.

정리하면 GPU도 Tensor Core라는 행렬 유닛을 갖고 있으므로 "GPU는 행렬 연산을 못 한다"는 말은 맞지 않습니다. 차이는 범용 프로그래밍 가능성과 행렬 유닛을 함께 가진 GPU, 그리고 MAC 배열과 온칩 메모리를 중심으로 데이터 흐름이 대부분 고정된 NPU라는 설계 초점에 있습니다.

학습과 추론에서는 어떻게 쓰임이 나뉘나요?

한 줄 답: 학습(순전파+역전파+가중치 갱신을 여러 번 반복)은 높은 정밀도와 유연성이 필요해 GPU를 흔히 쓰고, 추론(가중치를 고정한 순전파)은 많은 NPU가 목표로 하는 영역입니다.

학습은 순전파, 역전파, 가중치 갱신을 아주 많이 반복합니다. FP16, BF16, FP32 같은 부동소수점 정밀도가 주로 쓰이고 연산 종류도 다양해서, 일반적으로 GPU가 많이 쓰입니다.

추론은 가중치를 고정한 채 순전파만 수행하므로 INT8 같은 낮은 정밀도를 쓸 수 있습니다. 그래서 스마트폰과 엣지 SoC의 많은 NPU가 추론을 목표로 합니다. 앞서 본 Ethos-U55는 8비트 MAC을 쓰고(Arm TRM), Rockchip RV1126B NPU는 "3 TOPS @ INT8"로 소개됩니다(Forlinx 제품 페이지). 자세한 내용은 아래 전력·용도 항목에서 다룹니다. TPU v1 역시 추론 가속기였습니다(TPU v1 논문).

이후의 데이터센터 가속기 중에는 학습까지 맡는 것도 있습니다. 세부 사양은 여기서 다루지 않으니 TPU 글을 참고하십시오. 양자화는 깊이 설명하지 않으며, LLM 추론 쪽은 AI 스터디 Day3 LLM 추론·KV 캐시 글을 참고하십시오.

전력과 용도로 보면 무엇을 고르면 되나요?

한 줄 답: 저전력·상시 동작·배터리·엣지에서 고정된 모델을 돌리면 NPU, 유연성·학습·큰 모델·새 연산이 필요하면 GPU(전력과 발열은 더 큼)를 고르면 됩니다.
구분NPUGPU
목적신경망 연산 전용 가속범용 병렬 처리(그래픽, 다양한 연산)
연산 단위MAC 배열 + 온칩 메모리다수의 코어 + 행렬 유닛(Tensor Core)
정밀도(주로)INT8 등 낮은 정밀도 중심FP16/BF16/FP32 등 폭넓게 지원
프로그래밍 유연성지원 연산이 제한적, 컴파일러 의존높음(CUDA 같은 프로그래밍 모델)
전력·발열(정성)낮은 편높은 편, 냉각 필요
대표 용도엣지·모바일 추론, 상시 동작학습, 대형 모델, 새 연산 실험

전력 효율 수치로는 TPU 논문(Jouppi et al., 2017)의 주장이 자주 인용됩니다. 평균적으로 당시 GPU(Nvidia K80)나 CPU(Haswell)보다 약 15X~30X 빠르고, TOPS/Watt는 약 30X~80X 높았다고 합니다. 다만 이것은 2017년 논문에서 2015년 무렵의 특정 칩과 Google 자체 워크로드를 비교한 결과이며, 일반적인 법칙이 아닙니다.

엣지 예시로 Rockchip RV1126B NPU는 Forlinx FET1126Bx-S 제품 페이지(Forlinx FET1126Bx-S)에 "3 TOPS @ INT8"로 나와 있습니다. 이 3 TOPS 수치에 스파시티 조건이 붙는다는 점은 임베디드 비전용 3TOPS급 NPU SoM 글에서 정리했습니다. 또 다른 엣지 NPU 보드는 MediaTek 10TOPS급 AIoT 보드 글에서 볼 수 있습니다.

TOPS를 볼 때는 주의가 필요합니다. 같은 TOPS라도 정밀도(INT8인지 FP16인지), 스파시티 조건, 메모리 대역폭에 따라 달라지며, 최대(peak) 수치가 실제 처리량과 같지는 않습니다. 메모리 대역폭은 HBM과 GPU 메모리 대역폭 글과 HBM5·TSMC CoWoS 검증 글에서 이어서 볼 수 있습니다.

자주 묻는 질문은 무엇인가요?

한 줄 답: NPU와 GPU는 역할이 다르며, 엣지 NPU는 보통 추론용이고, TOPS만으로 속도를 판단할 수는 없습니다.
NPU가 있으면 GPU가 필요 없나요?

아니요, 역할이 다릅니다. NPU가 지원하지 않는 연산은 CPU나 GPU로 넘어가는 경우가 있습니다. Ethos-U55 TRM(Arm TRM)에 따르면 컴파일러가 그래프의 어느 부분을 NPU에서 실행할지 정하고, 나머지는 Cortex-M에서 실행됩니다.

NPU로 학습할 수 있나요?

엣지 NPU는 보통 추론용입니다. 데이터센터 가속기 중에는 학습용도 있으며, 자세한 내용은 TPU 글을 참고하십시오.

TOPS가 높으면 항상 더 빠른가요?

아니요. 정밀도, 스파시티 조건, 메모리 대역폭, 지원 연산에 따라 달라집니다. 메모리 대역폭은 HBM과 GPU 메모리 대역폭 글을 참고하십시오.

함께 보면 좋은 글은 무엇인가요?

한 줄 답: 엣지 NPU 보드, 메모리 대역폭, TPU 글을 이어서 보면 좋습니다.

출처는 어디인가요?

한 줄 답: TPU 논문, NVIDIA 문서, Arm 기술 참조 설명서, Forlinx 제품 페이지이며, 2026-10-21에 확인했습니다.

외부 출처 (확인일 2026-10-21)

핵심 요약

Grok 4.8에 대해 공식적으로 확인되는 것은 일론 머스크가 X에서 2026년 9월 14일에 남긴 게시글이 중심입니다. "2.5T 모델이며, 새로운 C++ 소프트웨어 스택으로 학습했고, 이번 주에 학습을 마치고 RL을 시작한다"는 내용입니다. 모델 카드, 벤치마크, 가격, 출시일은 공개되지 않았습니다. 2026년 10월 19일(KST) 확인 기준으로 xAI 공식 뉴스와 API 모델 목록에는 Grok 4.8이 없으며, 최신 주력 모델은 Grok 4.7입니다. 이 글은 2.5T 파라미터의 메모리 규모(감 잡기), RL 단계의 일반적 의미, C++ 스택의 일반적 이유를 정리합니다.

Grok 4.8에 대해 공식적으로 알려진 것은 무엇인가요?

한 줄 답: 일론 머스크가 X에 남긴 두 개의 게시글이 전부이며, 모델 카드·파라미터 구성·벤치마크·가격·출시일은 공개되지 않았습니다.

  • 게시글 1 (X 게시글, 2026-09-14 10:24 KST / 2026-09-14 01:24 UTC): @techdevnotes가 "Apart from Grok 4.7, what are we even expecting from SpaceXAI in september"라고 묻자 일론 머스크가 답글로 남겼습니다.
    • 원문: "Grok 4.8, which is a 2.5T model trained with our new C++ software stack, will finish training this week and start RL"
  • 게시글 2 (X 게시글, 2026-09-14 20:19 KST): "Grok 4.8 will be a noticeable improvement."
  • 알려지지 않은 것: 공식 모델 카드, 밀집(dense)인지 MoE인지에 대한 구성, 벤치마크, 가격, 출시일은 없습니다. 그 주에 학습이 실제로 끝났는지도 공식적으로 확인되지 않았습니다.

2.5T 파라미터는 메모리로 얼마나 큰가요?

한 줄 답: 감 잡기(대략 추정)로, 가중치만 담아도 BF16 기준 약 5TB이며 80GB급 GPU가 최소 약 63장 필요합니다. 학습 상태까지 담으면 약 40TB입니다.

감 잡기(대략 추정)입니다. 가중치만 계산하고, 10진수 GB 단위, 80GB급 GPU를 가정합니다. xAI의 실제 구성이 아닙니다.

  • 2.5T = 2.5 × 10^12 개 파라미터
  • BF16/FP16 (파라미터당 2바이트): 2.5 × 10^12 × 2 B = 5.0 TB = 5,000 GB → 5,000 ÷ 80 = 62.5 → 가중치 보관만으로 최소 약 63장
  • FP8 (1바이트): 2.5 TB = 2,500 GB → 2,500 ÷ 80 = 31.25 → 약 32장
  • INT4 (0.5바이트): 1.25 TB = 1,250 GB → 1,250 ÷ 80 = 15.6 → 약 16장
  • KV 캐시, 활성값, 프레임워크 오버헤드, 병렬화에 따른 중복은 제외한 값이므로 실제 배포에는 더 많이 필요합니다.
  • 학습은 훨씬 무겁습니다. 혼합 정밀도 Adam은 파라미터당 대략 16바이트를 쓴다고 알려져 있습니다(fp16 가중치 2 + fp16 그래디언트 2 + fp32 마스터 가중치 4 + Adam m 4 + v 4, ZeRO 논문, Rajbhandari et al., 2019). 2.5 × 10^12 × 16 B = 40 TB = 40,000 GB → 40,000 ÷ 80 = 500장으로, 활성값을 빼고 모델 상태만 담아도 이 정도입니다. 이 역시 감 잡기이며 xAI의 실제 학습 구성은 공개되지 않았습니다.
  • MoE 구조인지는 알려지지 않았습니다. 만약 MoE라면 토큰당 연산량은 줄어도 모든 전문가(expert) 가중치는 여전히 메모리에 올려야 합니다.
  • 정밀도를 낮춰 메모리를 줄이는 원리는 양자화 정리 글을 참고해 주시기 바랍니다.

"학습을 끝내고 RL을 시작한다"는 무슨 뜻인가요?

한 줄 답: 일반적으로 사전학습 단계가 끝나고, 보상 신호로 모델을 다듬는 후속 학습(post-training)에 들어간다는 뜻입니다. 아래 설명은 일반 지식이며 xAI의 발표가 아닙니다.

  • 사전학습(pretraining): 방대한 텍스트에서 다음 토큰을 맞추는 자기지도 학습입니다.
  • SFT: 지시와 응답 예시로 미세 조정하는 단계입니다.
  • RL(강화학습): 선호 모델의 보상, 또는 테스트 통과·수학 정답 같은 검증 가능한 보상으로 모델을 개선합니다. RLHF는 PPO 계열 업데이트를 쓰고, DPO는 선호 데이터만 쓰는 대안입니다.
  • 따라서 "학습을 끝내고 RL을 시작한다"는 사전학습 단계가 끝나고 후속 학습이 시작된다는 의미로 읽을 수 있습니다. 일반적으로 출시는 RL, 평가, 안전성 테스트 이후에 이뤄지지만, 이것도 일반론이며 xAI의 발표가 아닙니다.
  • 사전학습·SFT·RLHF의 구분은 관련 정리 글에서 확인하실 수 있습니다.

학습 스택을 C++로 쓰는 이유는 무엇일까요?

주의: 아래는 C++ 학습 스택에 대한 일반적인 추론이며, xAI가 밝힌 이유가 아닙니다. xAI는 이유를 설명한 적이 없습니다.

  • 핫 패스의 오버헤드: Python 인터프리터와 GIL 오버헤드를 연산 중심 경로에서 피할 수 있습니다.
  • 정밀한 메모리 제어: 할당자, 고정(pinned) 메모리, 단편화 회피를 직접 다룰 수 있어 2.5T 규모에서 유리할 수 있습니다.
  • CUDA·커널 직접 연동: 융합 커널(fused kernel)을 통합하기 쉽습니다.
  • 결정적 자원 관리: RAII로 자원 수명을 명확히 관리할 수 있습니다.
  • 통신·서빙과의 연동: NCCL 같은 통신 라이브러리, 추론 서버와의 연동이 수월합니다.
  • 참고로 PyTorch 같은 기존 프레임워크도 핵심은 C++(ATen/libtorch)이고 Python은 프런트엔드입니다.

10월 19일 기준 Grok 4.8은 출시됐나요?

한 줄 답: 아니요. 2026-10-19 01:00 KST경 확인한 공식 페이지에는 Grok 4.8 발표가 없습니다.

  • 확인일: 2026-10-19 약 01:00 KST
  • x.ai/news: 가장 최근 항목은 "Introducing Grok Voice 3.0"(2026-10-14)이며 Grok 4.8 발표는 없습니다.
  • docs.x.ai 모델 목록: 가장 새로운 텍스트 모델은 grok-4.7이며 grok-4.8은 없습니다. 페이지에는 "use Grok 4.7. It is the most capable model we've built."라고 적혀 있습니다.
  • Grok 4.7은 2026-09-21에 출시된 최신 주력 모델입니다. 독립 매체 versusapp.com(2026-10-16)도 같은 날짜를 전합니다.
  • 출시 시점: 공개된 출시일은 없습니다. 시점을 점치는 것은 추측에 불과하므로 이 글에서는 다루지 않겠습니다.

자주 묻는 질문은 무엇인가요?

Q1. Grok 4.8은 이미 출시됐나요?

2026-10-19 약 01:00 KST 확인 기준으로 출시되지 않았습니다. x.ai/news와 docs.x.ai 모델 목록 모두에 Grok 4.8이 없고, 최신 주력 모델은 Grok 4.7입니다. 출시일은 발표되지 않았습니다.

Q2. Grok 4.8은 밀집 모델인가요, MoE인가요?

알려지지 않았습니다. 머스크의 게시글은 "2.5T model"이라고만 밝혔고 파라미터 구성은 공개되지 않았습니다.

Q3. xAI가 C++ 스택을 쓰는 이유를 밝혔나요?

밝히지 않았습니다. 게시글에는 "new C++ software stack"이라는 표현만 있으며, 이 글의 C++ 이유는 모두 일반적인 추론입니다.

출처는 어디인가요?

핵심 요약

valgrind(기본 도구 Memcheck)는 이미 빌드된 바이너리를 다시 컴파일하지 않고 그대로 실행하면서 메모리 누수(definitely / indirectly / possibly lost), 초기화되지 않은 값 사용(uninitialised value), 잘못된 메모리 읽기·쓰기(invalid read/write), 잘못된 해제(bad free)를 잡아내는 도구입니다(정확한 줄 번호 확인을 위해 -g -O0 빌드를 권장합니다). 다만 공식 Quick Start 가이드에 나와 있듯 실행 속도가 평소보다 약 20 to 30 times(20~30배) 느려집니다. 따라서 재컴파일이 가능한 개발·테스트 환경에서는 Clang AddressSanitizer 문서 기준 속도 저하가 약 2x(2배) 수준인 AddressSanitizer(ASan, -fsanitize=address)를 먼저 쓰고, 재컴파일이 어렵거나 초기화되지 않은 값의 생성 위치 추적(--track-origins=yes)과 전체 누수 요약이 한 번에 필요할 때 valgrind를 선택합니다.

valgrind란? memcheck는 무엇을 잡나요?

한 줄 답: valgrind의 기본 도구인 Memcheck는 기존 바이너리를 재컴파일 없이 실행하면서 잘못된 메모리 읽기·쓰기, 초기화되지 않은 값 사용, 메모리 누수, 잘못된 해제를 탐지합니다.

  • 기본 도구 Memcheck와 빌드 옵션: valgrind를 별도 도구 지정 없이 실행하면 기본 메모리 오류 탐지기인 Memcheck가 동작합니다. Valgrind Quick Start 가이드에 따르면 프로그램을 다시 컴파일하지 않아도 기존 바이너리를 바로 실행할 수 있습니다. 다만 리포트에서 정확한 소스 파일과 줄 번호를 확인하려면 -g 플래그를 켜고, 최적화로 인해 줄 번호가 흐려지지 않도록 -O0으로 빌드하는 것이 좋습니다.
  • 주요 탐지 항목:
    • 잘못된 메모리 읽기·쓰기 (Invalid reads/writes): 할당된 힙 블록 범위를 벗어난 접근 등을 찾아냅니다.
    • 초기화되지 않은 값 사용 (Use of uninitialised values): 초기화하지 않은 변수를 조건문 분기 등에 사용할 때 경고를 출력합니다. 여기에 --track-origins=yes 옵션을 추가하면 Memcheck 매뉴얼 설명대로 실행 속도는 더 느려지지만("makes Memcheck run slower"), 초기화되지 않은 값이 어디서 만들어졌는지(origin)까지 보여줍니다.
    • 메모리 누수 (Memory leaks): 프로그램 종료 시점까지 해제되지 않은 힙 블록을 손실 유형별로 분류해 보고합니다.
    • 잘못된 해제 (Bad frees): 잘못된 포인터 해제나 중복 해제 등을 감지합니다.
  • 크래시 지점을 대화형으로 멈춰 변수와 콜스택을 직접 확인하려면 gdb 디버깅 정리 글을 참고하시기 바랍니다.

valgrind로 메모리 누수는 어떤 순서로 찾나요?

한 줄 답: -g -O0으로 바이너리를 빌드한 뒤 valgrind --leak-check=full --track-origins=yes ./바이너리를 실행하고, 출력된 런타임 오류 위치와 하단 LEAK SUMMARY의 4가지 누수 분류를 순서대로 확인합니다.

Step 1: -g -O0 빌드 및 valgrind 실행 명령

실제 실행 결과 (Debian, gcc 14.2.0, valgrind-3.24.0)

$ gcc -g -O0 -o leak leak.c
$ valgrind --leak-check=full --track-origins=yes ./leak

Step 2: 테스트 코드 (leak.c)

직접 누수(definitely lost), 간접 누수(indirectly lost), 초기화되지 않은 지역 변수 분기, 힙 범위 밖 쓰기(invalid write)를 한 번에 확인하기 위한 leak.c 코드입니다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct node { char *name; };

int main(void) {
    char *a = malloc(16);              /* never freed -> definitely lost */
    strcpy(a, "hello");

    struct node *n = malloc(sizeof *n); /* pointer dropped -> definitely lost */
    n->name = malloc(32);               /* only reachable via n -> indirectly lost */

    int x;                              /* uninitialised */
    if (x > 0) puts("positive");

    int *arr = malloc(4 * sizeof *arr);
    arr[4] = 1;                         /* invalid write, 1 past end */
    free(arr);

    a = NULL; n = NULL;
    return 0;
}

Step 3: Memcheck 출력 확인

실제 실행 결과 (Debian, gcc 14.2.0, valgrind-3.24.0; 프로세스 번호는 PID로 표기)

==PID== Memcheck, a memory error detector
==PID== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==PID== Using Valgrind-3.24.0 and LibVEX; rerun with -h for copyright info
==PID== Command: ./leak
==PID== 
==PID== Conditional jump or move depends on uninitialised value(s)
==PID==    at 0x1091A5: main (leak.c:15)
==PID==  Uninitialised value was created by a stack allocation
==PID==    at 0x109159: main (leak.c:7)
==PID== 
==PID== Invalid write of size 4
==PID==    at 0x1091CC: main (leak.c:18)
==PID==  Address 0x4a5e150 is 0 bytes after a block of size 16 alloc'd
==PID==    at 0x4844818: malloc (vg_replace_malloc.c:446)
==PID==    by 0x1091BF: main (leak.c:17)
==PID== 
==PID== 
==PID== HEAP SUMMARY:
==PID==     in use at exit: 56 bytes in 3 blocks
==PID==   total heap usage: 4 allocs, 1 frees, 72 bytes allocated
==PID== 
==PID== 16 bytes in 1 blocks are definitely lost in loss record 1 of 3
==PID==    at 0x4844818: malloc (vg_replace_malloc.c:446)
==PID==    by 0x10916A: main (leak.c:8)
==PID== 
==PID== 40 (8 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3
==PID==    at 0x4844818: malloc (vg_replace_malloc.c:446)
==PID==    by 0x109188: main (leak.c:11)
==PID== 
==PID== LEAK SUMMARY:
==PID==    definitely lost: 24 bytes in 2 blocks
==PID==    indirectly lost: 32 bytes in 1 blocks
==PID==      possibly lost: 0 bytes in 0 blocks
==PID==    still reachable: 0 bytes in 0 blocks
==PID==         suppressed: 0 bytes in 0 blocks
==PID== 
==PID== For lists of detected and suppressed errors, rerun with: -s
==PID== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0)

Step 4: 출력 읽는 법과 누수 4가지 분류

  • 실행 중 메모리 오류 2건:
    • leak.c:15: 초기화되지 않은 변수 x를 조건문에서 사용해 Conditional jump or move depends on uninitialised value(s)가 기록되었습니다. --track-origins=yes 옵션 덕분에 해당 값이 main (leak.c:7)의 스택 할당에서 생성되었음을 함께 알려줍니다.
    • leak.c:18: leak.c:17에서 할당한 16바이트 블록 바로 뒤(0 bytes after a block of size 16 alloc'd)에 4바이트를 쓰는 Invalid write of size 4가 잡혔습니다.
  • 누수 기록 해석:
    • 16 bytes in 1 blocks are definitely lost: leak.c:8에서 할당한 포인터 a가 해제 없이 소실된 기록입니다.
    • 40 (8 direct, 32 indirect) bytes in 1 blocks are definitely lost: leak.c:11에서 할당한 struct node 구조체 8바이트가 직접 손실(direct)되었고, 그 내부 멤버 n->name이 가리키던 32바이트 버퍼가 간접 손실(indirect)되었음을 뜻합니다.
  • LEAK SUMMARY 4가지 구분 (Memcheck 매뉴얼 기준):
    • definitely lost: 해당 블록을 가리키는 포인터가 전혀 남아 있지 않아 접근할 수 없는 누수입니다(최우선 수정 대상입니다).
    • indirectly lost: 이미 잃어버린(definitely lost) 블록을 통해서만 도달할 수 있는 블록입니다(루트 블록을 해제하면 함께 해결됩니다).
    • possibly lost: 블록의 시작점이 아니라 블록 내부 중간을 가리키는 포인터(interior pointer)만 남아 있는 경우입니다.
    • still reachable: 프로그램 종료 시점까지 시작점을 가리키는 포인터가 남아 있어 접근 가능한 블록입니다.

valgrind와 ASan은 무엇이 다르고, 언제 무엇을 쓰나요?

한 줄 답: 재컴파일이 가능한 평소 빌드·테스트에서는 속도 저하가 약 2x 수준인 ASan을 먼저 쓰고, 재컴파일이 불가능하거나 초기화되지 않은 값 추적 및 전체 오류·누수 요약이 한 번에 필요할 때는 약 20 to 30 times 느려지는 valgrind를 사용합니다.

동일한 leak.c 파일을 AddressSanitizer(ASan)로 빌드해 실행한 결과는 다음과 같습니다.

실제 실행 결과 (Debian, gcc 14.2.0 ASan, -O0; 처음 14줄, 실행마다 주소는 달라질 수 있음)

$ gcc -g -O0 -fsanitize=address -fno-omit-frame-pointer -o leak_asan leak.c
$ ./leak_asan
==PID===============================================================
==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x502000000060 at pc 0x556bd471129f bp 0x7ffd7fc4d6d0 sp 0x7ffd7fc4d6c8
WRITE of size 4 at 0x502000000060 thread T0
    #0 0x556bd471129e in main /tmp/vg/leak.c:18
    #1 0x7f67cf987ca7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
    #2 0x7f67cf987d64 in __libc_start_main_impl ../csu/libc-start.c:360
    #3 0x556bd4711100 in _start (/tmp/vg/leak_asan+0x1100) (BuildId: 835c3ecb59641265ff392653a6debb124f0659a0)

0x502000000060 is located 0 bytes after 16-byte region [0x502000000050,0x502000000060)
allocated by thread T0 here:
    #0 0x7f67cfc46c57 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
    #1 0x556bd471125b in main /tmp/vg/leak.c:17
    #2 0x7f67cf987ca7 in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
  • 실행 결과 비교:
    • 첫 오류에서 즉시 종료: ASan은 leak.c:18의 heap-buffer-overflow를 발견한 즉시 프로세스를 종료(exit code 1)하므로, 프로그램 종료 시점에 동작하는 누수 리포트(LeakSanitizer)가 이번 실행에서는 출력되지 않았습니다. 반면 valgrind는 오류를 기록한 뒤 끝까지 실행해 누수 요약까지 한 번에 출력합니다.
    • 초기화되지 않은 값 미탐지: ASan은 leak.c:15의 초기화되지 않은 변수(x) 읽기를 보고하지 않았습니다. Clang AddressSanitizer 공식 문서에 명시된 것처럼 초기화되지 않은 메모리 읽기 탐지는 ASan이 아니라 MemorySanitizer(또는 valgrind Memcheck)의 영역입니다.
    • 최적화 옵션 관찰 메모: 이번 실행에서는 동일한 코드를 -O1으로 ASan 빌드했을 때 컴파일러가 결과에 영향을 주지 않는(dead) malloc/쓰기/free를 제거해 아무런 오류도 출력되지 않았습니다. 재현용 테스트 코드에서는 -O0으로 확인하는 편이 안전합니다(ASan 자체의 세부 설정은 이전 AddressSanitizer 글을 참고하시기 바랍니다).
비교 항목 valgrind (Memcheck) AddressSanitizer (ASan)
작동 방식 기존 바이너리 직접 실행 (재컴파일 불필요, 줄 번호 확인용 -g -O0 권장) 컴파일 타임 계측 (-fsanitize=address, 재컴파일 필수)
실행 속도 저하 평소보다 약 20 to 30 times(20~30배) 느려짐 (Quick Start, --track-origins=yes 사용 시 추가 저하) 일반적인 속도 저하 약 2x(2배) (Clang ASan 문서)
초기화 안 된 값 읽기 탐지함 (--track-origins=yes로 생성 위치 추적 가능) 탐지 안 함 (MemorySanitizer의 역할)
메모리 누수 탐지 definitely / indirectly / possibly lost / still reachable 분류 리포트 Linux에서 LeakSanitizer 포함 (단, 앞선 오류로 즉시 종료되면 해당 실행에서는 생략됨)
오류 발생 시 동작 오류를 기록하고 끝까지 실행해 전체 요약을 한 번에 출력 첫 번째 오류(heap-buffer-overflow 등)에서 즉시 종료(exit code 1)
Linux 지원 플랫폼 ARM(ARMv7부터), ARM64(ARMv8), RISCV64, MIPS32/64 지원, X86은 유지보수 모드 (Platforms) 컴파일러(gcc/clang)가 -fsanitize=address를 지원하는 타깃

임베디드 보드에서 쓸 때 주의할 점은 무엇인가요?

한 줄 답: valgrind는 Linux가 도는 보드 범위에서만 사용할 수 있으며, 보드 RAM 여유를 확인해 Yocto/Buildroot 이미지에 패키지를 넣거나 가능한 경우 x86 호스트 빌드로 로직을 먼저 검증하는 것이 간편합니다.

  • Linux 유무와 아키텍처 지원 확인: Valgrind 공식 플랫폼 문서에 따르면 Linux 환경에서 ARM/Linux(supported since ARMv7), ARM64/Linux(ARMv8), RISCV64/Linux, MIPS32/Linux 및 MIPS64/Linux를 지원하며, X86/Linux는 유지보수 모드(in maintenance mode)입니다. OS 없이 동작하는 Cortex-M 베어메탈 MCU에는 valgrind를 적용할 수 없으므로, 반드시 Linux가 도는 보드 범위에서만 사용합니다.
  • Buildroot / Yocto 이미지에 포함하기:
    • Buildroot: 설정에서 BR2_PACKAGE_VALGRIND=y 옵션을 활성화합니다.
    • Yocto: 이미지 레시피의 IMAGE_INSTALL에 valgrind를 추가합니다(예: IMAGE_INSTALL:append = " valgrind").
  • 보드 메모리 여유와 호스트 검증 대안:
    • 소형 임베디드 보드에서는 valgrind 실행 시 추가 RAM이 필요하므로 보드의 가용 메모리가 충분한지 먼저 확인해야 합니다.
    • 호스트 PC에서 qemu-user로 ARM 바이너리를 실행하는 방식도 거론되지만, qemu-user 내부에 valgrind를 겹쳐 올리는 구성은 쉽지 않습니다. 하드웨어 종속 계층을 분리할 수 있다면 해당 로직을 x86 호스트 빌드로 재현해 호스트에서 valgrind나 ASan으로 먼저 검증하는 방법이 가장 단순합니다.

valgrind에 대해 자주 묻는 질문은 무엇인가요?

Q1. valgrind를 쓰려면 프로그램을 꼭 다시 컴파일해야 하나요?

재컴파일하지 않아도 기존 바이너리를 그대로 실행할 수 있습니다(Quick Start). 다만 출력 리포트에서 leak.c:15처럼 정확한 소스 파일 이름과 줄 번호를 확인하려면 -g 플래그(그리고 줄 번호 정확도를 위해 -O0 권장)로 빌드하는 것이 좋습니다.

Q2. definitely lost와 indirectly lost 중 무엇부터 고쳐야 하나요?

definitely lost부터 고치면 됩니다. indirectly lost는 이미 포인터를 잃어버린(definitely lost) 상위 블록(예: 위 코드의 struct node *n)을 통해서만 도달할 수 있는 하위 블록(예: n->name)이므로, 루트 블록을 해제하도록 수정하면 함께 해결됩니다.

Q3. ASan을 켰는데 왜 초기화되지 않은 변수(int x) 사용은 안 잡히나요?

AddressSanitizer(-fsanitize=address)는 힙/스택 버퍼 오버플로우 등을 잡는 도구이며, Clang ASan 문서에 안내되어 있듯 초기화되지 않은 메모리 읽기는 탐지 대상이 아닙니다. 초기화되지 않은 값 사용을 추적하려면 valgrind --track-origins=yes 또는 MemorySanitizer를 사용해야 합니다.

+ Recent posts