IT_Programming/C · C++

[ C++ ] Efficient C++ Key Point 정리

JJun ™ 2009. 6. 3. 00:25

 

Efficient C .txt

1. 객체를 정의하면 객체 생성자와 소멸자의 형태로 조용하 실행이 발생한다.
   객체의 생성과 소멸은 대개 부하가 아니기 때문에 우리는 "조용한 오버헤드"라는 말 대신 "조용한 실행"

   이라는 말을 사용한다. 만약 생성자와 소멸자가 수행하는 연산이 항상 필요하다면, 이 연산들은 효율적인

   코드로 간주 되어야 한다. (생성자와 소멸자를 인라인으로 만들면 호출과 반환 오버헤드를 줄여 줄 수

   있다.) 생성자와 소멸자는 항상 순수한 특성만을 가지는 것은 아니고 현저한 오버헤드를 생성할 수 있다.
   어떤 상황에서는 생성자(혹은 소멸자)가 수행하는 연산이 사용되지 않는다.
   이것은 C++ 언어적인 주제라기보다 디자인 주제에 밀접하다.
   하지만 C는 생성자/소멸자의 개념을 갖지 않기 때문에 C 코드에서 이러한 상황은 잘 발생하지 않는다.

 

 

2. 객체를 참조로 전달하는 것도 좋은 성능을 보장하지는 않는다. 객체 복사를 수행하지 않으면 도움이

   되겠지만, 우선 객체를 생성하고 소멸할 필요가 없다면 더욱 도움이 될 것이다.

 

 

3. 잘 사용되지 않는 결과를 만들어내는 연산을 방지한다. 추적이 꺼진 경우, string 멤버의 생성은 가치가

   없고, 부담이 많이 든다.

 

 

4. 디자인의 유연성에 너무 집착하지 말자. 진정으로 필요한 것은 문제 영역에 적합할 정도로 유연한

    디자인이다. char 포인터는 가끔 string에 비해서 더 효율적이면서도 단순한 작업을 잘 수행한다.

 

 

5. 인라인 - 작은 함수를 자주 호출하여 발생하는 함수호출 오버헤드를 제거한다.
   Trace 생성자와 소멸자를 인라인으로 만들면 Trace 오버헤드를 줄이기 쉬워진다.

 

 

6. 생성자와 소멸자는 손으로 작성한 C 코드만큼 효율적이어야 할 것이다.
   하지만, 실제상황에서 생성자와 소멸자는 과잉 연산의 형태를 가진 오버헤드를 종종 포함한다. (주의!)
 


7. 객체의 생성(소멸)은 부모와 멤버 객체의 재귀적인 생성(소멸)을 발생시킨다.
   복잡한 계층에서 객체 생성/소멸의 조합을 조심해야 한다. 이것 때문에 생성과 소멸이 더 많은 부하를

   낳게 된다.

 

 

8. 코드가 생성한 모든 객체와 수행한 모든 연산을 실제로 사용하는지 확인한다.
   프로그래머는 자신이 사용하는 클래스 내부를 주의 깊게 들여다보기를 권유한다.
   이 충고는 OOP가 주창하는 것과는 약간 거리가 있다. OOP는 클래스를 캡슐화된 암흑상자로 간주하고,
   내부를 들여다 보는 것을 권유하지 않기 때문이다. 위 두 충고간의 상반되는 관점에서 어떻게 균형을

   맞춰야 할까? 이 질문에 대한 해답은 상황에 따라 다르기 때문에 간단해질 수 없다. 암흑 상자 접근이

   코드의 80%에 대해서는 완벽하게 작동하겠지만, 성능에 치명적인 20%의 코드에 대해서는 위험요소를

   만들 수 있다. 이 문제는 또한 응용 프로그램에 의존하기도 한다. 어떤 응용 프로그램은 유지 보수성과

   유연성에 가중치를 부여하며, 다른 프로그램은 성능을 가장 최우선으로 고려하기도 한다.

   프로그래머로서 여러분은 최대화시키고자 하는 사항이 정확하게 무엇인지에 관한 질문에 대답해야 한다.

 

 

9. 객체 생명 주기는 전혀 부담을 갖지 않는 것이 아니다. 최소한 객체의 생성과 소멸은 CPU 주기를 소모한다.
   꼭 필요하지 않으면 객체를 생성하지 말자. 전형적으로 객체가 실제로 사용되는 범위까지 객체 생성을
   연기하는 것이 좋다.

 

 

10. 컴파일러는 생성자 본문으로 들어가기 전에 포함된 객체 멤버를 초기화한다.

     멤버 객체 생성을 완료하기 위해서 초기화 단계(멤버 초기화 구문)를 사용해야 한다.

     이렇게 하면 생성자 본문에서 이후에 대입 연산자를 호출하는 오버헤드를 절약할 수 있다.

     어떤 경우에는 이것 덕분에 임시 객체의 생성을 방지 할 수 있다.

 

 

11. 가상함수의 부하는 실행 시에 동적으로 바인딩되는 함수 호출들이 인라인 될 수 없기 때문에 발생한다.
     잠재적으로 효율성을 증가시킬 수 있는 유일한 방법은 인라인을 통하여 속도를 개선 시킬 수 있다는

     것이다. 호출과 반환 오버헤드가 중요하지 않은 함수에 대해서는 인라인으로 인해 얻을 수 있는 효율성이

     그다지 높지 않다.

 

 

12. 템플릿은 상속 계층보다 더욱 성능 친화적이다. 템플릿을 사용하면 컴파일 시에 형식 확인이 이루어지며,
    컴파일 시에 발생하는 부하는 없다.

 

 

13. 객체를 값으로 반환해야 하는 경우, 반환값 최적화를 사용하면 지역 객체가 생성하고 소멸할 필요가

     없어지므로 성능이 좋아질 수 있다. (ex) return 생성자(인자);

 

 

 

14. 컴파일러 구현에 따라 RVO(반환값 최적화)의 적용 사례가 달라진다. 언제 그리고 어떤 조건에서 RVO가

     적용되는지 알려면 컴파일러 문서를 참조하거나 직접 실험해 보아야 한다.

 

 

15. 연산 생성자(연산자 오버로딩)를 사용하여 RVO가 적용될 확률을 높일 수 있다.
    (ex) // 연산 생성자  
         Complex::Complex(const Complex& c1,const Complex& c2):real(c1.real+c2.real),imag 

                                        (c1.imag+c1.imag) {}  
         Complex operator+(const Complex& lhs,const Complex& rhs)  
         {  
              return Complex(lhs,rhs);  
         }

 

 

16. 임시 객체는 생성자와 소멸자 실행 때문에 인과 성능 부하를 두 배로 만들어낸다. 

 

 

17. 생성자에 explicit을 선언하면 컴파일러는 이 생성자를 사용하여 이면에서 형식 변환을 수행하지 못한다.
     (묵시적 형변환에 의한 객체 생성을 할 수 없다.)

 

 

18. 가능하다면 객체 복사를 방지하자. 참조로 전달하고 반환하자.

 

 

19. <op>= 연산자를 사용하여 임시객체를 제거할 수 있다. (<op> : +, -, *, /)
    (ex) const Rational operator+(const Rational &lhs, const Rational &rhs)
           {
                  return Rational(lhs) += rhs; 
          

 


20. 유연성은 속도와 절충된다. 메모리 관리의 강력함과 유연성이 높아질수록 실행 속도는 떨어진다.

 

 

21. new()와 delete()에 의해 구현되는 전역 메모리 관리자는 일반적인 목적을 가지고 있으며,

     그러므로 부하를 많이 준다.

 

 

22. 고정된 크기의 메모리 블록을 주로 할당한다면, 특화된 고정 크기 메모리 관리자가 성능을 현저하게

     높여줄 것이다.

 

 

23. 단일 쓰레드에 한정된 메모리 블록을 주로 할당한다면 위와 동일한 성능 개선이 가능하다.
  
  단일 쓰레드 메모리 관리자는 전역 new()와 delete()가 처리해야 하는 동시성을 고려할 필요가 없기

     때문에 성능에 많은 도움을 준다.

 

 

24. 전역 메모리 관리자는 일반적인 목적을 가지고 있기 때문에 부하를 많이 준다.

 

 

25. 대부분 단일 쓰레드에 한정된 메모리 블록을 할당한다면 비약적인 성능 개선을 꾀할 수 있다.
     단일 쓰레드 메모리 관리자는 멀티쓰레드 메모리 관리자보다 훨씬 빠르다.

 

 

26. 효율적인 단일 쓰레드 할당기를 여러 개 개발한다면, 켐플릿을 사용하여 이 할당기들을 멀티쓰레드

     환경으로 쉽게 확장 할 수 있다.

 

 

27. 인라인은 메서드 호출을 메서드의 코드로 교체하는 작업이다.

 

28. 인라인은 호출 오버헤드를 없애고 호출간 최적화가 일어날 수 있도록 하여 성능을 향상시킨다.

 

 

29. 인라인은 주로 실행 시간 최적화이지만, 실행 이미지의 크기도 작게 만들어 준다.

 

 

30. 리터럴 인자와 인라인이 복합할 경우, 컴파일러가 성능을 대폭 개선시킬 수 있는 가능성이 매우 커진다.

 

 

31. 인라인은 기대에 어긋난 결과를 낳을 수 있고, 너무 과도하게 인라인 작업을 수행하면

     반드시 어긋난 결과를 낳게 된다. 인라인은 코드 크기를 증가 시킬 수 있다. 코드 크기가 크면 작은 코드에

     해 캐시 누락과 페이지 실패가 자주 발생할 수 있다.

 

 

32. 명백하지 않은 메서드를 인라인으로 만들기 위해서는 단순한 직감이 아닌 샘플 실행 프로파일이 필요하다.

 

 

33. 정적 크기가 크고 동적 크기가 작은 메서드가 자주 불려진다면, 이 메서드를 다시 작성하여

     주요 동적 기능을 밖으로 빼내고 동적 구성 요소를 인라인으로 만들 수 있는지 고려한다.

 

 

34. 명백한 메서드와 싱글톤 메서드는 항상 인라인이 될 수 있다.

 

 

35. 인라인은 성능을 개선시킬 수 있다. 목적은 프로그램의 빠른 경로를 찾아 이 경로를 인라인으로

     만드는 것이다. 하지만 이 경로를 인라인으로 만드는 것은 명백하지 않을 수 있다.

 

 

36. 조건부 인라인 작업을 수행하면 메서드가 인라인이 되는 것을 방지할 수 있다.
     컴파일 시간을 줄여주고, 개발 초기의 디버깅을 간략화 시켜 준다.

 

 

37. 선택적 인라인은 모양새는 좋지 않으나, 재귀 메서드의 성능을 개선시키는데 효과적인 기술이다.

 

 

38. 지역 정적 변수를 다룰 때에는 조심해야 한다.

 

 

39. 인라인의 목적은 호출 제거에 있다. 인라인을 사용하기 전에 시스템의 진정한 호출 부하를 확인하자.

 

 

40. STL은 추상성, 유연성, 효율성을 모두 갖춘 보기 드문 라이브러리다.

 

 

41. 응용 프로그램이 어떤 방식으로 컨테이너를 사용하느냐에 따라 가장 효율적인 컨테이너는 달라진다.

 

 

42. STL이 다루지 못하는 문제 영역에 관한 사항을 알지 못하면, 들인 노력만큼 효율적으로 sTL 구현의

     성능을 이기기는 힘들다.

 

 

43. 어떤 특정한 시나리오에서 STL 구현의 성능을 능가하는 것은 가능하다.

 

 

44. 참조 횟수는 자동으로 성능을 증가 시켜주지는 못한다. 성능이 중요한 요소라면 참조 횟수, 실행 속도,
    리소스 보존은 주의 깊게 평가되어야 할 섬세한 상호 작용을 이룬다. 참조 횟수는 사용 패턴에 따라
    성능을 도와주기도 하고 해치기도 한다.

 

    <참조 횟수가 이로울 경우>
     - 대상 객체가 리소스를 많이 차지한다.
     - 필요한 리소스를 할당하고 해지하는데 부하가 많이 든다.
     - 공유의 정도가 크다. 복사 생성자와 대입 연산자의 사용 때문에 참조 횟수가 커지기 마련이다.
     - 참조의 생성과 소멸이 상대적으로 부담이 적다.
  
    만약 어떤 상황이 위 항목들과 반대라면, 참조 횟수를 사용하지 않고 참조 횟수를 가지지 않은
    평범한 객체를 사용하는 것이 좋다.

 

 

45. 코딩 최적화는 국지적 범위를 가지고 있으며, 전체적인 프로그램 디자인의 이해를 필요로 하지 않는다.
    가장 빠른 코드는 절대 실행하지 않는 코드이다. 부하를 많이 주는 연산을 밖으로 빼내기 위해
    다음 사항을 점검해보자.
 
    - 결과를 사용할 것이가? 당연한듯이 들리지만 실제로 일어난다.

      종종 연산을 수행해 놓고 결과를 절대 사용하지 않는다.

 

    - 결과가 지금 필요한가? 결과가 실제로 필요한 지점까지 연산을 연기한다.
       미숙한 연산이 어떤 실행 경로에서 절대 사용되지 않을 수 있다.

 

    - 결과를 이미 알고 있는가? 부하를 많이 주는 연산의 결과를 두 줄 위에 나와 있음에도 불고하고

       이 연산을 다시 수행하는 것을 보았다. 만약 실행 흐름의 앞부분에서 미리 계산하였다면,
       결과를 재사용할 수 있도록 만든다.

 

    가끔 연산을 밖으로 빼낼 수 없으며 수행해야만 하는 경우가 있다. 이제 속도를 증가시키기 위해 도전하자.
  
    - 연산이 전체적으로 일반적인가?

      문제 영역이 필요로 하는 만큼만 유연하면 되고 더 이상은 필요하지 않다.


    - 단순한 가정을 이용하자. 유연함을 감소시키면 속도는 증가한다.

 

    - 어떤 유연성은 라이브러리 호출에 숨겨져 있다. 자주 호출되는 특정 라이브러리 호출을 자체 제작하여
      속도를 증가시킬 수 있다. (사용하는 라이브러리와 시스템 호출의 숨겨진 부하에 대해서 잘 알아야 한다.)

 

    - 메모리 관리를 최소화한다. 대부분의 컴파일러에서 메모리 관리는 상대적으로 부하를 많이 준다.

 

    - 모든 입력 데이터의 집합을 고려한다면 이것의 20%가 시간의 80%를 차지한다.
       다른 시나리오를 희생하고, 전형적인 입력을 처리하는데 드는 속도를 개선시킨다.

 

   - 캐시, RAM, 디스크 액세스간의 속도 차이는 상당히 크다.
      캐시 적중이 많이 일어날만한 코드를 작성한다.

 

 

46. 소프트웨어 성능과 유연성 사이에는 기본적인 장력이 존재한다.
    소프트웨어의 20%가 80%의 시간을 실행하며, 성능은 가장 먼저 유연성이라는 댓가를 치른다.

 

 

47. 캐싱 기회는 세밀한 코딩 세부 사항뿐만 아니라, 전체 프로그램 디자인에서도 나타난다.
     이전 계산 결과를 단순히 저장함으로써 상당한 양의 계산을 방지 할 수 있다.

 

 

48. 어떤 계산은 발생 가능한 전체 실행 시나리오 중에서 일부에서만 필요할 수도 있다.
    이러한 계산을 반드시 필요로 하는 실행 경로 지점까지 연기해야 한다.
    만약 계산이 먼저 수행된다면 이것의 결과는 사용되지 않을 수도 있다.

 

 

49. 대규모 소프트웨어는 혼란해지는 경향이 있다. 혼란스러운 소프트웨어의 부산물 중에 하나는
    사용되지 않는 코드(어떤 역할을 수행한 적이 있지만, 더이상 그러하지 않은 코드)의 실행이다.
    사용되지 않는 코드와 다른 불필요한 계산을 주기적으로 제거하는 것은 전체 소프트웨어를 튼튼하게
    할 뿐만 아니라, 성능을 개선시킬 것이다.

 

 

50. SMP는 현재 MP 아키텍쳐의 주류이다. 이것은 하나의 버스를 통하여 하나의 메모리 시스템에 연결된
    여러 대칭 프로세서로 구성된다. 버스는 SMP 아키텍쳐의 확장성 취약 연결이다. 하나의 프로세서당
    대형 캐시를 사용하면 버스 경쟁을 제어할 수 있다.

 

 

51. Amdahl의 법칙은 응용 프로그램의 잠재적인 확장성에 상한선을 가한다.
     확장성의 한계는 순차적인 계산의 부분이다.

 

 

52. 단일 작업을 여러 하위 작업으로 나누어 여러 쓰레드가 동시에 병렬적으로 작업을 수행할 수 있도록

     만든다.

 

 

53. 코드 메커니즘, 임계 영역은 꼭 필요한 코드 이외에는 어떠한 것도 포함하지 않아야 한다.
     공유 리소스를 직접 조작하지 않는 코드는 임계 영역에 있으면 안된다.

 

 

54. 캐시 - 가끔 이전에 임계 영역을 거친 결과를 캐시에 저장하여 임계 영역을 또 다시 실행하는 것을

               제거할 수 있다.

 

 

55. 아무것도 공유하지 않는다. 만약 적고 고정된 수의 리소스 인스턴스만이 필요하다면,
    공용 리소스 풀을 사용하지 않는다. 이 인스턴스들을 쓰레드 전용으로 만들고 재활용한다.

 

 

56. 부분 공유 - 두 개의 동인한 풀을 사용하여 경쟁을 반감시키는 것이 좋다.

 

 

57. 잠금 단위 - 리소스를 항상 함께 갱신하지 않는 한, 동일한 잠금 아래 리소스들을 혼합하여 보호하지

                     않는다.

 

 

58. 거짓 공유 - 클래스 정의에서 두 개의 잠금을 근접하여 정의하지 않는다.
                     두 개의 잠금이 같은 캐시 선을 공유하여 캐시 일관성 부재를 야기하는 것을

                     바라지 않을 것이다.

 

 

59. 놀란 양떼 - 잠금 호출의 특성을 분석한다.
                     잠금이 해지될 때 모든 쓰레드가 깨어나는가? 아니면 단 하나의 쓰레드만이 깨어나는가?
                     모든 쓰레드를 깨우는 잠금 호출은 응용 프로그램의 확장성을 위협한다.

 

 

60. 시스템과 라이브러리 호출 - 구현 특성을 분석한다.

                                           (몇몇은 순차적 코드의 상당한 부분을 숨기고 있을 지도 모른다.)

 

 

61. 리더/라이터 잠금 - 주로 읽기 용의 공유 데이터는 이 잠금으로부터 많은 혜택을 볼 수 있다.
                                이 잠금은 리더 쓰레드간의 경쟁을 없애준다.

 

 

62. 사용하길 원하는 메모리가 프로세서와 멀어질수록 액세스하는 시간도 길어진다.
     프로세서, 레지스터에 밀접한 리소스는 용량은 작지만 매우 빠르다. 이들의 최적화는 매우 중요하다.

 

 

63. 가상 메모리는 무료가 아니다. 시스템이 관리하는 가상 구조체에 무절제하게 의존한다면 성능에

     매우 심각한 영향을 줄 수 있는데, 보통은 부정적인 영향을 준다.

 

 

64. 컨텍스트 전환은 부하는 많이 준다. 방지하자!
                              

65. 내부적으로 관리하는 비동기 I/O가 더 나은 경우도 있음을 알지만, 프로세서 아키텍처의 다가오는 전환은
    일체식 쓰레딩 접근의 단점을 더욱 부각시킬 것이라는 예상을 해본다. 


Efficient C .txt
0.01MB