簡體   English   中英

錯過優化:std::vector<t> ::pop_back() 不是限定析構函數調用? </t>

[英]Missed Optimization: std::vector<T>::pop_back() not qualifying destructor call?

std::vector<T>中,向量擁有分配的存儲空間,它構造T s 並破壞T s。 不管T的 class 層次結構如何, std::vector<T>知道它只創建了一個T ,因此當.pop_back()時它只需要銷毀一個T (不是T的一些派生的 class )。 采取以下代碼:

#include <vector>

struct Bar {
    virtual ~Bar() noexcept = default;
};

struct FooOpen : Bar {
    int a;
};

struct FooFinal final : Bar {
    int a;
};

void popEm(std::vector<FooOpen>& v) {
    v.pop_back();
}

void popEm(std::vector<FooFinal>& v) {
    v.pop_back();
}

https://godbolt.org/z/G5ceGe6rq

PopEmFooFinal只是將向量的大小減少 1(元素)。 這是有道理的。 但是PopEmFooOpen調用 class 通過擴展Bar獲得的虛擬析構函數。 鑒於FooOpen不是最終的,如果在FooOpen*指針上調用普通delete fooOpen ,則需要執行虛擬析構函數,但在std::vector的情況下,它知道它只生成了FooOpen而沒有派生 class它是建造的。 因此, std::vector<FooOpen>不能將 class 視為 final 並省略對pop_back()上的虛擬析構函數的調用嗎?

長話短說 - 編譯器沒有足夠的上下文信息來推斷它https://godbolt.org/z/roq7sYdvT

無聊的部分:

所有 3 的結果都相似:msvc、clang 和 gcc,所以我猜這個問題是一般性的。 我分析了 libstdc++ 代碼只是為了發現 pop_back() 運行如下:

void pop_back() // a bit more convoluted but boils-down to this
{
    --back;
    back->~T();
}

一點也不意外。 就像 C++ 教科書一樣。 但它顯示了問題 - 從指針虛擬調用析構函數。 我們正在尋找的是此處描述的“去虛擬化”技術: 最終用於 C++ 中的優化- 它指出去虛擬化是“假設”行為,因此如果編譯器有足夠的信息來優化做。

我的意見:

我稍微干預了代碼,我認為優化不會發生,因為編譯器無法推斷出“back”指向的唯一對象是 FooOpen 實例。 我們——人類——知道它是因為我們分析了整個 class,並看到了將元素存儲在向量中的整體概念。 我們知道指針必須只指向 FooOpen 實例,但編譯器看不到它 - 它只看到一個可以指向任何地方的指針(向量分配 memory 的未初始化塊,它的解釋是向量邏輯的一部分,指針也在外部修改pop_back()) 的 scope。 在不了解 vector<> 的整個概念的情況下,我不知道如何推斷(不分析整個類)它不會指向可以在其他翻譯單元中定義的 FooOpen 的任何后代。

FooFinal 沒有這個問題,因為它已經保證沒有其他 class 可以從它繼承,因此去虛擬化對於 FooFinal* 或 FooFinal& 指向的對象是安全的。

對於我發現非最終類可能會發生去虛擬化(使用此代碼段https://godbolt.org/z/3a1bvax4o ),只要不涉及指針算術。

是的,這是一個錯過的優化。

請記住,編譯器是一個軟件項目,必須編寫功能才能存在。 在這種情況下,虛擬破壞的相對開銷可能足夠低,以至於到目前為止,添加它並不是 gcc 團隊的優先事項。

這是一個開源項目,所以你可以提交一個補丁來添加它。

感覺很像 § 11.4.7 (14) 對此提供了一些見解。 截至最新的工作草案(N4910 Post-Winter 2022 C++ 工作草案,2022 年 3 月):

在執行析構函數的主體並銷毀主體內分配的自動存儲持續時間的任何對象后,class X 的析構函數調用 X 的直接非變量非靜態數據成員的析構函數,X 的非虛擬直接基類的析構函數並且,如果 X 是派生最多的 class (11.9.3),則它的析構函數調用 X 的虛擬基類的析構函數。 所有的析構函數都被調用,就好像它們被一個限定名引用一樣,也就是說,忽略更多派生類中任何可能的虛擬覆蓋析構函數。 基和成員按照其構造函數完成的相反順序被銷毀(見 11.9.3)。 [注4:析構函數中的return語句(8.7.4)可能不會直接返回給調用者; 在將控制權轉移給調用者之前,會調用成員和基的析構函數。 — 尾注]數組元素的析構函數按照其構造的相反順序調用(參見 11.9)。

這個主題也很有趣,§ 11.4.6, (17):

在顯式析構函數調用中,析構函數由 ~ 指定,后跟類型名稱或 decltype 說明符,表示析構函數的 class 類型。 析構函數的調用遵循成員函數的一般規則(11.4.2); that is, if the object is not of the destructor's class type and not of a class derived from the destructor's class type (including when the destructor is invoked via a null pointer value), the program has undefined behavior.

因此,就標准而言,析構函數的調用受制於成員函數的通常規則。

對我來說,這聽起來很像析構函數調用做了很多事情,以至於編譯器可能無法在編譯時確定析構函數調用“什么都不做”——因為它也調用成員的析構函數,而 std::vector 沒有這個不知道

如果從 FooOpen 擴展了 class 'Trivial',並且將其中的 object 插入到vector<FooOpen>中,則插入的成員將是 'FooOpen' 的實例,而不是 'Trivial' 的實例。 這就是你遇到的問題! 只需使用vector<FooOpen*>vector<unqiue_ptr<FooOpen>>而不是vector<FooOpen>即可解決問題。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM