본문 바로가기

Development

[프로그래밍 최적화 ②] C++ 프로그래밍 최적화 기법

1부 | 개발 환경의 변화와 대응하는 프로그래밍 최적화의 재발견
2부 | OPP적 개발을 위한 C++ 프로그래밍 최적화 기법
3부 | 리팩토링을 이용한 자바 성능 최적화 기법
4부 | 성능 이슈 해결을 위한 닷넷 프로그래밍 최적화 기법
5부 | ARM과 파워pc에 기반한 임베디드 프로그래밍 최적화 기법
C+ +에 대한 이야기를 지면에 실어 나르자면 몇 백 페이지에 걸쳐서 써도 모자랄 것이다. 객체 지향적 프로그램 기법부터 C 영역의 포인터에 이르기 까지 하고 싶은 말도, 하지 못할 말도 많은 것이 C++이다. 감히 누가 여러 개의 머리와 수십 개의 팔다리가 달린 C++이란 괴물 언어를 불과 몇 페이지에 담을 수 있겠는가. C++을 사용하는 그 많은 프로그래머들만큼이나 많은 이야기가 담긴 것이 C++일 것이다. 필자는 C++를 객체 지향적 개념의 몇 가지 이슈를 가지고 이야기해 볼 것이다.

며칠 전에 필자의 여자 친구로부터 클래스(Class)와 스트럭처(Structure)의 차이에 대한 질문을 받았다. 필자의 대답은‘어떤 스트럭처에서요?’라는 질문 한마디였다. 클래스와 스트럭처의 차이는 무엇인가?

다들 잘 알고 있겠지만 일단 C의 스트럭처와 C++의 스트럭처는 다르다. 그도 그럴 것이 C++의 스트럭처는 C와는 다르게 Constructor와 같은 Class Abstraction의 필수 요소를 지원하며, 스트럭처 안에 함수를 바인딩 할 수 있다.

그럼 C++에서의 스트럭처와 클래스의 차이는 무엇인가. 사실 차이가 없다. 단지 멤버 선언 때 클래스의 기본 값(Default)은 Private이고 스트럭처는 Public이라는 것 외에는 다른 것이 없다.

적어도 언어적인 측면에서는 그렇다. 독자 여러분이 필자라면 같은 컴퓨터를 전공하는 여자 친구에게 단지 차이가 없다고 대답하겠는가? 물론 아니다가 정답일 것이다.

마찬가지 이유에서 C++의 최적화에서는 포인터의 이용, 코드 사이즈 등 언어 이용적인 측면만을 고려 할 수 없다. 왜? OOP를 다루는 언어는 적어도 OOP에 맞게 쓰는 것이 가장 큰 비중을 가지며 언어는 객체지향적 패러다임을 구현하는 하나의 도구에 불과 하기 때문이다. 여기에서는 바로 그런 관점에서 C++을 살펴볼 것이다.

  C++ 프로그래머 VS C 프로그래머

다음 질문에 대해 생각해 보자.‘ C로 구현하는 객체 지향은 불가능한가?’잠시 책을 덮고 생각한 뒤에 다음 글을 읽어도 괜찮다. 우리는 개발자와 술자리에서 이런 화제를 가지고 밤 새워 가며 목에 핏대를 세우는 일이 허다하다. C로 C++처럼 객체 지향적 개발을 하는 것은 불가능 한가?

물론 화려한 OOP는 어렵겠지만 잘 구조화된 C문법으로도 충분히 표현 가능하다. 상위 영역의 Object C로 제작하는 GTK+ 같은 곳에서 C++과 유사한 추상화를 가진 아키텍처를 쉽게 만날 수 있다. 또한 C의 전형적인 영역인 임베디드 소프트웨어에서도 구조체와 함수 포인터들을 이용하여 C++을 흉내 낸 추상화 코드들을 간혹 경험할 수있다.

스트라우스트럽이 C++을 만들기 시작할 때에도‘C with data abstraction’라는 프로토타입 언어로부터 시작 되었던 만큼 C와 C++ 언어의 영역 차이는 애매하다. 이러한 언어를 사용하는 프로그래머도 그 차이를 명확히 구분하기 어려울 수밖에 없다.

C++은 C의 불편한 점을 보완해주는 언어도 아니며 C보다 좀 더 고급 기술을 구사하는 도구도 아니다. 더욱이 현업에서 모든 문제를 한방에 해결 해주는 은 탄환(Silver bullet) 같은 존재는 더 더욱 아니다.

그럼에도 불구하고 우리는 왜 C++을 사용해야 하는가? 그것은 문제 해결법이 C와는 판이하게 다르기 때문이다. 이것은 C와 C++의 언어학적 문제를 벗어난다. C 프로그래머와 C++ 프로그래머의 차이는 어떤 언어를 사용하는가가 아니라 어떠한 관점에서 문제를 해결 하는 가이다.

아무래도 하드웨어 개발자들이 주류를 이루는 필자의 현업에서는 가끔 개발 도중에 C언어를 자유자제로 구현하는 개발자가 ‘C++ 그거 하루면 하는데 뭘?’이라는 얘기를 들을 때 마다 마음이 아프다. C++을 하루(?) 만에 하였다면 클래스를 쓰지 말고 잘 구조화 된 C를 쓰면 된다.

구조체에 각 메소드가 될 함수 포인터들을 선언하고 생성자가 될 멤버 변수를 넣어 이 함수 포인터들을 초기화 해주고 나서 전역에 존재 하는 new라는 함수를 만든다. 다시 이 함수가 각각의 구조체를 할당 받게 하면 그런 이야기를 하는 개발자들이 사용하는 클래스를 흉내 낸 C를 쉽게 만들 수 있다.


 <리스트 1> Class를 흉내 낸 C코드



물론 추상화 할 수 있는 범위가 제한적이고 불필요한 코드가 들러붙기는 하지만 분명 클래스와 유사한 행동을 하도록 처리 할수 있다. 특히 앞서 소개한 GTK+이나 brew등의 코드를 보면 이러한 기법들은 빈번하게 적용 되어 있는 것을 확인할 수 있다.

C의 구조체와는 다르게 C++에서는 가상 함수 테이블을 각 클래스 마다 가진다. 이러한 것들을 이용한 객체 지향적인 기법인 C++에서 다형성(Ploymorphism), 동적 바인딩(Dynamic binding), 클래스 상속(Inheritance)을 뺀다면 그것은 스트럭처와 같이 발라낸 자료 추상화 관점의 C++에 불과 하다. 다시 정리하자면, C++은 단지 OOP 패러다임을 구현 할 수 있게 해주는 언어적인 도구 일뿐이지 C++그 자체가 의미 있는 것은 아니다.

  시스템 해석학적 관점에서 본 C와 C++의 차이

객체 지향적 문제 해결법에 들어가기 전에 시스템 해석학적 관점에서 이를 관찰 해보자. C로 구현된 함수 모듈과 C++로 구현된 함수 모듈은 어떤 차이를 가질까? 우리는 <그림 1>, <그림2>와 같은 블록 개념의 다이어그램을 생각해볼 수 있다.

<그림 1> C 관점의 시스템

<그림 2> C++ 관점의 시스템

어떤 차이를 볼 수 있는가? C의 관점에서는 각각의 데이터들이 노드를 이루고 이것들이 다른 상태의 데이터로 이동할 때 링크를 함수로 만들고 있다. 하지만 C++관점에서 보면, 클래스라는 추상화 관점의 모듈이 노드를 이루고 각각의 관계가 링크를 이루게 된다. 이 둘은 큰 차이를 가져 온다.

흔히 시스템의 복잡도를 논하는 두 가지 중 하나는 시스템을 구성하는 컴포넌트 수이고 다른 하나는 그 구성원을 연결하는 링크의 수이다.

컴포넌트의 수는 n개로 상수에 비례하여 증가하지만 이 컴포넌트를 연결하는 링크인 함수들은 n개에서 2개를 선택하는 조합과 같으므로 n(n-1)/2 와 같다. 이 복잡도는 O(n2)으로 시스템이 커질수록 알고리즘의 복잡도를 제어하기 힘들어 진다.

사실 C언어로 작성하든 C++로 작성하든 결국 컴파일 하고 나면 기능과 데이터만 존재 하는 바이너리 파일에 불과하다. 이러한 복잡도 제어는 C와 C++ 패러다임의 적용이 시스템 해석학적 입장에서 봤을때 얼마나 복잡해지는지를 알 수 있다.

  객체지향 문제 해결법

객체지향(Object Oriented)의 문제 해결법에 대해 알아보기에 앞서 다음의 함수를 잠시 살펴보자.

inline int square(int a) { return a *= a; }
inline float square(float a) { return a *= a; }

물론 이 함수를 보면서 Template을 통한 구현을 생각 하거나 Function pointer를 사용한 Generic Function을 생각 할 수도 있다. 하지만 필자가 이야기 하려는 것은 조금 다른 이야기다. square 함수의 정의역(Domain)과 치역(Range)은 무엇인가? 정의역은 Integer나 float형의 숫자이고 치역도 마찬가지로 숫자이다.

static int factorial(int i)
{
if (i<2) return 1;
return i*factorial(i-1);
}

이 팩토리얼 코드에서의 공리(Axiom)는 무엇 인가. If (i<2) return 1; 쯤 될 것이다. 우리는 흔히 C언어를 라이프 니찌가 정의한 수학의 함수를 모방하고 있다고 생각할 수 있다.

프로시져(Procedure)라는 이름으로 함수와는 달리 프로세스 중심의 상태 변화를 나타내는 함수들도 존재하지만 그 또한 치역이 void인 함수일 뿐이다. 수학에서 1:1, n:1, n:n은 함수에 들어가더라도1:n은 함수가 아니다. 여러분의 코드 중에 int float double Squre(int a)라는 함수가 없는 것과 마찬가지다.

그렇다면 소프트웨어를 개발한다는 것은 본질적으로 어떤 의미를 가지는가? 실제 세계의 정보나 현상들 중 관심 있거나 구현되어야 하는 부분만을 시스템으로 옮기는 시뮬레이션이 가장 큰의미를 가질 것이다. 수학 또한 실세계를 매핑하는 것의 일종인 언어라고 볼 수 있으니 말이다. 다만 수학으로 증명되지 않는 것이 얼마나 많은가?

우리는 윈도우에서 사용자가 클릭하고 입력하는 것을 정의역(Domain)으로, 화면의 출력 내용을 치역으로 불 수 있다. 이러한 사용자의 움직임을 수학적인 함수로만(수학적으로 증명되는 사실보다 증명하지 못하는 사실이 훨씬 많다) 정의하기에는 배보다 배꼽이 더 큰 것처럼 오버헤드가 들며, 이러한 행동들을 수학적 증명으로 표현하기에는 무리가 있다.

우리는 이 같은 실세계를 컴퓨터 내부로 반영하기 위하여 각각의 개체들을 클래스의 속성(Attribute)과 행동(Method)들로 정의하고 이들의 관계를 통해 좀 더 편리하게 실세계를 추상화 하고 복사 반영할 수 있을 것이다.

특별한 수학적 증명 없이도 우리 눈으로 보고 느낀 것을 추상화에 이용할 수 있고 그러한 추상화 단위들이 엮어져서 하나의 프로그램이 된다. 그 덕에 좀 더 많은 분야의 사람들이 프로그램이라는 것을 할 수 있게 되었다. 이러한 객체 지향적 문제 해결법에서 중요한 것은 개체들의 동등함이다.

우리는 간혹 코드에서 전역 변수나 전역 함수 등을 보게 된다. 이러한 개체들은 클래스로 구현된 내용들과 동등한가? 클래스의 속성과 행동은 그 클래스 안에 종속적이다. 생각해보면 당연한 것이다. 사람이라는 클래스를 제작할 때 사람이 먹고 입고 잔다는 행동과, 내부적으로 생각, 사고, 성별 등을 가지는 것은 사람에게만 종속적인 것이다.

이러한 행동들이 다른 여타 동물, 사물 등의 행동들과 엮이면서 상태가 변화하는 것인데, 앞에서 말한 전역 변수나 전역 함수를 살펴보라. 이 둘은 죄악인 코드이다(물론 현업에서 이러한 코드를 남발 하기도 한다).

모두 클래스가 동등한데 이 둘만 신(