C++ 프로그램 실행 효율 극대화 기법

C++에서 변수를 선언하면 해당 변수의 생성자가 호출되며, 변수의 스코프를 벗어날 때 소멸자가 호출됩니다. 복잡한 객체의 경우 이러한 생성 및 소멸 과정은 상당한 오버헤드를 유발할 수 있습니다. 따라서 불필요한 변수 선언을 최소화하는 것이 중요합니다. 아래 코드를 살펴보겠습니다.

#include <string>

std::string prepareGreeting(const std::string& name) {
    std::string greeting; // 생성자 호출
    if (name.empty()) {
        // 예외 발생 가능성
        throw std::runtime_error("Name cannot be empty.");
    }
    greeting = "Hello, " + name;
    return greeting; // 소멸자 호출
}

prepareGreeting 함수는 greeting이라는 문자열을 반환합니다. 만약 함수 내에서 예외가 발생하면, greeting 변수가 사용되지 않은 채로 함수가 종료될 수 있습니다. 이 경우 greeting의 생성 및 소멸 비용이 낭비됩니다. 더 나은 접근 방식은 다음과 같습니다.

#include <string>

std::string prepareGreetingOptimized(const std::string& name) {
    if (name.empty()) {
        // 예외 발생 가능성
        throw std::runtime_error("Name cannot be empty.");
    }
    std::string greeting = "Hello, " + name;
    return greeting; // 생성 직후 반환
}

이 방식은 greeting 변수가 필요한 시점에 생성되고, 함수가 정상적으로 반환될 때만 소멸되므로 불필요한 비용을 줄입니다.

루프 내에서 임시 변수를 사용하는 경우에도 효율성을 고려해야 합니다.

// 방식 A: 루프 외부 변수 사용
class Widget {
public:
    Widget() { /*...*/ }
    ~Widget() { /*...*/ }
    Widget(const Widget& other) { /* 복사 생성자 */ }
    void process() { /*...*/ }
};

void loopExampleA() {
    Widget w; // 1회 생성
    for (int i = 0; i < 10000; ++i) {
        w.process(); // n회 복사 또는 이동 발생 가능
    }
    // 1회 소멸
}

// 방식 B: 루프 내부 변수 사용
void loopExampleB() {
    for (int i = 0; i < 10000; ++i) {
        Widget w; // 10000회 생성
        w.process();
        // 10000회 소멸
    }
}

방식 A는 Widget 객체를 한 번 생성하고, 루프가 종료될 때 한 번 소멸합니다. 루프 내에서는 복사 또는 이동 연산이 발생할 수 있습니다. 방식 B는 루프를 반복할 때마다 Widget 객체를 새로 생성하고 소멸합니다. Widget의 생성 및 소멸 비용이 복사/이동 비용보다 크다면 방식 A가 더 효율적입니다. 반대의 경우라면 방식 B가 더 유리할 수 있습니다. 각 경우에 대한 성능 분석이 필요합니다.

타입 캐스팅 역시 프로그램 실행 효율에 영향을 미칩니다. 많은 개발자들이 타입 캐스팅이 단순히 컴파일러에게 타입 정보를 알려주는 것일 뿐, 런타임에 별도의 코드를 생성하지 않는다고 생각하지만, 이는 잘못된 인식입니다. 모든 타입 변환은 런타임에 실행될 코드를 필요로 합니다.

다음 클래스 계층 구조를 살펴보겠습니다.

#include <vector>
#include <memory>
#include <iostream>

class CBase {
public:
    virtual ~CBase() = default;
    virtual void func1() { std::cout << "CBase::func1()" << std::endl; }
};

class CSon1 : public CBase {
public:
    void func1() override { std::cout << "CSon1::func1()" << std::endl; }
};

class CSon2 : public CBase {
public:
    void func1() override { std::cout << "CSon2::func1()" << std::endl; }
};

class CSon3 : public CBase {
public:
    void func1() override { std::cout << "CSon3::func1()" << std::endl; }
};

class CSon4 : public CBase {
public:
    void func1() override { std::cout << "CSon4::func1()" << std::endl; }
};

위 예시에서는 CBase 클래스와 이를 상속받는 네 개의 파생 클래스가 있습니다. 각 클래스는 func1 멤버 함수를 가지고 있습니다. CBase 포인터를 저장하는 컨테이너를 사용하고 특정 파생 클래스의 함수를 호출하려는 상황을 가정해 보겠습니다.

#include <vector>
#include <memory>
#include <typeinfo> // dynamic_cast 사용 시 필요

void callFunc1FromBasePointers(const std::vector<std::shared_ptr<CBase>>& ptrs) {
    for (const auto& ptr : ptrs) {
        if (CSon1* son1_ptr = dynamic_cast<CSon1*>(ptr.get())) {
            son1_ptr->func1();
        }
    }
}

이 코드는 문법적으로는 올바르지만, dynamic_cast를 사용하여 각 요소를 검사하므로 컨테이너의 모든 요소를 순회하며 타입 변환을 시도하게 되어 실행 효율을 저하시킬 수 있습니다.

만약 컨테이너의 타입을 std::vector<std::shared_ptr<CSon1>>으로 지정하면 타입 변환이 필요 없지만, 다른 파생 클래스의 함수를 호출해야 할 경우 별도의 컨테이너를 관리해야 하는 불편함이 있습니다.

이러한 문제를 해결하기 위해 func1을 가상 함수(virtual function)로 선언하는 것이 효과적입니다.

// CBase와 파생 클래스 정의는 위와 동일

void callVirtualFunc1(const std::vector<std::shared_ptr<CBase>>& ptrs) {
    for (const auto& ptr : ptrs) {
        ptr->func1(); // 가상 함수 호출 (동적 바인딩)
    }
}

가상 함수를 사용하면 CBase 포인터를 통해 실제 객체 타입에 맞는 func1이 동적으로 호출되므로, dynamic_cast와 같은 명시적인 타입 변환이 필요하지 않아 성능 저하를 피할 수 있습니다.

결론적으로, 타입 캐스팅은 실행 효율을 저하시킬 수 있으므로, 불필요한 명시적 타입 변환을 피하고 가능한 경우 가상 함수와 같은 대체 메커니즘을 활용하는 것이 좋습니다.

태그: C++ 성능 최적화 객체 지향 가상 함수 타입 캐스팅

7월 27일 20:53에 게시됨