简体   繁体   English

错过优化:std::vector<t> ::pop_back() 不是限定析构函数调用? </t>

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

In an std::vector<T> the vector owns the allocated storage and it constructs T s and destructs T s.std::vector<T>中,向量拥有分配的存储空间,它构造T s 并破坏T s。 Regardless of T 's class hierarchy, std::vector<T> knows that it has only created a T and thus when .pop_back() is called it only has to destroy a T (not some derived class of T ).不管T的 class 层次结构如何, std::vector<T>知道它只创建了一个T ,因此当.pop_back()时它只需要销毁一个T (不是T的一些派生的 class )。 Take the following code:采取以下代码:

#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 https://godbolt.org/z/G5ceGe6rq

The PopEm for FooFinal simply just reduces the vector's size by 1 (element). PopEmFooFinal只是将向量的大小减少 1(元素)。 This makes sense.这是有道理的。 But PopEm for FooOpen calls the virtual destructor that the class got by extending Bar .但是PopEmFooOpen调用 class 通过扩展Bar获得的虚拟析构函数。 Given that FooOpen is not final, if a normal delete fooOpen was called on a FooOpen* pointer, it would need to do the virtual destructor, but in the case of std::vector it knows that it only made a FooOpen and no derived class of it was constructed.鉴于FooOpen不是最终的,如果在FooOpen*指针上调用普通delete fooOpen ,则需要执行虚拟析构函数,但在std::vector的情况下,它知道它只生成了FooOpen而没有派生 class它是建造的。 Therefore, couldn't std::vector<FooOpen> treat the class as final and omit the call to the virtual destructor on the pop_back() ?因此, std::vector<FooOpen>不能将 class 视为 final 并省略对pop_back()上的虚拟析构函数的调用吗?

Long story short - compiler doesn't have enough context information to deduce it https://godbolt.org/z/roq7sYdvT长话短说 - 编译器没有足够的上下文信息来推断它https://godbolt.org/z/roq7sYdvT

Boring part:无聊的部分:

The results are similar for all 3: msvc, clang, and gcc, so I guess the problem is general.所有 3 的结果都相似:msvc、clang 和 gcc,所以我猜这个问题是一般性的。 I analysed the libstdc++ code just to find pop_back() runs like this:我分析了 libstdc++ 代码只是为了发现 pop_back() 运行如下:

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

Not any surprise.一点也不意外。 It's like in C++ textbooks.就像 C++ 教科书一样。 But it shows the problem - virtual call to a destructor from a pointer.但它显示了问题 - 从指针虚拟调用析构函数。 What we're looking for is the 'devirtualisation' technique described here: Is final used for optimisation in C++ - it states devirtualisation is 'as-if' behaviour, so it looks like it is open for optimisation if the compiler has enough information to do it.我们正在寻找的是此处描述的“去虚拟化”技术: 最终用于 C++ 中的优化- 它指出去虚拟化是“假设”行为,因此如果编译器有足够的信息来优化做。

My opinion:我的意见:

I meddled with the code a little and i think optimisation doesn't happen because the compiler cannot deduce the only objects pointed by "back" are FooOpen instances.我稍微干预了代码,我认为优化不会发生,因为编译器无法推断出“back”指向的唯一对象是 FooOpen 实例。 We - humans - know it because we analyse the entire class, and see the overall concept of storing the elements in a vector.我们——人类——知道它是因为我们分析了整个 class,并看到了将元素存储在向量中的整体概念。 We know the pointer must point to FooOpen instance only, but compiler fails to see it - it only sees a pointer which can point anywhere (vector allocates uninitialized chunk of memory and its interpretation is a part of vector's logic, also the pointer is modified outside the scope of pop_back()).我们知道指针必须只指向 FooOpen 实例,但编译器看不到它 - 它只看到一个可以指向任何地方的指针(向量分配 memory 的未初始化块,它的解释是向量逻辑的一部分,指针也在外部修改pop_back()) 的 scope。 Without knowing the entire concept of vector<> i don't think of how it can be deduced (without analysing the entire class) that it won't point to any descendant of FooOpen which can be defined in other translation units.在不了解 vector<> 的整个概念的情况下,我不知道如何推断(不分析整个类)它不会指向可以在其他翻译单元中定义的 FooOpen 的任何后代。

FooFinal doesn't have this problem because it already guarantees no other class can inherit from it so devirtualisation is safe for objects pointed by FooFinal* or FooFinal&. FooFinal 没有这个问题,因为它已经保证没有其他 class 可以从它继承,因此去虚拟化对于 FooFinal* 或 FooFinal& 指向的对象是安全的。

For what i've found devirtualisation can occur for non-final classes (Take this snippet https://godbolt.org/z/3a1bvax4o ), as long as there is no pointer arithmetic involved.对于我发现非最终类可能会发生去虚拟化(使用此代码段https://godbolt.org/z/3a1bvax4o ),只要不涉及指针算术。

Yes, this is a missed optimisation.是的,这是一个错过的优化。

Remember that a compiler is a software project, where features have to be written to exist.请记住,编译器是一个软件项目,必须编写功能才能存在。 It may be that the relative overhead of virtual destruction in cases like this is low enough that adding this in hasn't been a priority for the gcc team so far.在这种情况下,虚拟破坏的相对开销可能足够低,以至于到目前为止,添加它并不是 gcc 团队的优先事项。

It is an open-source project, so you could submit a patch that adds this in.这是一个开源项目,所以你可以提交一个补丁来添加它。

It feels a lot like § 11.4.7 (14) gives some insight into this.感觉很像 § 11.4.7 (14) 对此提供了一些见解。 As of latest working draft (N4910 Post-Winter 2022 C++ working draft, Mar. 2022):截至最新的工作草案(N4910 Post-Winter 2022 C++ 工作草案,2022 年 3 月):

After executing the body of the destructor and destroying any objects with automatic storage duration allocated within the body, a destructor for class X calls the destructors for X's direct non-variant non-static data members, the destructors for X's non-virtual direct base classes and, if X is the most derived class (11.9.3), its destructor calls the destructors for X's virtual base classes.在执行析构函数的主体并销毁主体内分配的自动存储持续时间的任何对象后,class X 的析构函数调用 X 的直接非变量非静态数据成员的析构函数,X 的非虚拟直接基类的析构函数并且,如果 X 是派生最多的 class (11.9.3),则它的析构函数调用 X 的虚拟基类的析构函数。 All destructors are called as if they were referenced with a qualified name, that is, ignoring any possible virtual overriding destructors in more derived classes.所有的析构函数都被调用,就好像它们被一个限定名引用一样,也就是说,忽略更多派生类中任何可能的虚拟覆盖析构函数。 Bases and members are destroyed in the reverse order of the completion of their constructor (see 11.9.3).基和成员按照其构造函数完成的相反顺序被销毁(见 11.9.3)。 [Note 4: A return statement (8.7.4) in a destructor might not directly return to the caller; [注4:析构函数中的return语句(8.7.4)可能不会直接返回给调用者; before transferring control to the caller, the destructors for the members and bases are called.在将控制权转移给调用者之前,会调用成员和基的析构函数。 — end note] Destructors for elements of an array are called in reverse order of their construction (see 11.9). — 尾注]数组元素的析构函数按照其构造的相反顺序调用(参见 11.9)。

Also interesting for this topic, § 11.4.6, (17):这个主题也很有趣,§ 11.4.6, (17):

In an explicit destructor call, the destructor is specified by a ~ followed by a type-name or decltype-specifier that denotes the destructor's class type.在显式析构函数调用中,析构函数由 ~ 指定,后跟类型名称或 decltype 说明符,表示析构函数的 class 类型。 The invocation of a destructor is subject to the usual rules for member functions (11.4.2);析构函数的调用遵循成员函数的一般规则(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. 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.

So, as far as the standard cares, the invocation of a destructor is subject to the usual rules for member functions.因此,就标准而言,析构函数的调用受制于成员函数的通常规则。

This, to me, sounds a lot like destructor calls do so much that compilers are likely unable to determine, at compile-time , that a destructor call does "nothing" - as it also calls destructors of members, and std::vector doesn't know this.对我来说,这听起来很像析构函数调用做了很多事情,以至于编译器可能无法在编译时确定析构函数调用“什么都不做”——因为它也调用成员的析构函数,而 std::vector 没有这个不知道

If a class 'Trivial' is extended from FooOpen, and an object of it is inserted into vector<FooOpen> , the inserted of member would be a instance of 'FooOpen' but not of 'Trivial'.如果从 FooOpen 扩展了 class 'Trivial',并且将其中的 object 插入到vector<FooOpen>中,则插入的成员将是 'FooOpen' 的实例,而不是 'Trivial' 的实例。 That's the problem you get!这就是你遇到的问题! Simply using vector<FooOpen*> or vector<unqiue_ptr<FooOpen>> instead of vector<FooOpen> to resolve the problem.只需使用vector<FooOpen*>vector<unqiue_ptr<FooOpen>>而不是vector<FooOpen>即可解决问题。

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

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