簡體   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