C++와 미크로아키텍처의 뉘앙스

발행: (2026년 6월 18일 AM 08:38 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to
C++ 소스 코드는 순서대로 작성됩니다. 하지만 프로세서는 이를 순차적으로 실행하지 않습니다.

이것은 첫 번째 수정입니다. 또한 많은 성능 논의가 피하려는 부분이기도 합니다.
현대 고성능 코어는 명령어 스트림을 순차적으로 받아들이면서, 이를 내부 연산으로 분해하고, 레지스터를 재명명하며, 작업을 일정 구조에 배치하고, 준비된 연산을 조기에 실행한 뒤 프로그램 순서대로 결과를 퇴출합니다. 이 기계는 순차적 실행의 가시적인 동작을 보존합니다. 내부적으로는 라인 단위로 하나씩 진행하지 않습니다.
일반 소프트웨어에서는 거의 눈에 보이지 않지만, 수 십 나노초 수준으로 실행되도록 설계된 C++에서는 그렇지 않습니다.
그 규모에서는 성능은 단순히 명령어 개수만으로 결정되지 않습니다. 명령어를 병렬로 스케줄링할 수 있는지, 아니면 프로그램이 조용히 의존성 사슬을 만들고 나중에 놀랐는지에 달려 있습니다.
명령어 순서 외부 실행이 존재하기 때문에 in-order 파이프라인이 시간 낭비 때문에 필요합니다.
오래된 인스트럭션이 stall(정지)될 경우, 명령어 순서 외부 실행은 후속 독립 가능한 인스트럭션이 준비돼 있더라도 대기해야 합니다. 이는 하드웨어 활용이 불량한 것입니다.
칩에는 실행 단위가 충분히 있지만, 명령어 스트림은 더 많은 작업을 담고 있습니다.
동적 스케줄링은 이 문제를 일부 해결합니다. 프로세서는 입력이 준비된 연산을 추적합니다. 연산이 준비돼 있고 실행 단위가 가용하면 즉시 발행할 수 있습니다. 오래된 연산은 여전히 대기 중일 수 있으며, 후속 연산은 먼저 실행될 수 있습니다. 최종 아키텍처 상태는 여전히 순서대로 커밋되므로 프로그램이 올바르게 동작합니다.
Tomasulo 알고리즘은 이 아이디어를 구현한 고전적인 모델입니다. Reservation stations(예약 스테이션)와 레지스터 재명명을 사용해 명령이 원래 프로그램에서 나타나는 순서가 아니라 연산자가 준비될 때 실행하도록 했습니다 (Tomasulo, 1967). 이후 초스칼라 프로세서는 추측과 리오더 버퍼를 추가하여 동일한 기본 접근법을 확장했지만, 핵심 아이디어는 변했습니다: 텍스트 순서가 아닌 준비 정도에 따라 실행합니다 (Hennessy & Patterson, 2019).
이것은 C++에 특히 중요한 점입니다: 프로세서는 프로그램 리스트를 단순히 읽는 것이 아니라 그래프를 해결합니다.
노드는 연산이고, 엣지는 의존성입니다.
엣지가 없으면(의존성이 없을 경우) 병렬 실행이 가능합니다.
명령어 순서 외부 실행은 진정한 데이터 의존성을 위반하지 않습니다.
명령어 B가 명령어 A에 의해 생성된 값을 필요로 한다면, B는 A를 기다려야 합니다. 스케줄러도 이걸 바꿀 수 없으며, “현대 CPU는 똑똑하다”는 믿음도 영향을 주지 않습니다.

다음은 간단한 예시입니다:
std::uint64_t s = 0;
for (std::size_t i = 0; i < value; i++) {
p = p->next;
}
다음 주소는 현재 로드에 의존합니다. 프로세서는 p->next를 먼저 로드한 뒤에만 p->next->next의 로드를 발행할 수 있습니다. 이 체인은 직렬적입니다.

이것은 주로 캐시 로컬리티에 대한 강의가 아닙니다. 캐시 로컬리티는 중요하지만, 명령어 순서 외부 점은 더 좁습니다: 미래 작업의 주소가 아직 존재하지 않습니다. 스케줄러는 계산되지 않은 주소를 가진 연산을 발행할 수 없습니다.

포인터가 많은 구조는 수십 나노초 코드에서는 자주 성능이 떨어집니다. 프로세서는 충분히 리소스를 가지고 있지만, 프로그램은 한 번에 하나씩 의존적인 단계를 제공합니다.
큰 outside 창은 인접한 다른 독립 작업이 있을 때만 도움이 됩니다. 전체 루프가 의존성 사슬로 구성되어 있다면, 창은 대기 상태로 채워집니다.

그 코드는 실제로는 CPU에 바운드되지 않습니다. 의존성에 의해 제한됩니다.
명령어 순서 외부 실행은 컴파일러가 만든 명령어 스트림을 기반으로 동작합니다. 소스 레벨 의도는 볼 수 없습니다.
함수 호출, 별칭, 가상 디스패치, 혹은 투명한 제어 흐름 뒤에 유용한 독립성이 숨겨져 있다면, 컴파일러는 이를 방출 코드에 노출시키지 못할 수 있습니다.

인라인화는 최적화가 더 많은 맥락을 가질 수 있게 해주기 때문에 중요합니다. 더 많은 맥락을 갖춘 경우, 컴파일러는 중복 작업을 제거하고, 값을 레지스터에 유지하며, 독립적인 연산을 재배열하거나 루프를 충분히 전개해 여러 개의 독립 체인을 노출시킬 수 있습니다.
Pikus는 C++ 구조, 컴파일 최적화, 그리고 CPU 활용 사이의 이 상호작용을 강조합니다: 고성능 C++은 컴파이터와 하드웨어가 가용한 자원을 효과적으로 사용하도록 충분한 정보를 제공하는 데 달려 있습니다 (Pikus, 2021).

모든 함수 호출이 나쁜 것이라고 할 수는 없습니다. 그것은 편리한 규칙일 뿐이며, 따라서 의심스럽습니다.
실제 질문은 최종 명령어 스트림이 코어에 스케줄링될 충분한 독립 작업을 노출하고 있는지입니다.
소스 코드는 측정 대상이 아닙니다. 방출된 머신 코드가 대상입니다.

명령어 순서 외부 실행은 또한 레이턴시와 처리량을 구분하게 합니다.
연산의 레이턴시는 그 결과가 의존적인 연산에 제공되는 데 걸리는 시간을 의미합니다.
연산의 처리량은 충분한 독립 작업이 존재할 때, 한 사이클당 시작될 수 있는 연산 횟수를 나타냅니다.
이 구분은 끊임없이 중요합니다.

멀티플라이 연산이 수 사이클의 레이턴시를 가지고 있지만, 프로세서는 한 사이클에 하나씩 멀티플라이 시작이 가능하면, 하나의 의존성 사슬은 전체 레이턴시를 반복적으로 겪게 됩니다. 네 개의 독립적인 체인이라면 처리량 한계에 근접할 수 있습니다. 동일한 명령어지만, 다른 의존성 그래프, 다른 성능.

이전에 제시한 누적기 예시가 중요한 이유는, 이는 덧셈 자체를 빠르게 만들지 않으며, 루프를 레이턴시 제한에서 처리량 제한으로 전환하기 때문입니다.

저지연 C++은 보통 이런 작업을 포함합니다: 핵심 의존성 사슬을 찾고, 단축하고, 분할하거나, 그 사이에 독립적인 작업을 배치하는 작업입니다. 화려하지 않지만, 일반적으로 효과적이며, 드문 조합입니다.

Wall-clock 시간만 보고하는 벤치마크는 종종 충분하지 않습니다. 한 버전이 빠르다는 것만을 보여줄 뿐, 그 이유까지 설명하지 못합니다.

명령어 순서 외부 실행에 유용한 질문은 다음과 같습니다:

  • 루프가 한 의존성 사슬에 의해 제한되는가?
  • 전개(unrolling)가 독립적인 연산을 노출했는가?
  • 컴파일러가 실제로 여러 누적기를 방출했는가?
  • 명령어가 피연산자나 실행 자원에 대기하고 있는가?
  • 작은 소스 변경이 생성된 의존성 그래프에 영향을 미쳤는가?

Compiler Explorer, objdump, LLVM-MCA, perf, 그리고 Intel VTune과 같은 도구들은 이 질문들을 답변하는 데 도움이 됩니다. 이들은 사고를 대체하지 않지만, 사고에 허구의 요소를 줄여줍니다.

간단한 타이밍 결과는 “빠르다”고 말할 수 있습니다. 어셈블리와 카운터는 속도가 적은 작업량, 더 나은 스케줄링, 감소한 대기 상태, 혹은 짧은 크리틱 경로에서 비롯되었는지 설명합니다.

나노초 수준 코드에서는 이 차이가 중요합니다.

명령어 순서 외부 실행은 느린 코드에 일반적으로 적용되는 복스러운 것이 아니라, 명령어 스트림 내에 준비된 작업을 찾는 구체적인 하드웨어 전략입니다.
독립적인 작업이 존재할 때 대기 상태를 숨길 수 있습니다.

저지연 C++을 작성하는 프로그래머는 다른 질문을 해야 합니다. ‘작성한 줄 수’를 묻기보다, ‘컴파일러가 방출한 명령어 수’를 묻는 것이 더 낫습니다.

더 좋은 질문은 다음과 같습니다: 제가 프로세서에게 제공한 의존성 그래프는 무엇인가요?
그 그래프는 outside 실행 코어가 실제로 스케줄링하는 것입니다. 소스 코드는 문제 제출 방법일 뿐입니다.

0 조회
Back to Blog

관련 글

더 보기 »

코드 리뷰가 잘못됐다

!Cover image for Code Review Gone Wronghttps://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2F%2Flavkesh.com%2F...