SW 그리고 아빠 엔지니어링 중...

아는 만큼 보이고, 경험해봐야 알 수 있고, 자꾸 써야 내 것이 된다.

L C++

🚀성능 최적화 도구 - constexpr과 inline 함수 이해하기

보리남편 김 주부 2025. 3. 21. 01:04
🗨️ 프로그래밍에서 성능 최적화는 매우 중요한 주제이며 지금 얘기하는 내용들은 체감할 정도의 성능개선이 아니다. 하지만 constexpr과 inline 이 두 키워드의 의미와 사용법에 대해 알게 된다면 기계어 동작 간소화오버헤드를 절약하는 이점을 취하고, 기존 레거시 코드에 constexpr과 inline 도구를 잘 못 사용하여 성능을 저하시키고 있는 문제를 개선하게 될지도 모른다. 🤓

 

constexpr: 컴파일 시간의 마법


📜 C++ core guidelines
 F.4: 함수가 컴파일 시간에 평가되어야 한다면 constexpr로 선언하라

 

constexpr란?

📌 constexpr 발음을 어느 교수님께 배우느냐에 따라 조금씩 다르게 알던데 저는 뭐 '콘스트 익스퍼'라로 부르고 있다.

 

constexpr은 C++11에서 도입된 키워드로, '상수 표현식(constant expression)'의 줄임말이다. 이름에서 알 수 있듯이 변수 및 함수를 상수로 표현한다는 뜻이며, 특징으로는 일반적인 함수는 런타임에 실행되지만, constexpr 함수는 컴파일 시간에 그 값이 결정될 수 있다.

 

'결정된다'가 아니라 '결정될 수 있다?' 🤔

결정된다가 아니라 결정될 수 있다는 이유를 예시를 통해 알아보자.

두수 중 큰 값을 리턴하는 constexpr 함수 코드
// 두 수 중 큰 값을 return 하는 템플릿 max 함수
template<typename T>
T max(T lhs, T rhs)
{
	return lhs < rhs ? rhs : lhs;
}

// 두 수 중 큰 값을 return 하는 템플릿 constexpr max 함수
template<typename T>
constexpr T max_constexpr(T lhs, T rhs)
{
	return lhs < rhs ? rhs : lhs;
}

int main()
{
	int ret1 = max(1, 2); // max 함수 호출

	// 컴파일 시간에 계산됨
	int ret2 = max_constexpr(1, 2);	  // 2로 컴파일됨

	// 런타임에도 사용 가능
	int a,b;
	int ret3 = max_constexpr(a, b); // 하지만 결과는?
}
 

 

둘다 이상 없이 실행은 되지만, max 함수와 max_constexpr 함수에 상수를 넣은 경우와 변수를 넣은 결과를 기계어로 비교해 보자.

 

max 함수 : max 함수가 호출되어 처리한다.

 

max_constexpr(1,2) : 컴파일에서 계산된 2의 값을 넣는 것으로 끝!

 

하지만 max_constexpr(a,b) : max 함수와 동일하게 함수가 호출되어 처리된다.

 

예시에도 알 수 있듯이 constexpr 함수는 실행되어도 컴파일타임에서 결과를 알 수 없다면 constexpr로 동작하지 않는다.

 

이외에도 버전별 제약사항을 정리하면 아래와 같다.

 

constexpr 함수의 제약 사항

  • C++11: 단일 return 문만 허용
  • C++14: 여러 문장, 지역 변수, 루프, 조건문 허용
  • C++17: if constexpr, 람다 표현식에서의 constexpr 지원
  • C++20에서는 훨씬 더 많은 기능을 constexpr에서 사용할 수 있게 되었다고 한다.
🗨️ 사실 나는 C++11 수준의 코드만 사용하고 있었는데, 리서치하면서 점점 발전해 오고 있음을 알게 되었다.
claude.ai에게 constexpr함수의 제약사항 완화 변천사를 물어봤는데 관심 있으면 더보기로 확인해 보자.

 

더보기

constexpr 함수의 제약 사항 완화 변천사

C++11에서 처음 소개된 constexpr 함수는 많은 제약이 있었지만, 이후 C++ 표준이 발전하면서 점차 완화되었습니다. 각 버전별 주요 개선 사항을 예시 코드와 함께 설명해 드리겠습니다.

 

C++11: 초기 constexpr 함수의 제약

C++11에서 constexpr 함수는 매우 제한적이었습니다:

  • 함수 본문에 단일 return 문만 허용
  • 지역 변수, 루프, 조건문 불가
  • 제한된 표현식만 사용 가능
// C++11에서의 constexpr 함수 예시
constexpr int factorial(int n) {
    return n <= 1 ? 1 : n * factorial(n - 1);  // 재귀는 가능하지만 단일 return문만 허용
}

// 컴파일 시간에 계산
constexpr int result = factorial(5);  // 120

 

C++14: 주요 제약 완화

C++14에서는 제약 사항이 크게 완화되었습니다:

  • 다중 문장 허용
  • 지역 변수 사용 가능
  • 루프와 조건문 허용
  • 단, 가변 크기 배열, try-catch, goto 등은 여전히 금지
// C++14에서 가능해진 constexpr 함수
constexpr int factorial(int n) {
    int result = 1;  // 지역 변수 허용
    for (int i = 1; i <= n; ++i) {  // 루프 허용
        result *= i;
    }
    return result;
}

// if문 사용 예시
constexpr int max_value(int a, int b) {
    if (a > b)  // 조건문 허용
        return a;
    return b;
}

 

C++17: 추가 제약 완화

C++17에서는 constexpr 함수 내에서 더 많은 기능을 사용할 수 있게 되었습니다:

  • if constexpr 도입 (컴파일 타임 조건부 컴파일)
  • 람다 표현식에서 constexpr 사용 가능
// C++17: if constexpr 사용 예시
template <typename T>
constexpr auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>) {
        return *t;  // T가 포인터 타입일 때만 컴파일
    } else {
        return t;   // T가 포인터가 아닐 때만 컴파일
    }
}

// C++17: constexpr 람다
constexpr auto squared = [](int n) constexpr {
    return n * n;
};

constexpr int result = squared(4);  // 16

 

C++20: 가상 함수 및 try-catch 지원

C++20에서는 더 많은 제약이 완화되었습니다:

  • constexpr 가상 함수 허용
  • try-catch 허용
  • 동적 메모리 할당(new/delete) 사용 가능
  • std::vector, std::string 등 일부 표준 라이브러리 컨테이너의 constexpr 지원
// C++20: constexpr 가상 함수
struct Base {
    constexpr virtual int getValue() const { return 1; }
};

struct Derived : Base {
    constexpr int getValue() const override { return 2; }
};

constexpr int getBaseValue(const Base& b) {
    return b.getValue();  // 컴파일 타임에 가상 함수 호출
}

// C++20: constexpr try-catch
constexpr int divide(int a, int b) {
    try {
        if (b == 0)
            throw "Division by zero";
        return a / b;
    } catch (...) {
        return 0;
    }
}

// C++20: constexpr 동적 메모리 할당
constexpr auto make_array() {
    auto* arr = new int[3]{1, 2, 3};
    int sum = arr[0] + arr[1] + arr[2];
    delete[] arr;
    return sum;
}

constexpr int array_sum = make_array();  // 6

 

C++23: 추가 개선 사항

C++23에서는 다음과 같은 기능들이 추가되었습니다:

  • constexpr에서 thread_local 변수 사용 가능
  • constexpr 함수에서 std::unique_ptr, std::shared_ptr 등의 스마트 포인터 사용 가능
  • std::expected, std::optional의 constexpr 지원 확대
// C++23: constexpr 스마트 포인터
constexpr int use_smart_pointer() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    int value = *ptr;
    return value;
}

constexpr int smart_ptr_value = use_smart_pointer();  // 42

// C++23: constexpr std::expected 사용
constexpr std::expected<int, std::string> safe_divide(int a, int b) {
    if (b == 0)
        return std::unexpected("Division by zero");
    return a / b;
}

constexpr auto result = safe_divide(10, 2);  // contains 5

 

🗨️ 필살기🧨는 아무 때나 쓸 수 없지만 강력한 한방이 있듯이 제약사항을 고려하여 아래의 장점을 코드 속에 녹여내자.😁

 

constexpr의 장점

  1. 컴파일 타임 검증: 컴파일 시 계산되므로 일부 오류를 미리 발견할 수 있다.
  2. 실행 시간 단축: 계산이 컴파일 시간에 이루어지므로 런타임 오버헤드가 없다.
  3. 메모리 사용 최적화: 상수로 계산된 값은 런타임에 다시 계산할 필요가 없다.

inline: 함수 호출 오버헤드 제거하기


📜 C++ core guidelines
F.5: 함수가 매우 짧고 수행시간이 중요하다면 inline으로 선언하라

 

inline이란 무엇인가?

inline 키워드는 컴파일러에게 함수 호출 대신 함수의 본문을 직접 삽입하도록 제안하는 키워드이다.

 

예시로 내용을 좀 더 알아보자.

inline 함수 예시
// 간단한 inline 함수
inline int max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    int x = 5, y = 10;
    int larger = max(x, y);  // 컴파일러가 "int larger = (x > y) ? x : y;"로 대체할 수 있음
    
    return 0;
}

 

일반함수라면 max 함수로 점프해야 하지만 아래코드처럼 max 함수의 수행이 main 함수 안에서 동작이 된다.

 

 

이로 인한 이점을 정리해 보면 다음과 같다.

 

inline의 장점

  1. 함수 호출 오버헤드 제거: 스택 프레임 설정, 매개변수 전달, 반환 값 처리 등의 오버헤드를 절약할 수 있다.
  2. 추가 최적화 기회: 함수 본문이 호출 지점에 직접 삽입되면 컴파일러가 더 많은 최적화를 수행할 수 있다.
  3. 헤더 파일에 함수 정의 가능: inline 함수는 여러 번 정의되어도 링커 오류 - ODR(One Definition Rule) 위반을 방지한다.

반대로 inline에 대한 오해를 부가적으로 정리하면 다음과 같다.

inline에 관한 오해와 진실

  1. 오해: inline 키워드를 사용하면 항상 함수가 인라인화된다.
    • 진실: inline은 단지 제안일 뿐, 컴파일러가 최종 결정을 내린다.
  2. 오해: 인라인 함수는 항상 더 빠르다.
    • 진실: 너무 큰 함수를 인라인화하면 코드 크기가 증가하여 캐시 효율성이 떨어진다.
  3. 오해: 모든 작은 함수는 inline으로 선언해야 한다.
    • 진실: 현대 컴파일러는 inline 키워드 없이도 함수를 자동으로 인라인화하고 있다. (오히려 ODR 위반 방지용으로 더 사용한다.)

 

constexpr vs inline 차이 정리

 

비스한 듯 다른 두 키워드는 모두 성능 최적화와 관련 있지만, 다음과 같은 차이점이 있다.

특성 constexpr inline
주요 목적 컴파일 시간 계산 함수 호출 오버헤드 제거
계산 시점 컴파일 시간(가능한 경우) 런타임
ODR 위반 방지 가능 가능
상수 표현식에서 사용 가능 불가능

 

흥미로운 점은 constexpr 함수가 암시적으로 inline이라는 것이다. 즉, constexpr 함수는 컴파일 시간 계산과 함수 인라인화의 장점을 모두 가지고 있다. 

 

알아서 컴파일에 판단하고 동작을 한다면 모든 함수에 constexpr로 만들면 되지 않을까? 🤔

이렇게 하면, 컴파일에서 분석과 최적화 작업을 하느라 컴파일 시간이 증가한다. 그리고 오류 발생 시 복잡하고 이해하기 어려운 오류 메시지가 생성될 수도 있다. 그래서 C++ Core guidelines는 이렇게 권고를 하고 있는 것이다.

📜 C++ core guidelines
F.4: 함수가 컴파일 시간에 평가되어야 한다면 constexpr로 선언하라
F.5: 함수가 매우 짧고 수행시간이 중요하다면 inline으로 선언하라

 

각각 어떤 상황에서 쓰면 좋을지를 상세히 정리하면 다음과 같다. 

constexpr 사용:

  • 컴파일 시간에 결과를 알 수 있는 수학적 계산
  • 상수 테이블 초기화
  • 타입 특성(type traits) 구현
  • 메타프로그래밍
  • 템플릿 매개변수 계산

inline 사용:

  • 매우 작고 자주 호출되는 함수
  • 헤더 파일에 정의해야 하는 함수
  • 함수 템플릿 구현
  • 성능이 중요한 코드 경로에 있는 간단한 함수

 

이 내용들 중 헤더 파일에 정의해야 하는 함수 예시는 참고 바란다.

 

constexpr과 inline의 활용 예시

헤더 전용 라이브러리 구현
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

namespace math {
    // 헤더에 inline 함수 정의
    inline double square(double x) {
        return x * x;
    }
    
    // 컴파일 시간 계산이 가능한 constexpr 함수
    constexpr double pi() {
        return 3.14159265358979323846;
    }
    
    // constexpr 및 inline 함수
    constexpr double circle_area(double radius) {
        return pi() * square(radius);
    }
}

#endif // MATH_UTILS_H

 

이 헤더 파일은 구현 파일(.cpp) 없이도 여러 소스 파일에서 포함하여 사용할 수 있다.


🗨️ 마치며, constexpr과 inline은 C++ 프로그래밍에서 성능 최적화를 위한 강력한 도구이다.  각각의 키워드가 가진 특성과 목적을 이해하여 좀 더 효율적이고 빠른 코드를 작성하길 바란다.

 

참고


C++ 핵심 가이드라인 한글화 프로젝트 링크

 

C++ Core Guidelines 원문 링크

 

728x90