【问题标题】:What optimizations are enabled by non-type template parameters?非类型模板参数启用了哪些优化?
【发布时间】:2014-12-12 02:06:44
【问题描述】:

我在cppreference.com 找到了这个示例,它似乎是整个 StackOverflow 中使用的实际示例:

template<int N>
struct S {
    int a[N];
};

当然,非类型模板化比这个例子更有价值。此语法还支持哪些其他优化?为什么要创建它?

我很好奇,因为我的代码依赖于已安装的单独库的版本。我在嵌入式环境中工作,所以优化很重要,但我也希望有可读的代码。话虽如此,我想使用这种模板风格来处理版本差异(下面的示例)。首先,我是否正确地考虑了这一点,最重要的是与使用 #ifdef 语句相比,它提供了好处还是缺点?

尝试 1:

template<int VERSION = 500>
void print (char *s);

template<int VERSION>
void print (char *s) {
    std::cout << "ERROR! Unsupported version: " << VERSION << "!" << std::endl;
}

template<>
void print<500> (char *s) {
    // print using 500 syntax
}

template<>
void print<600> (char *s) {
    // print using 600 syntax
}

OR - 由于模板在编译时是常量,编译器是否可以使用类似于以下语法的 if 语句的其他分支来考虑死代码:

尝试 2:

template<int VERSION = 500>
void print (char *s) {
   if (VERSION == 500) {
       // print using 500 syntax
   } else if (VERSION == 600) {
       // print using 600 syntax
   } else {
       std::cout << "ERROR! Unsupported version: " << VERSION << "!" << std::endl;
   }
}

是否会尝试产生与此大小相当的输出?

void print (char *s) {
#if defined(500)
    // print using 500 syntax
#elif defined(600)
    // print using 600 syntax
#else
    std::cout << "ERROR! Unsupported version: " << VERSION << "!" << std::endl;
#endif
}

如果你看不出我对这一切有些迷惑,那么就我而言,解释越深入越好。

【问题讨论】:

标签: c++ templates compiler-optimization non-type


【解决方案1】:

编译器发现死代码消除很容易。在这种情况下,您有一个 ifs 链,这取决于(仅)template 参数的值或类型。所有分支都必须包含有效代码,但在编译和优化后,死分支就会消失。

一个经典的例子是使用template 参数编写的每像素操作,这些参数控制代码流的细节。主体可以充满分支,但编译后的输出是无分支的。

类似的技术可以用来展开循环(比如扫描线循环)。必须注意理解可能导致的代码大小乘法:特别是如果您的编译器缺少 ICF(又名 comdat 折叠),例如 gold gcc 链接器和 msvc(以及其他)。

还可以做一些更有趣的事情,比如手动跳表。

您可以在没有运行时行为的情况下进行纯编译时类型检查,例如维度分析。或者区分n空间中的点和向量。

枚举可用于命名类型或开关。指向启用高效内联的函数的指针。指向数据的指针,以允许可模拟、可孤立或与实现分离的“全局”状态。指向字符串的指针,以允许代码中有效的可读名称。用于无数目的的整数值列表,例如解包元组的索引技巧。对静态数据的复杂操作,例如编译时对多个索引中的数据进行排序,或者检查具有复杂不变量的静态数据的完整性。

我确定我错过了一些。

【讨论】:

  • 我喜欢这里的内容!你能在这里洒一些示例代码吗?
【解决方案2】:

一个明显的优化是当使用一个整数时,编译器有一个常量而不是一个变量:

int foo(size_t); // definition not visible
// vs
template<size_t N>
size_t foo() {return N*N;}

使用模板,在运行时无需计算任何内容,并且可以将结果用作常量,这有助于其他优化。您可以通过将其声明为 constexpr 来进一步了解此示例,如下所述的 5gon12eder。

下一个例子:

int foo(double, size_t); // definition not visible
// vs
template<size_t N>
size_t foo(double p) {
 double r(p);
 for (size_t i(0) i < N; ++i) {
  r *= p;
 }
 return r;
}

好的。现在循环的迭代次数是已知的。可以相应地展开/优化循环,这对于大小、速度和消除分支都有好处。

另外,根据您的示例,std::array&lt;&gt; 存在。 std::array&lt;&gt; 在某些情况下可能比 std::vector&lt;&gt; 好得多,因为 std::vector&lt;&gt; 使用堆分配和非本地内存。

还有一些专业化可能会有不同的实现。您可以将它们分开并(可能)减少其他引用的定义。

当然,templates&lt;&gt; 也可以防止您不必要地重复您的程序。

templates&lt;&gt; 还需要更长的符号名称。

回到您的版本示例:是的,如果VERSION 在编译时已知,则可以删除从未执行的代码,并且您还可以减少引用的函数。主要区别在于void print (char *s) 的名称将比模板短(其符号名称包括所有模板参数)。对于一个函数,这是计算字节数。对于具有许多功能和模板的复杂程序,成本会迅速上升。

【讨论】:

  • 你的观点都是有效的,但我要补充一点,你的第一个例子最好写成 constexpr 函数,它将函数的灵活性与静态评估的优化潜力结合起来。
  • 喜欢关于模板符号名称开销的观点。这对于嵌入式环境来说是一个极其重要的考虑因素。
【解决方案3】:

typename 模板参数的潜在应用范围非常广泛。在他的C++ 编程语言一书中,Stroustrup 给出了一个有趣的例子,它勾勒出一个用于处理物理量的类型安全的零开销框架。基本上,这个想法是他编写了一个模板,该模板接受表示基本物理量(例如 lengthmass)的幂的整数,然后对它们定义算术。在生成的框架中,您可以将 speedspeed 相加或将 distance 除以 time 但您不能添加 质量时间。请查看Boost.Units,了解该想法的行业实力实施。

关于你的第二个问题。任何合理的编译器都应该能够为

生成完全相同的机器代码
#define FOO

#ifdef FOO
do_foo();
#else
do_bar();
#endif

#define FOO_P 1

if (FOO_P)
  do_foo();
else
  do_bar();

除了第二个版本更具可读性并且编译器可以同时捕获两个分支中的错误。使用模板是生成相同代码的第三种方法,但我怀疑它是否会提高可读性。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-05-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-15
    • 2021-03-18
    相关资源
    最近更新 更多