为什么 consteval/constexpr 和模板元函数之间的编译时间差异如此之大?
Why is there such a massive difference in compile time between consteval/constexpr and template metafunctions?
我很好奇就编译时评估而言,我可以将 gcc 推到什么程度,所以我让它计算 Ackermann 函数,特别是输入值为 4 和 1(任何高于此值的值)不切实际):
consteval unsigned int A(unsigned int x, unsigned int y)
{
if(x == 0)
return y+1;
else if(y == 0)
return A(x-1, 1);
else
return A(x-1, A(x, y-1));
}
unsigned int result = A(4, 1);
(我认为递归深度限制在 ~16K 但为了安全起见我用 -std=c++20 -fconstexpr-depth=100000 -fconstexpr-ops-limit=12800000000
编译了它)
毫不奇怪,这会占用大量堆栈 space(事实上,如果 运行 默认进程堆栈大小为 8mb,它会导致编译器崩溃)并且需要几分钟时间计算。然而,它最终确实到达了那里,所以显然编译器可以处理它。
之后我决定尝试使用模板、元函数和部分专业化模式匹配来实现 Ackermann 函数。令人惊讶的是,下面的实现只需要几秒钟就可以评估:
template<unsigned int x, unsigned int y>
struct A {
static constexpr unsigned int value = A<x-1, A<x, y-1>::value>::value;
};
template<unsigned int y>
struct A<0, y> {
static constexpr unsigned int value = y+1;
};
template<unsigned int x>
struct A<x, 0> {
static constexpr unsigned int value = A<x-1, 1>::value;
};
unsigned int result = A<4,1>::value;
(用-ftemplate-depth=17000
编译)
为什么评估时间会有如此大的差异?这些本质上不是等价的吗?我想我可以理解需要更多内存和评估时间的 consteval
解决方案,因为从语义上讲它由一堆函数调用组成,但这并不能解释为什么这个完全相同的(非consteval)函数在 运行时间只比元功能版本稍长(编译时未优化)。
为什么 consteval
这么慢?我几乎想得出结论,它正在由 GIMPLE 解释器或类似的东西进行评估。另外,元功能版本怎么会这么快?它实际上并不比优化的机器代码慢多少。
在 A
的模板版本中,当一个特定的特化,比如 A<2,3>
,被实例化时,编译器会记住这个类型,并且永远不需要再次实例化它。这是因为类型是唯一的,每次“调用”这个元函数只是计算一个类型。
consteval
函数版本未针对此进行优化,因此 A(2,3)
可能会被多次计算,具体取决于控制流,从而导致您观察到的性能差异。没有什么可以阻止编译器“缓存”函数调用的结果,但这些优化可能还没有实现。
我很好奇就编译时评估而言,我可以将 gcc 推到什么程度,所以我让它计算 Ackermann 函数,特别是输入值为 4 和 1(任何高于此值的值)不切实际):
consteval unsigned int A(unsigned int x, unsigned int y)
{
if(x == 0)
return y+1;
else if(y == 0)
return A(x-1, 1);
else
return A(x-1, A(x, y-1));
}
unsigned int result = A(4, 1);
(我认为递归深度限制在 ~16K 但为了安全起见我用 -std=c++20 -fconstexpr-depth=100000 -fconstexpr-ops-limit=12800000000
编译了它)
毫不奇怪,这会占用大量堆栈 space(事实上,如果 运行 默认进程堆栈大小为 8mb,它会导致编译器崩溃)并且需要几分钟时间计算。然而,它最终确实到达了那里,所以显然编译器可以处理它。
之后我决定尝试使用模板、元函数和部分专业化模式匹配来实现 Ackermann 函数。令人惊讶的是,下面的实现只需要几秒钟就可以评估:
template<unsigned int x, unsigned int y>
struct A {
static constexpr unsigned int value = A<x-1, A<x, y-1>::value>::value;
};
template<unsigned int y>
struct A<0, y> {
static constexpr unsigned int value = y+1;
};
template<unsigned int x>
struct A<x, 0> {
static constexpr unsigned int value = A<x-1, 1>::value;
};
unsigned int result = A<4,1>::value;
(用-ftemplate-depth=17000
编译)
为什么评估时间会有如此大的差异?这些本质上不是等价的吗?我想我可以理解需要更多内存和评估时间的 consteval
解决方案,因为从语义上讲它由一堆函数调用组成,但这并不能解释为什么这个完全相同的(非consteval)函数在 运行时间只比元功能版本稍长(编译时未优化)。
为什么 consteval
这么慢?我几乎想得出结论,它正在由 GIMPLE 解释器或类似的东西进行评估。另外,元功能版本怎么会这么快?它实际上并不比优化的机器代码慢多少。
在 A
的模板版本中,当一个特定的特化,比如 A<2,3>
,被实例化时,编译器会记住这个类型,并且永远不需要再次实例化它。这是因为类型是唯一的,每次“调用”这个元函数只是计算一个类型。
consteval
函数版本未针对此进行优化,因此 A(2,3)
可能会被多次计算,具体取决于控制流,从而导致您观察到的性能差异。没有什么可以阻止编译器“缓存”函数调用的结果,但这些优化可能还没有实现。