【问题标题】:C++: Optimizing out destructor callC++:优化析构函数调用
【发布时间】:2017-06-17 06:27:17
【问题描述】:

这里有一个小代码示例:

struct Data {
};

struct Init {
    Data *m_data;

    Init() : m_data(new Data) { }
    ~Init() {
        delete m_data;
    }
};

class Object {
    private:
        int m_initType;
        Data *m_data;
    public:
        Object(const Init &init) : m_initType(0), m_data(init.m_data) { }
        Object(Init &&init) : m_initType(1), m_data(init.m_data) { init.m_data = nullptr; }
        ~Object() {
            if (m_initType==1) {
                delete m_data;
            }
        }
};

对象可以通过两种方式初始化:

  • const Init &:这个初始化只是将 m_data 存储为一个指针,m_data 不是拥有的,所以 ~Object() 不需要做任何事情(在这种情况下,m_data将在 ~Init()) 被销毁
  • Init &&:这个初始化转移了m_data的所有权,Object成为m_data的所有者,所以~Object()需要销毁它

现在,有一个函数:

void somefunction(Object object);

这个函数在callInitA和callInitB中被调用:

void callInitA() {
    Init x;
    somefunction(x); // calls the "const Init &" constructor
}

void callInitB() {
    somefunction(Init()); // calls the "Init &&" constructor
}

现在,这是我想要完成的事情:在 callInitA 的情况下,我想让编译器优化生成的临时 Object (Object 经常使用,我想减少代码大小)。

但是,编译器不会对其进行优化(使用 GCC 和 clang 进行测试)。

Object 被设计成它没有任何改变 m_initType 的函数,因此编译器将能够发现如果 m_initType 在构造时设置为 0,那么它不会改变,所以在析构函数中它仍然是 0 -> 根本不需要调用析构函数,因为它什么都不做。

甚至,m_initType 是 Object 的不必要成员:它只在销毁时才需要。

你有什么设计想法可以做到这一点吗?

更新:我的意思是使用某种 c++ 构造(帮助类等)。 C++ 是一门强大的语言,也许通过某种 c++ 技巧可以做到这一点。

(我最初的问题比这个简化的问题更复杂:对象可以用其他类型的初始化结构初始化,但所有对象的构造函数归结为以某种方式获取“数据*”)

【问题讨论】:

  • 您已经分析并发现析构函数的存在是一个瓶颈?
  • 为什么要手动管理内存?为什么不直接将m_data 设为std::unique_ptr,然后将析构函数设为= default;
  • 你真的需要Object(Init &&init),因为没有它,你也有正确的破坏。
  • The assembly 从您的代码生成表明 GCC 可以并且将避免执行 if (m_initType==1) 检查,因为它可以证明它不需要这样做。这完全取决于编译器及其执行的优化。
  • 这是库代码。此代码符合 >10000 个位置。所以实际上,如果不对其进行分析,我知道这个额外的代码会稍微减慢代码的速度。分析这种事情并不容易,如果不是不可能的话。如果我找到了解决这个问题的方法,那么我可以分析它:我获得了多少时间。

标签: c++ c++11


【解决方案1】:
void callInitA() {
    Init x;
    somefunction(x); // calls the "const Init &" constructor
}

x 的破坏不能被优化掉,不管Init 的内容如何。这样做会违反语言的设计。

这不仅仅是Init 是否包含资源的问题。 Init x 与所有对象一样,将在堆栈上分配稍后需要清理的空间,作为析构函数的隐式(不是您自己编写的代码的一部分)的一部分。这是无法避免的。

如果打算让x 成为somefunction 可以调用而不必重复创建和删除对x 的引用的对象,您应该这样处理它:

void callInitA(Init & x) { //Or Init const& x
    somefunction(x); // calls the "const Init &" constructor
}

其他几点说明:

  • 确保在任何拥有资源的对象上实施Rule of Five(有时称为三法则)。
  • 您可以考虑将所有指针封装在 std::unique_ptr 中,因为您似乎不需要 std::unique_ptr 提供的功能之外的功能。

【讨论】:

  • 我想优化掉 ~Object(),而不是 ~Init()。谢谢你的笔记。这是一个精简的例子,所以更容易理解。
  • @geza 我关注了错误的代码,但这是相同的基本前提:没有办法优化析构函数。如果编译器发现可以提高性能的方法,析构函数的内部工作可以由编译器优化和重构,但析构函数执行的任务不能被优化出来。
  • Xirema:在这种情况下,可以进行优化。看看 Miles linked:如果编译器看到某个函数内部,它会被优化掉。我想要一个没有它的解决方案。
【解决方案2】:

您的m_initType 实际上区分了两种Objects - 拥有自己的记忆的和不拥有记忆的。另外,您提到实际上有多种Objects 可以使用各种输入进行初始化;所以实际上有各种各样的Objects。这表明Object 最好是一些抽象基类。现在,这不会加速任何事情或避免析构函数调用,但它可能会使您的设计更合理。或者 Object 可能是 std::variant(C++17 中的新内容,您可以继续阅读)。

但是,你说临时的Objects 是“经常使用的”。所以也许你应该走另一条路:在你的例子中,假设你有

template <bool Owning> class Object;

然后您将专门针对非拥有情况,只有一个 const Init& 构造函数和默认销毁,以及拥有情况,只有一个 Init&& 构造函数(考虑到您提到的两个)和一个析构函数 deletes .这意味着将使用 Object 的代码模板化,这意味着更大的代码大小,并且必须知道您传入的 Objects 类型;但是 if 会避免条件检查是否真的让你如此烦恼。

我想减小代码大小

我有点怀疑你这样做。您是否正在为嵌入式系统编写代码?在这种情况下,您使用大量临时的Objects 有点奇怪,它们是多态的。

【讨论】:

  • 在我的真实代码中,Object 是 2 字大小的(所以它很小)。将此对象放入堆栈可以通过两条(或在 ARM 上,一条!)asm 指令完成。但是调用析构函数要多得多(如果它是内联的) - 所以它肯定会减少代码大小。它是“一点点”多态性,是的(存在 3 个子类型)。 Object 不能是模板,它会破坏它的用途(有些函数有更多的 Object 参数。那么这些函数也应该是模板)。 Object 的主要目的是它可以提供来自 3 种不同来源的 Data 对象。
猜你喜欢
  • 1970-01-01
  • 2014-05-03
  • 1970-01-01
  • 2012-09-27
  • 2021-07-19
  • 2017-04-28
  • 2013-04-17
  • 2012-03-04
  • 2012-07-12
相关资源
最近更新 更多