繁体   English   中英

函数内的静态 constexpr 变量有意义吗?

[英]Does static constexpr variable inside a function make sense?

如果我在一个函数中有一个变量(比如一个大数组),将它同时声明为staticconstexpr是否有意义? constexpr保证数组是在编译时创建的,那么static会无用吗?

void f() {
    static constexpr int x [] = {
        // a few thousand elements
    };
    // do something with the array
}

就生成的代码或语义而言, static实际上是否在做任何事情?

简短的回答是, static不仅有用,而且总是很受欢迎。

首先,注意staticconstexpr是完全独立的。 static定义了对象在执行期间的生命周期; constexpr指定对象在编译期间应该可用。 编译和执行在时间和空间上都是不相交和不连续的。 所以一旦程序被编译, constexpr就不再相关了。

声明为constexpr每个变量都是隐式constconststatic几乎是正交的(除了与static const整数的交互。)

C++对象模型(第 1.9 节)要求除位域之外的所有对象至少占用一个字节的内存并具有地址; 此外,在给定时刻在程序中可观察到的所有此类对象必须具有不同的地址(第 6 段)。 这并不完全要求编译器在每次调用具有本地非静态 const 数组的函数时都在堆栈上创建一个新数组,因为编译器可以求助于as-if原则,前提是它可以证明没有其他此类可以观察物体。

不幸的是,这并不容易证明,除非该函数是微不足道的(例如,它不调用其主体在翻译单元中不可见的任何其他函数),因为数组或多或少根据定义是地址。 因此,在大多数情况下,每次调用时都必须在堆栈上重新创建非静态const(expr)数组,这违背了能够在编译时计算它的意义。

另一方面,局部static const对象被所有观察者共享,而且即使定义它的函数从未被调用,也可能被初始化。 所以以上都不适用,编译器不仅可以自由地生成它的单个实例; 可以在只读存储中免费生成它的单个实例。

所以你绝对应该在你的例子中使用static constexpr

但是,在一种情况下,您不想使用static constexpr 除非constexpr声明的对象是ODR 使用的或声明为static ,否则编译器可以完全不包含它。 这非常有用,因为它允许使用编译时临时constexpr数组,而不会用不必要的字节污染已编译的程序。 在这种情况下,您显然不想使用static ,因为static可能会强制对象在运行时存在。

除了给出的答案之外,值得注意的是,编译器不需要在编译时初始化constexpr变量,知道constexprstatic constexpr之间的区别在于使用static constexpr您确保变量只初始化一次。

以下代码演示了如何多次初始化constexpr变量(尽管具有相同的值),而static constexpr肯定只初始化一次。

此外,代码将constexprconst结合static的优势进行了比较。

#include <iostream>
#include <string>
#include <cassert>
#include <sstream>

const short const_short = 0;
constexpr short constexpr_short = 0;

// print only last 3 address value numbers
const short addr_offset = 3;

// This function will print name, value and address for given parameter
void print_properties(std::string ref_name, const short* param, short offset)
{
    // determine initial size of strings
    std::string title = "value \\ address of ";
    const size_t ref_size = ref_name.size();
    const size_t title_size = title.size();
    assert(title_size > ref_size);

    // create title (resize)
    title.append(ref_name);
    title.append(" is ");
    title.append(title_size - ref_size, ' ');

    // extract last 'offset' values from address
    std::stringstream addr;
    addr << param;
    const std::string addr_str = addr.str();
    const size_t addr_size = addr_str.size();
    assert(addr_size - offset > 0);

    // print title / ref value / address at offset
    std::cout << title << *param << " " << addr_str.substr(addr_size - offset) << std::endl;
}

// here we test initialization of const variable (runtime)
void const_value(const short counter)
{
    static short temp = const_short;
    const short const_var = ++temp;
    print_properties("const", &const_var, addr_offset);

    if (counter)
        const_value(counter - 1);
}

// here we test initialization of static variable (runtime)
void static_value(const short counter)
{
    static short temp = const_short;
    static short static_var = ++temp;
    print_properties("static", &static_var, addr_offset);

    if (counter)
        static_value(counter - 1);
}

// here we test initialization of static const variable (runtime)
void static_const_value(const short counter)
{
    static short temp = const_short;
    static const short static_var = ++temp;
    print_properties("static const", &static_var, addr_offset);

    if (counter)
        static_const_value(counter - 1);
}

// here we test initialization of constexpr variable (compile time)
void constexpr_value(const short counter)
{
    constexpr short constexpr_var = constexpr_short;
    print_properties("constexpr", &constexpr_var, addr_offset);

    if (counter)
        constexpr_value(counter - 1);
}

// here we test initialization of static constexpr variable (compile time)
void static_constexpr_value(const short counter)
{
    static constexpr short static_constexpr_var = constexpr_short;
    print_properties("static constexpr", &static_constexpr_var, addr_offset);

    if (counter)
        static_constexpr_value(counter - 1);
}

// final test call this method from main()
void test_static_const()
{
    constexpr short counter = 2;

    const_value(counter);
    std::cout << std::endl;

    static_value(counter);
    std::cout << std::endl;

    static_const_value(counter);
    std::cout << std::endl;

    constexpr_value(counter);
    std::cout << std::endl;

    static_constexpr_value(counter);
    std::cout << std::endl;
}

可能的程序输出:

value \ address of const is               1 564
value \ address of const is               2 3D4
value \ address of const is               3 244

value \ address of static is              1 C58
value \ address of static is              1 C58
value \ address of static is              1 C58

value \ address of static const is        1 C64
value \ address of static const is        1 C64
value \ address of static const is        1 C64

value \ address of constexpr is           0 564
value \ address of constexpr is           0 3D4
value \ address of constexpr is           0 244

value \ address of static constexpr is    0 EA0
value \ address of static constexpr is    0 EA0
value \ address of static constexpr is    0 EA0

正如您所看到的, constexpr被初始化多次(地址不相同),而static关键字确保初始化只执行一次。

不使大型数组static ,即使它们是constexpr也会产生巨大的性能影响,并可能导致许多优化错​​过。 它可能会按数量级减慢您的代码。 您的变量仍然是本地的,编译器可能会决定在运行时初始化它们,而不是将它们作为数据存储在可执行文件中。

考虑以下示例:

template <int N>
void foo();

void bar(int n)
{
    // array of four function pointers to void(void)
    constexpr void(*table[])(void) {
        &foo<0>,
        &foo<1>,
        &foo<2>,
        &foo<3>
    };
    // look up function pointer and call it
    table[n]();
}

您可能希望gcc-10 -O3bar()编译为jmp到它从表中获取的地址,但事实并非如此:

bar(int):
        mov     eax, OFFSET FLAT:_Z3fooILi0EEvv
        movsx   rdi, edi
        movq    xmm0, rax
        mov     eax, OFFSET FLAT:_Z3fooILi2EEvv
        movhps  xmm0, QWORD PTR .LC0[rip]
        movaps  XMMWORD PTR [rsp-40], xmm0
        movq    xmm0, rax
        movhps  xmm0, QWORD PTR .LC1[rip]
        movaps  XMMWORD PTR [rsp-24], xmm0
        jmp     [QWORD PTR [rsp-40+rdi*8]]
.LC0:
        .quad   void foo<1>()
.LC1:
        .quad   void foo<3>()

这是因为 GCC 决定不在可执行文件的数据部分中存储table ,而是在每次函数运行时用其内容初始化一个局部变量。 事实上,如果我们在这里删除constexpr ,编译后的二进制文件是 100% 相同的。

这很容易比以下代码慢 10 倍:

template <int N>
void foo();

void bar(int n)
{
    static constexpr void(*table[])(void) {
        &foo<0>,
        &foo<1>,
        &foo<2>,
        &foo<3>
    };
    table[n]();
}

我们唯一的变化是我们使table static ,但影响是巨大的:

bar(int):
        movsx   rdi, edi
        jmp     [QWORD PTR bar(int)::table[0+rdi*8]]
bar(int)::table:
        .quad   void foo<0>()
        .quad   void foo<1>()
        .quad   void foo<2>()
        .quad   void foo<3>()

总之,永远不要让你的查找表成为局部变量,即使它们是constexpr Clang 实际上很好地优化了这样的查找表,但其他编译器没有。 请参阅编译器资源管理器以获取实时示例

最初的问题仍然成立。 尚不清楚为什么当编译器认为static constexpr更有效时, constexpr不能产生与static constexpr相同的结果的规则。 有人可以提供一个用户需要非静态constexpr的例子吗?

这可能是一个不同的问题,但我一直在寻找为什么在类的constexpr成员之前需要static 至少在 g++ 中,除非它在那里,否则它不会编译。 恕我直言,这相当于声明一个枚举值( enum {FOO=1}类似于constexpr int FOO=1 )并且枚举不需要static

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM