C++ 使用递归模板函数是否会带来函数调用开销,或者编译器在大多数情况下都会将其内联(下面的示例)?
我一直在努力理解TMP的实际用途。我看到很多代码都是这样的:C++ 使用递归模板函数是否会带来函数调用开销,或者编译器在大多数情况下都会将其内联(下面的示例)?,c++,recursion,c++14,template-meta-programming,C++,Recursion,C++14,Template Meta Programming,我一直在努力理解TMP的实际用途。我看到很多代码都是这样的: #ifndef LOOP2_HPP #define LOOP2_HPP // primary template template <int DIM, typename T> class DotProduct { public: static T result (T* a, T* b) { return *a * *b + DotProduct<DIM-1,T>::result(
#ifndef LOOP2_HPP
#define LOOP2_HPP
// primary template
template <int DIM, typename T>
class DotProduct {
public:
static T result (T* a, T* b) {
return *a * *b + DotProduct<DIM-1,T>::result(a+1,b+1);
}
};
// partial specialization as end criteria
template <typename T>
class DotProduct<1,T> {
public:
static T result (T* a, T* b) {
return *a * *b;
}
};
// convenience function
template <int DIM, typename T>
inline T dot_product (T* a, T* b)
{
return DotProduct<DIM,T>::result(a,b);
}
似乎每次展开都会导致一个运行时调用指令。这就是我想要避免的成本。我只希望最终生成的代码是多个cout的串联
总是显式地内联这种高度递归的函数是一种好的做法吗
由于我们已经使用constexpr很多年了,现在还需要用MTP模板化这些函数吗?即使constexpris还不够,我们也希望c++20中有consteval
内联只是让编译器有机会优化代码,但这并不是保证。使其成为递归模板函数使编译器有机会将内存浪费在非内联递归模板实例上,这与您想要实现的相反!如果使用-O0编译,您会看到从示例生成的大量代码
您可以强制编译器在编译时生成结果,例如,只要可以将结果值用作模板参数
但与往常一样,关于优化:
1尝试获得最佳算法
2.尝试实现使代码可维护
3措施
4措施
5措施
只有当你的代码不能满足你的速度要求时,才开始手工优化
事实上,您的代码有可能浪费大量内存,也有可能进行优化。但您应该使用constexpr函数,而不是使用或多或少不可读的MTP代码。所以内联只是问题的一小部分
你的编译器比你想象的要好!正常地如果你不信任:测量!只有当你看到一个真正的问题:手工优化
如果使用constexpr,尤其是递归函数,大多数编译器都会提供命令行标志,以便在不强制编译器在编译时使用结果作为模板参数或任何其他必须是编译时常量(如数组大小)的情况下,提供更深入的编译时计算级别。这取决于您使用的编译器,因此请阅读手册
如果在递归/循环中使用std::cout,您将永远不会看到单个输出优化。但无论如何:如果您有足够的时间使用std::cout,您就不必考虑它周围的几行汇编。与生成应该写入控制台的数据的代码相关,std::cout通常比较慢
不要优化错误的东西
附加内容:
如果您真的想从整数列表生成编译时字符串,您可以将其作为示例的基础:1在运行时成本方面,模板递归函数与非模板递归函数应该如何不同?编译后的代码中没有模板。模板只是编译时的概念。2您是否查看/调查了生成的程序集?它与您的期望有什么不同?标题中的问题与问题中的问题略有不同。@AlgirdasPreidžius问题在于它不是真正的递归。在每个递归级别中调用不同的函数。深度在编译时已知。@GuillaumeRacicot更正确的说法是,在实例化模板时,递归发生在编译时,而在运行时,它只是一组相互调用的函数。为了防御,后来添加了递归标记,而且标记描述集中在函数调用的递归上,很少提到其他类型的函数调用,这是一个缺陷recursion@formerlyknownas_463035818在某些情况下,您需要将模板函数标记为内联,例如越界成员函数模板或显式专门化。当然,现在不是这样,但不是说你永远不需要内联模板函数。我试图理解什么是在编译时展开东西的好方法,有点像C宏。cout只是一个非纯函数的例子,我想多次调用它。也许我可以让compile帮我写,而不是写难看的重复语句。@AbhishekKumar:正如所说的:MTP是一回事,递归constexpr函数是另一种得到相同结果的方法。但重要的是要真正了解编译器将生成什么,并了解如何强制编译器在编译时内联和计算结果。谢谢您的回答。下面的文章似乎建议使用MTP-展开循环。我尝试了那里显示的确切代码,它似乎仍然生成多个调用指令。如果你正在进行函数调用,那么它不是破坏了循环展开的整个点吗?@AbhishekKumar:这是预期的结果:如果你有一个循环,你只能看到一个调用和循环的代码。如果编译器展开,将得到一系列调用,而不是一个循环。
那么这里有什么出乎意料的呢?您将能够在编译时根据我提供的链接计算示例输出的完整字符串。但是,嘿,为什么?您已经了解了MTP循环的工作原理,仅此而已;顺便说一句:如果你想感谢我,请投票,如果答案似乎是正确的,请接受;我试图避免的调用是从f调用void f。f和f再次调用std::basic_stream这将是循环展开中唯一的函数调用。MTP版本与编译器循环展开的不同之处在于,它执行一个循环的工作,然后调用一个函数,该函数再次执行一个循环的工作并调用另一个函数,而不是将各个循环体连接在一起。
template <int N>
inline void f() {
f<N-1>();
std::cout << N << "\n";
}
template <>
void f<0>() {
std::cout << 0 << "\n";
};
int main() {
f<1>();
return 0;
}
void f<0>():
push rbp
mov rbp, rsp
mov esi, 0
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC0
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& s
td::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
pop rbp
ret
main:
push rbp
mov rbp, rsp
call void f<1>()
mov eax, 0
pop rbp
ret
void f<1>():
push rbp
mov rbp, rsp
call void f<0>()
mov esi, 1
mov edi, OFFSET FLAT:_ZSt4cout
call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
mov esi, OFFSET FLAT:.LC0
mov rdi, rax
call std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)
nop
pop rbp
ret