개발 팁

컴파일 타임 반복 (integer sequence, ford expression)

be_en 2025. 2. 12. 17:42

TLS를 활용한 메모리 풀을 만들었다.

 

메모리 풀의 내부 버킷의 크기를 어느정도로 하는 것이 좋을지 크기를 바꿔가며 측정하고 싶었다.

 

그러려면 메모리 풀의 내부 버킷의 수를 바꿔가면서 측정해야 하는데, 버킷 내부는 배열로 선언 되어 있다.

배열의 크기로는 상수 표현식만 들어갈 수 있기 때문에

템플릿의 비 타입 매개변수로 배열의 크기를 정해야 했다.

 

템플릿을 활용해서 버킷의 크기를 16 ~ 5120사이를 측정해보려고 했는데 이게 모든 크기를 복붙으로 하자니 너무 불편했다.

 

사실 시도를 조금 했었다.

Benchmark<16>();
Benchmark<32>();
Benchmark<48>();
Benchmark<64>();
Benchmark<80>();
Benchmark<96>();
Benchmark<112>();
Benchmark<128>();
Benchmark<144>();
Benchmark<160>();
Benchmark<176>();
Benchmark<192>();
Benchmark<208>();
Benchmark<224>();
Benchmark<240>();
Benchmark<256>();
Benchmark<272>();
Benchmark<288>();
Benchmark<304>();
Benchmark<320>();
Benchmark<336>();
Benchmark<352>();
Benchmark<368>();
Benchmark<384>();
Benchmark<400>();
Benchmark<416>();
Benchmark<432>();
Benchmark<448>();
Benchmark<464>();
Benchmark<480>();
Benchmark<496>();
Benchmark<512>();
Benchmark<528>();
Benchmark<544>();
Benchmark<560>();
Benchmark<576>();
Benchmark<592>();
Benchmark<608>();
Benchmark<624>();
Benchmark<640>();
Benchmark<656>();
Benchmark<672>();
Benchmark<688>();
Benchmark<704>();
Benchmark<720>();
Benchmark<736>();
Benchmark<752>();
Benchmark<768>();
Benchmark<784>();
Benchmark<800>();
Benchmark<816>();
Benchmark<832>();
Benchmark<848>();
Benchmark<864>();
Benchmark<880>();
Benchmark<896>();
Benchmark<912>();
Benchmark<928>();
Benchmark<944>();
Benchmark<960>();
Benchmark<976>();
Benchmark<992>();
Benchmark<1008>();
Benchmark<1024>();

 

 

다른 방법은 코드를 만들어주는 프로그램을 작성하는 방법이 있는데 이것도 할 수는 있겠지만 구현하기가 귀찮았다.

조금 우아한 방법이 없을까 하고 인터넷을 뒤져보다가 C++ 17부터 쓸 수 있는 integer_sequencefold expression을 활용하면 깔끔하게 문제를 해결할 수 있음을 알게 되었다.

 

fold expression

https://en.cppreference.com/w/cpp/language/fold

 

Fold expressions (since C++17) - cppreference.com

Reduces (folds) a pack over a binary operator. [edit] Syntax ( pack op ... ) (1) ( ... op pack ) (2) ( pack op ... op init ) (3) ( init op ... op pack ) (4) 1) Unary right fold. 2) Unary left fold. 3) Binary right fold. 4) Binary left fold. op - any of the

en.cppreference.com

 

예제 코드 따라 쳐본 것이다. (주석 설명)

#include <iostream>
#include <memory>

//* 폴딩 표현식 공부 예제

//가변 인자 템플릿이 나오기 전에는 아래와 같이 여러 버전의 함수를 오버로딩해야 했음
template <typename T>
auto Multiply(T a, T b)
{
    return a * b;
}

template <typename T>
auto Multiply(T a, T b, T c)
{
    return a * b * c;
}

template <typename T>
auto Multiply(T a, T b, T c, T d)
{
    return a * b * c * d;
}

//가변 인자 템플릿 등장 후에는 재귀적인 호출로 가변 인자를 처리할 수 있었음
//재귀 호출의 종료 조건
auto VariadicMultiply()
{
    return 1;
}

template<typename T1, typename...T>
auto VariadicMultiply(T1 t, T... ts)
{
    //재귀적인 호출
    return t * VariadicMultiply(ts...);
}

//폴드 표현식을 활용한 가변 인자 곱셈 (오버로드나 종료 조건이 딱히 없다. 아름답다...)
template <typename... Args>
auto FoldMultiply(Args... args)
{
    return (args * ... * 1);
    //     pack  op    op init
    // 4개의 인자가 들어왔다고 가정하면
    // p1 op (p2 op (p3 op (p4 op init)))로 변환된다!!
    // 
    //return (args * ...) 으로도 가능 => p1 * ( p2 * (p3 * p4))가 된다.
}

int main() 
{
    //가변 인자 템플릿 재귀로 구현한 방식
    std::cout << VariadicMultiply<int, int>(10, 10, 10, 10) << '\n';
    /*
        내부 동작
        VariadicMultiply<int, int>(10, 10, 10, 10);
        return 10 * VariadicMultiply(10, 10, 10);
        return 10 * 10 * VariadicMultiply(10, 10);
        return 10 * 10 * 10 * VariadicMultiply(10);
        return 10 * 10 * 10 * 10 * VariadicMultiply(); <- 종료 조건 도달 인자가 없는 함수는 1을 리턴 
        결과 10 * 10 * 10 * 10 * 1 = 10,000 출력
    */


    //폴드 표현식을 사용한 방식
    std::cout << FoldMultiply(10, 10, 10, 10) << '\n'; //Prints 10,000
    return 0;
}

integer_sequence

https://en.cppreference.com/w/cpp/utility/integer_sequence

 

std::integer_sequence - cppreference.com

template< class T, T... Ints > class integer_sequence; (since C++14) The class template std::integer_sequence represents a compile-time sequence of integers. When used as an argument to a function template, the parameter pack Ints can be deduced and used i

en.cppreference.com

 

integer_sequence는 utility 헤더에 정의되어 있다.

 

쉽게 말해 정수 시퀀스를 컴파일 타임에 만드는 도구이다.

 

컴파일 타임 반복

이 두 기능을 통해 컴파일 타임 반복을 수행할 수 있다 

 

내가 했던 방식은 다음 코드와 같다. 

 

template<int BucketSize>
void Benchmark()
{
	//테스트 코드
}

template<std::size_t... Is>
void runBenchmark(std::index_sequence<Is...>)
{
	//Fold Expression을 사용해 각 값에 대해 Benchmark를 호출
	(Benchmark<(Is + 1)> (), ...);
}

int main()
{
	//데이터 크기별 벤치마크 수행
	runBenchmark(std::make_index_sequence<MAX_BUCKET_SIZE>{});

	return 0;
}

 

runBenchmark 함수의 인자로 std::make_index_sequence<최대 버킷 크기>{}를 호출해서 index_sequence에 0 ~ 최대 버킷 크기 -1 까지의 정수 시퀀스를 전달한다.

 

그럼 Is라는 템플릿 파라미터 팩에 그 정수 시퀀스가 들어간다.

그 파라미터 팩을 폴드 표현식으로 언팩하며 Benchmark<Is + 1>()를 해서 0부터 최대 버킷 크기 -1만큼의 Benchmark 함수가 호출되도록 했다.

 

이 방식의 단점은 코드가 간결하다는 점이며, 단점은 최대 버킷 크기가 커질 수록 빌드 시간이 엄청 오래 걸린다. (컴파일 타임에 모든 버킷 크기에 대응되는 Benchmark 함수를 만들어야 하기 때문이다.)

 

아무튼 이렇게 해서 컴파일 타임 반복을 수행할 수 있었다.

 

템플릿하고는 별로 친하진 않지만

테스트 할 때 유용할 듯 하다.