Optimized C++ 책을 읽고나서
포스트
취소

Optimized C++ 책을 읽고나서

개요

1줄 요약: 초보자를 위한 책도 아닌, 중급자 그 이상이 읽어야 정말 재밌습니다.

C++ 언어를 공부하면서 조금 더 많은 언어적인 테크닉에 대해서 공부하고 싶었었다. 그래서 컴파일러를 통해 배우는 C++ 언어라던지, Effective C++ 등 다양한 책을 읽어보았었다. 특히, 필자는 언어적인 테크닉 외에 조금 무언가 로우 레벨적으로 최적화를 이뤄보고 싶었기 떄문에 Optimized C++ 책을 읽어보고 싶다! 란 생각을 하게 된 것 같다. 결론적으로 이야기를 하자면 나름 재미있었고, C++의 STL 라이브러리는 상당히 빠르면서 동시에 범용성을 지녔기 때문에 느리다는것을 알게 되었다. 그래서 STL 라이브러리를 사용하는것 보다 직접 구현한다면 (C언어 형태로 구현한) 성능 개선이 상당히 이뤄질수 있다는 것이 인상적이였다.

Optmized C++의 책에서 이야기 하는 내용은 하드웨어적인 최적화 관련된것이 아닌, 범용적으로 언어적인 레벨, 이동식 시멘트나 생성자, 메모리 매니저와 같은 내용이였다. 그래서 이 책을 읽고자하는 독자가 있다면, 어떻게 코드를 이쁘고 효율적으로 최적화할것인지 한다면 최고인 책인것 같다. 즉, 하드웨어적인 (캐시 메모리, 하드웨어의 Instruction Set Architecture에 종속적이지 않은) 내용은 없다.

핵심 내용

필자가 재밌게 읽었던 부분을 요약하자면 아래와 같고, 이름은 필자가 조금 변형하였다.

  1. 메모리를 읽는건 1바이트가 아닌 동시에 많은 데이터를 읽습니다. (45p)
  2. 문자열은 잦은 메모리 재할당과 복사가 자주 발생합니다. (110p)
  3. 기초적인 알고리즘 최적화 기법 (chapter 5) (너무 당연한 이야기들)
  4. 스마트 포인터는 성능과 거리가 멉니다. (162p)
  5. 비 캡처 람다는 일반 정적 함수에서 이름이 없는 것과 같다. (위치 까먹었습니다..)
  6. 클래스 상속과 가상 함수는 매우 매우 매우 느리다. (위치 까먹었습니다..)
  7. do-while은 어셈블리 입장에서 효율적 입니다. (위치 까먹었습니다..)
  8. 라이브러리 바깥에서 메모리 할당하도록 결정하세요. (264p) RVO 기법(187p)
  9. 검색 및 정렬 최적화 챕터(최고!) [너무 재밌어서 문제로 만들어보았습니다.]
  10. 메모리 관리 최적화 (너무 어렵습니다..)

요약

필자가 열심히 공부하면서 얻었던 지식을 한번 더, 책으로 읽을수 있어서 정말로 좋았습니다. 하지만, 책의 두께라던지, 각 문단 문단 하나가 너무 크게 쳐져 있는 듯하고, 너무 광대한 범위를 적은 페이지수로 설명하다 보니 아쉬웠던 점이 넘 많았습니다. 예시로는 필자가 작성한 핵심 내용 (1) 으로, CPU 는 메모리로 부터 데이터를 읽을 때 1 바이트씩 읽는것이 아닌 64 바이트씩 데이터를 읽는다. 라는 내용이다. 여기서, Optmized C++은 단순하게 메모리는 랜덤 엑세스가 가능하지만, 캐시 메모리로 인해, 순차 접근하는것이 더 효율적이다 라는 내용으로 끝나게 된다.

하지만, 실상은 여기서 끝나는것이 아니라, 멀티 쓰레드 환경에서 False Sharing이라는 문제도 발생한다. 이 처럼 뭔가… 책의 페이지수를 2000 페이지이나 3000 페이지로 늘려도 괜찮으니, 더 자세하고 많은 내용으로 적어줬으면 좋았을것 같았다. (약간 욕심이다.)

각각 요소를 필자가 기억하기 정리하는 목적으로 작성할 계획이다.

요약 2

  1. 메모리를 읽는건 1바이트가 아닌 동시에 많은 데이터를 읽습니다. (45p)

엄청 옛날의 시스템이나, 아두이노 같은 마이크로 프로세서의 경우에는 CPU에 집적가능한 회로의 수가 적으므로 프로그램 코드를 메모리로 부터 읽는것에 많은 회로를 작성하는 것이 어렵다. 또한, 캐시 메모리 없이 DRAM에서 바로 Register로 적재하는 경우도 있다. 그렇지만 이는 엄청 적은 임베디드 시스템이지, 일반적인 대형 컴퓨터 또는 일반 컴퓨터에 들어가는 CPU (x86 계열)의 경우에는 캐시메모리와 더 많은 레지스터, 한 번에 많은 데이터를 DRAM으로 부터 읽어온다. 따라서 일반적인 컴퓨팅 환경에서는 1바이트씩 메모리로부터 읽는것이 아닌 64 바이트나 특정 바이트의 수 만큼 데이터를 읽어서 캐시 메모리로 데이터를 적재한다.

그렇기 때문에, DRAM은 랜덤 엑세스가 가능하지만, 캐시 메모리로 데이터를 읽어서 적재하는 과정에서 순차적으로 데이터가 정렬되어 있어야 효율적이므로, 최대한 메모리를 순차적으로 정렬해 두는것이 좋다.

그렇다면 여기서 의문이 들 수 있다. 캐시 메모리의 크기에 맞춰서 데이터를 읽고, 재정렬하고 랜덤 액세스 하도록 만든다면 성능 개선이 이뤄질까? 라는 내용이다. 정답이다. 놀랍게도 이쪽으로 연구 분야로 Cache Aware for Computation 이라는 주제로 존재한다.

두 번째로 의문은 하나가 더 있다. 해당 책의 경우에서는 딱 하나의 경우, 싱글 쓰레드의 경우에서 문제점을 제기하였다. 그래서 더 많이 다뤄야하는 주제임에도 불구하고, 책의 두께나 내용상으로 생략한 내용이 있다. 필자가 생각하는 추가적인 문제는, 싱글 쓰레드가 아닌 멀티 쓰레드 환경에서 Cache Memory로 인한 데이터 정합성 쉽게 발생하는 문제가 있다.

주제로는 False Sharing 이라는 주제인데, 코드에서 변수 선언은 Stack MemoryData Section 에서 생성 되었고, 순차적으로 데이터를 선언했을 때 발생하는 문제이다. 여기서 다룰 주제는 아니므로, 간단하게 설명하자면, 아래의 코드를 생성하고, A 쓰레드 에서는 변수 a를, B 쓰레드 에서는 변수 b를 접근한다고 가정했을 때 문제가 발생한다. 위에서 이야기했던, DRAM에 순차적으로 정의 된 변수 a와 b는 각기 다른 CPU 코어의 캐시 메모리에 함께 적재되는 문제가 있다. 그렇기 때문에, B 쓰레드에서 사용되지 않는 변수 a도 함께 적재 되면서 A 쓰레드에서 변수 a 값이 변경이 된다면 B 쓰레드변수 a의 값도 함께 맞춰줘야하므로 생기는 문제이다.

1
int a, b;
  1. 스마트 포인터는 성능과 거리가 멉니다. (162p)

스마트 포인터는 안정성과 메모리를 반드시 Allocdealloc 하기 위해서 내부적으로 얼마나 변수에 대해서 참조하고 있는지 (참조 카운팅, use_count) 계산하고 있다. 하지만, C++ 표준 라이브러리는 안정성을 위해서, 특히 멀티 쓰레드 환경에서도 동작을 보장하기 위해서 참조 카운팅 변수를 atomic(원자성)으로 지정되어져 있다. 따라서, 스마트 포인터를 이용하여 데이터를 주고 받거나, 생성자, 파괴자가 잦은 호출이 발생이 되는 경우에는 생성자와 파괴자 호출 비용과 추가적으로 atomic한 변수로 인해 성능 지연이 발생하게 된다.

  1. 비 캡처 람다는 일반 정적 함수에서 이름이 없는 것과 같다. (위치 까먹었습니다..)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int func(int a, int b) {
    return a + b;
}

auto data = [](int a, int b) -> int {
    return a *b;
}
auto cap_data = [data](int a, int b) -> int {
    return a *b;
}

// lambda
data(10, 20);

// correctly work!
data = func;
data(10, 20);

// Error! because of captured [data]
cap_data = func; 

일반 함수는 코드의 길이가 짧다면 인라인화 되거나, 추가적인 최적화를 수행이 될 가능성이 있다. 그렇기 때문에, 비 캡처 람다 함수는 이상하게도 일반 정적 함수로 치환이 될 수 있고, 이를 통해 인라인화 될 가능성이 존재한다. 그렇다면 호출 스택 지점이나, Program Count의 주소, 함수의 코드 플로우가 변경이 되지 않으므로 성능 이점을 얻을 수 있다. 그렇기 떄문에 약간 신기하지만 비 캡처 람다 함수가 일반 함수와 동일하는 점이 신기하였다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

프로젝트, 누가 더 빠르게 알고리즘 문제를 푸는가?

Programming Language with LLVM in Udemy 인강을 듣고 나서