2020년 5월 28일 목요일

C++ 최적화 책, 4장 문자열 최적화


C++ 최적화 : 최고 성능을 구현하는 10가지 검증된 기법 
커트 건서로스 저/옥찬호 역/박수현 감수 | 한빛미디어

책을 보고 간략히 정리하려고 함.
오랜만에 보는 괜찮은 c++ 언어 서적이다.
"Effective Modern C++" 을 보다가 내용과 번역에 질려버리고 
"모던 C++ 입문" 을 봤었는데 그 책의 저자가 이 책을 번역하였음.

책의 전반적인 내용은 최적화라는 관점에서 가능한 모든 방법을 소개하는 것 같다.
최적화의 방법이 정도가 없어 일반적인 내용, 저자의 경험과 테스트를 통해서 알게 된 내용들을 소개하고 있다. 
C++ 문법, skill을 가르치는 것은 아니고 효율적인 코드를 작성하기 위해 알아야 하는 내용과 방법을 제시한다.



1장 최적화란

다음 권고 사항들로 정리할 수 있어 보인다.
- 더 좋은 컴파일러를 사용하고 최적화 설정을 사용하세요.
- 최적의 알고리즘을 사용하세요.
- 더 좋은 라이브러리를 더 잘 사용하세요.
- 메모리 할당을 줄이세요.
- 복사를 줄이세요.
- 계산을 제거하세요.
- 최적의 자료구조를 사용하세요.
- 동시성을 증가시키세요.
- 메모리 관리를 최적화하세요.

책의 목차를 보면 알겠지만 위 내용을 책에서 나눠 설명하고 있고
각 내용을 간략히 정리하는 챕터이다. c++ 언어 특성과 관련이 있는 팁도 있지만, 프로그래밍 시 일반적으로 알아야 하고 적용할 수 있는 조언이 대부분이라 유익하다.



4장 문자열 최적화

(오라일리에서 제공하는 online book 페이지입니다.
아래 내용, 코드는 모드 아래 출처에서 발췌하였다.)

std::string은 저장소를 동적으로 할당합니다.
문자열이 추가되면서 문자열보다 저장소가 훨씬 크게 할당할 수 있어 메모리가 낭비될 수 있다고 합니다.
문자열은 값처럼 동작하여 문자열 사용 시 복사가 많이 일어날 수밖에 없다고 합니다. 

책에서는 아래 함수를 예시로 최적화를 진행합니다.

std::string remove_ctrl(std::string s) {
    std::string result;
    for (int i=0; i<s.length(); ++i) {
        if (s[i] >= 0x20)
            result = result + s[i];
    }
    return result; 
} 

그냥 봐도 수정할 것이 많아 보임...

(임시 문자열 객체 생성 제거) 
result = result + s[i]  에서 
s[i]를 더한 새로운 result를 저장하기 위해 
생성된 임시 문자열을 
  result += s[i];
로 변경하여 임시 문자열이 생성되지 않게 하여 성능을 개선합니다.

(문자열 저장 공간 예약)
result에 저장하는 문자열은 최대 s의 크기이므로
result.reserve(s.length())를 호출하여
미리 저장 공간을 예약하여 
result 값이 증가함에 따라 저장 공간 확보 시도를 줄여 성능을 개선합니다. 

(함수 인자 복사를 제거)
std::string s로 넘겨지는 인자의 복사(pass by value)를 제거하기 위해
std::string const& s
로 변경하여 인자를 레퍼런스(pass by reference)로 받아
불필요한 임시객체 생성&복사를 방지하여 성능을 개선하려고 하였지만
s의 포인터 역참조(포인터의 값을 사용하기 위해 포인터가 가리키는 주소를 접근)로 인해 성능이 저하 되었습니다. 

std::string remove_ctrl_ref_args(std::string const& s) {
    std::string result;
    result.reserve(s.length());
    for (int i=0; i<s.length(); ++i) {
        if (s[i] >= 0x20)
            result += s[i];
    }
    return result; 
} 


(반복자로 포인터 역참조 제거)
역참조를 제거 하기 위해 s[i] 를 
iterator로 사용하여 성능을 개선할 수 있었습니다.
함께 loop을 반복할 때 마다 종료 조건에서의 간접 참조 비용을 줄이기 위해 
it != s.end() 대신 s.end()를 캐싱하여 it != end로 변경하여 성능을 개선합니다. 
  
std::string remove_ctrl_ref_args_it(std::string const& s) {
    std::string result;
    result.reserve(s.length());
    for (auto it=s.begin(),end=s.end(); it != end; ++it) {
        if (*it >= 0x20)
            result += *it;
    }
    return result; 
} 


(반환된 문자열 값의 복사 제거하기)
반환되는 std::string이 복사되게 되어
이를 생략하기 위해 함수의 참조 인자(std::string& result)로 변경하여 성능을 개선합니다.

마지막으로 속도가 중요한 경우에 사용할 수 있는
C 스타일 문자열을 사용하여 성능을 개선합니다. 

책에서는 성능 요약을 표로 정리했는데 릴리즈 빌드로 24.8 마이크로 걸리던 코드를 1.02 마이크로초로 줄이고 c 스타일 문자열을 사용하여 0.15 마이크로초까지 성능을 개선했다.

그 외 성능 개선을 위한 다른 방법들을 설명하고 있고  
다음의 방법으로 개선을 시도할 수 있다고 말하고 있음. 
- 더 좋은 알고리즘 사용
- 저 좋은 컴파일러 사용
- 더 좋은 문자열 라이브러리 사용
- 더 좋은 할당자 사용