Skip to content

7.0 컴퓨터 구조 — 같은 코드가 같은 속도로 실행되지 않는 이유

알고리즘도 명령어 수도 거의 같은 두 코드의 실행 시간이 수 배씩 갈리고, 스레드를 늘렸는데 처리량이 떨어지는 일이 실제로 일어난다. 이 인트로는 CPU 내부 구조를 미리 외우는 대신, 이런 현상 앞에서 어떤 경쟁 가설을 세우고 무엇을 측정해야 하는지 — 챕터 7이 만들려는 진단 모델의 뼈대를 세운다.

학습 목표

  • 소스 코드의 순차 실행 모델과 실제 하드웨어의 실행 방식 사이의 간극이 성능 차이로 나타나는 이유를 설명한다.
  • 성능 저하 현상 앞에서 명령 공급, 데이터 의존, 메모리 대기, 코어 간 간섭이라는 네 경쟁 가설을 세운다.
  • 챕터 7의 두 본문이 각각 어느 가설을 검증하는 도구를 제공하는지 파악하고 학습 경로를 정한다.

출발점: 두 개의 배신

다음 두 상황은 이 챕터의 실측에서 실제로 재현되는 현상이다.

첫 번째. 배열에서 조건에 맞는 값만 합산하는 루프가 느려서, 팀원이 분기 예측 실패를 의심하고 if를 비트 마스크 산술로 바꿨다. 결과는 두 갈래였다 — 무작위 데이터에서는 6배 빨라졌지만, 정렬된 데이터에서는 개선이 미미했다. 같은 변환이 왜 어떤 입력에서는 극적으로 이기고 어떤 입력에서는 무의미한가?

두 번째. 멀티스레드 통계 수집기에서 worker마다 자기 카운터만 갱신하도록 설계했다. 락도 없고 공유 데이터도 없다고 믿었는데, worker를 1개에서 4개로 늘리자 총처리량이 오히려 절반 이하로 떨어졌다. 각자 자기 변수만 쓰는데 무엇을 공유하고 있었던 것인가?

두 현상의 공통점은 코드만 보아서는 원인이 보이지 않는다는 것이다. 소스 코드 어디에도 파이프라인, 캐시 라인, 일관성 프로토콜은 등장하지 않기 때문이다.

순차 실행이라는 착시

우리는 코드를 한 줄씩 읽고, 디버거도 한 줄씩 멈춰 주므로, CPU도 한 줄씩 실행한다고 생각하기 쉽다. 그러나 순차성은 결과에 대한 약속이지 실행 방식이 아니다. 실제 하드웨어에서는:

  • 명령어 하나가 끝나기 전에 다음 명령어들이 이미 파이프라인에 들어와 겹쳐 흐른다.
  • 분기의 결과가 나오기 전에 CPU가 방향을 예측하고 그쪽 명령어를 투기적으로 실행한다.
  • 서로 의존하지 않는 명령어들은 프로그램 순서와 다르게, 동시에 여러 개씩 실행된다.
  • 메모리 접근은 위치에 따라 비용이 백 배 가까이 다르고, CPU는 그 대기를 숨기려고 미리 데이터를 가져온다.
  • 코어마다 캐시 사본을 들고 있고, 한 코어의 쓰기가 다른 코어의 사본을 무효화하는 트래픽이 오간다.

이 장치들은 평소에는 완벽하게 숨어 있다. 예측이 맞고, 데이터가 캐시에 있고, 코어끼리 같은 캐시 라인을 건드리지 않는 동안에는 순차 실행 모델로 생각해도 아무 문제가 없다. 성능 문제는 이 장치들이 베팅에 실패하는 지점에서 나타난다. 그래서 명령어 수를 세는 비용 모델(1.1의 RAM 모델이 그 극단이다)은 실패 지점 근처에서 수 배씩 틀린다.

1.1에서 관찰한 147배 실측이 정확히 이 사례다. 같은 Θ(n) 순회가 배열이냐 무작위 연결 리스트냐에 따라 147배 갈렸다. 챕터 1은 그것을 현상으로 관찰하고 판단 기준으로만 삼았다. 챕터 7은 그 원인 — 캐시 라인, 프리페처, 그리고 비순차 실행조차 숨길 수 없는 의존적 메모리 접근 — 을 규명한다.

네 가지 진단 질문

이 챕터의 모든 성능 서사는 다음 네 질문을 공통 프레임으로 사용한다. 어떤 코드가 기대보다 느릴 때, 하드웨어 수준의 원인은 대부분 이 넷 중 하나 이상이다.

#질문확인하는 것
1명령이 충분히 공급되는가분기 예측 실패나 프론트엔드 정체가 실행 파이프라인을 비우는가
2명령이 서로 기다리는가데이터 의존 체인이 병렬 실행 자원을 놀리는가
3CPU가 데이터를 기다리는가캐시 미스, DRAM 지연, 프리페처 무력화가 병목인가
4코어끼리 방해하는가공유 캐시 라인과 일관성 트래픽이 병렬 확장을 막는가

출발점의 두 사례를 이 프레임에 놓으면: 분기 제거 실험은 질문 1(예측 실패가 진짜 병목이었는가)의 검증이고, 느려진 멀티스레드 카운터는 질문 4(논리적으로 공유하지 않아도 물리적으로 공유할 수 있는가)의 사례다.

중요한 것은 네 가설이 경쟁 관계라는 점이다. 하나의 낮은 처리량 앞에서 넷 다 후보가 될 수 있고, 증상만으로는 구분되지 않는 경우가 많다. IPC(사이클당 명령 수)가 낮다는 사실 하나는 넷 중 어느 것도 확정하지 못한다. 그래서 이 챕터는 결론을 외우는 대신 가설을 구분하는 대조 실험을 설계하는 법을 훈련한다.

팁이 아니라 진단 루프

"분기를 줄여라", "캐시 친화적으로 짜라" 같은 조언은 방향으로는 유용하지만, 조건 없이 적용하면 배신당한다. 예측이 잘 되는 분기는 거의 공짜라서 제거해도 얻는 게 없고, 데이터가 L1 캐시에 다 들어가는 크기라면 배치 최적화는 무의미하다. 이 챕터가 팁 대신 반복해서 적용하는 것은 다음 진단 루프다.

  1. 현상을 시간으로 관찰한다 — 1.1의 벤치마크 방법론(워밍업, 반복, 중앙값, 환경 명시)을 그대로 재사용한다.
  2. 네 질문에서 경쟁 가설을 둘 이상 세우고, 각 가설이 틀렸다면 무엇이 관찰될지 적는다.
  3. 한 변수만 바꾼 최소 대조 실험을 만든다 — 데이터 정렬 여부만, 패딩 유무만, worker 수만.
  4. 시간, 하드웨어 카운터, 생성 코드를 관찰한다. 측정 전에 컴파일러나 JIT이 실험 변수 자체를 제거하지 않았는지 확인한다.
  5. 개선을 적용하고, 그 개선이 지는 조건(경계 조건)까지 확인한다.

카운터 하나로 원인을 확정하는 서술은 이 챕터에 없다. 관찰된 상관관계와 원인을 입증하는 증거를 구분하는 것 — ch-1에서 세운 측정의 규율을 하드웨어 층위로 확장하는 것이 챕터 7의 태도다.

학습 지도

두 본문이 네 질문을 나눠 맡는다.

7.1 CPU와 파이프라인 — 질문 1, 2

명령어 수가 거의 같은 두 코드의 실행 시간이 왜 수 배 차이 나는가?

ISA(계약)와 마이크로아키텍처(구현)를 구분하는 데서 시작해, 파이프라인·분기 예측·슈퍼스칼라·비순차 실행이 순차 실행이라는 착시를 어떻게 만드는지 세운다. 정렬 여부만 다른 조건 합산과 누산기 개수만 다른 합산 루프를 실측해, 분기 예측 실패와 의존 체인이 각각 어떤 흔적을 남기는지 분리한다. cycles·instructions·IPC·branch-misses라는 측정 어휘를 갖추고, 낮은 IPC의 경쟁 원인을 좁히는 법을 다룬다.

7.2 메모리 계층과 캐시 일관성 — 질문 3, 4

같은 O(n) 순회 두 개의 실측이 왜 10배 갈리는가? 스레드를 늘렸는데 왜 더 느려지는가?

L1에서 DRAM까지의 지연 자릿수, 캐시 라인, 연관도, 프리페처, 메모리 수준 병렬성(MLP)으로 "CPU가 데이터를 기다리는" 구조를 세우고, working set 크기를 늘리며 캐시 계층의 경계를 직접 관찰한다. 후반부는 멀티코어로 넘어가 캐시 일관성 프로토콜이 만드는 트래픽과 false sharing을 다루고, 출발점의 "느려진 카운터"를 재현·진단·수리한다. 7.1의 비순차 실행 모델은 여기서 "메모리 지연을 왜 다 숨길 수 없는가"의 전제가 된다.

두 문서를 관통하고 나면 챕터 실습에서 행/열 순회, 분기 예측 실패, false sharing을 마이크로벤치마크로 재현하고 하드웨어 카운터로 원인을 확인한다. 이후 챕터와의 연결도 여기서 예고된다 — 주소 변환에도 캐시(TLB)가 있다는 사실과 컨텍스트 스위치가 캐시를 오염시키는 비용은 챕터 8이 이어받는다.

정리

  • 순차 실행은 결과에 대한 약속이지 실행 방식이 아니다. 실제 실행은 파이프라인, 투기, 비순차 실행, 캐시, 일관성 트래픽 위에서 흐른다.
  • 이 장치들은 베팅이 성공하는 동안 완벽하게 숨어 있고, 성능 문제는 베팅이 실패하는 지점에서 나타난다. 그래서 명령어 수 기반 비용 모델이 수 배씩 틀린다.
  • 하드웨어 수준 성능 진단은 네 질문으로 시작한다: 명령이 공급되는가, 명령이 서로 기다리는가, 데이터를 기다리는가, 코어끼리 방해하는가.
  • 챕터 7의 산출물은 팁 목록이 아니라 진단 루프다 — 경쟁 가설, 최소 대조 실험, 시간·카운터·생성 코드의 교차 검증, 경계 조건 확인.

확인 문제

1. 팀원이 "프로파일에서 IPC가 0.4로 낮게 나왔으니 메모리 병목이 확실하다. 캐시 최적화부터 하자"고 제안한다. 이 추론의 문제를 네 가지 진단 질문의 관점에서 지적하고, 다음에 할 일을 제시하라.

정답과 해설

낮은 IPC는 "사이클당 명령이 적게 완료됐다"는 결과일 뿐 원인을 말하지 않는다. 메모리 대기(질문 3)뿐 아니라 분기 예측 실패로 파이프라인이 반복해서 비워지는 경우(질문 1), 긴 의존 체인이 실행을 직렬화하는 경우(질문 2)도 같은 증상을 만든다. 다음 할 일은 가설을 구분하는 증거 수집이다 — branch-misses 비율을 함께 읽고, 데이터 정렬 여부·누산기 분리·working set 크기처럼 한 가설씩 겨냥한 최소 대조 실험으로 원인을 좁힌 뒤에 최적화를 고른다. 구체적인 방법은 7.1에서 다룬다.

2. 멀티스레드 프로그램에서 각 스레드가 서로 다른 변수만 갱신하도록 설계했는데도 스레드 수를 늘리자 느려졌다. "공유 데이터가 없으니 동기화 비용도 없다"는 믿음에서 무엇이 틀렸을 수 있는지, 이 챕터의 관점에서 가설을 세워라.

정답과 해설

논리적 공유(같은 변수)와 물리적 공유(같은 캐시 라인)는 다르다. 서로 다른 변수라도 메모리에서 인접해 같은 캐시 라인에 놓이면, 코어들의 캐시 일관성 프로토콜이 그 라인의 사본을 서로 무효화하는 트래픽을 만든다(false sharing). 검증 가설: 변수들을 캐시 라인 크기만큼 떨어뜨려 배치(패딩)했을 때 확장성이 회복되면 물리적 공유가 원인이었다는 증거다. 재현과 진단은 7.2에서 다룬다.

참고 자료

  • John L. Hennessy, David A. Patterson, Computer Architecture: A Quantitative Approach 6th ed. (2017) — 파이프라인, 캐시, 일관성의 표준 서술. 챕터 7 전체의 개념 지도로 활용한다.
  • Ulrich Drepper, What Every Programmer Should Know About Memory (2007) — 메모리 계층과 캐시 구조, 측정 방법의 1차 참조. 수치는 오래됐지만 구조 서술은 여전히 유효하다.
  • Brendan Gregg, Systems Performance 2nd ed. (2020) — 하드웨어 카운터를 포함한 성능 관찰 방법론. "카운터가 말하는 것과 말하지 못하는 것"의 감각을 얻는 데 참고한다.