【问题标题】:How to Avoid Copies When Doing Method Chaining in C++在 C++ 中进行方法链时如何避免复制
【发布时间】:2018-12-05 13:22:34
【问题描述】:

我喜欢使用方法链来完全初始化对象,然后将它们存储在 const 变量中。在分析生成的代码时,事实证明这意味着执行许多复制构造函数。因此,我想知道 C++ 11 移动语义是否有助于优化方法链。

确实,通过将带有 ref 限定符的重载添加到我的链方法中,我已经能够显着加快我的代码速度。请考虑以下源代码:

#include <chrono>
#include <iostream>
#include <string>

#undef DEBUGGING_OUTPUT
#undef ENABLE_MOVING

class Entity
{
public:

        Entity() :
                        data(0.0), text("Standard Text")
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Constructing entity." << std::endl;
#endif
        }

        Entity(const Entity& entity) :
                        data(entity.data), text(entity.text)
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Copying entity." << std::endl;
#endif
        }

        Entity(Entity&& entity) :
                        data(entity.data), text(std::move(entity.text))
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Moving entity." << std::endl;
#endif
        }

        ~Entity()
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Cleaning up entity." << std::endl;
#endif
        }

        double getData() const
        {
                return data;
        }

        const std::string& getText() const
        {
                return text;
        }

        void modify1()
        {
                data += 1.0;
                text += " 1";
        }

        Entity getModified1() const &
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Lvalue version of getModified1" << std::endl;
#endif

                Entity newEntity = *this;
                newEntity.modify1();

                return newEntity;
        }

#ifdef ENABLE_MOVING
        Entity getModified1() &&
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Rvalue version of getModified1" << std::endl;
#endif

                modify1();

                return std::move(*this);
        }
#endif

        void modify2()
        {
                data += 2.0;
                text += " 2";
        }

        Entity getModified2() const &
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Lvalue version of getModified2" << std::endl;
#endif

                Entity newEntity = *this;
                newEntity.modify2();

                return newEntity;
        }

#ifdef ENABLE_MOVING
        Entity getModified2() &&
        {
#ifdef DEBUGGING_OUTPUT
                std::cout << "Rvalue version of getModified2" << std::endl;
#endif

                modify2();

                return std::move(*this);
        }
#endif

private:

        double data;
        std::string text;
};

int main()
{
        const int interationCount = 1000;

        {
            // Create a temporary entity, modify it and store it in a const variable
            // by taking use of method chaining.
            //
            // This approach is elegant to write and read, but it is slower than the
            // other approach.

                const std::chrono::steady_clock::time_point startTimePoint =
                                std::chrono::steady_clock::now();

                for (int i = 0; i < interationCount; ++i)
                {
                        const Entity entity = Entity().getModified1().getModified1().getModified2().getModified2();

#ifdef DEBUGGING_OUTPUT
                        std::cout << "Entity has text " << entity.getText() << " and data "
                                        << entity.getData() << std::endl;
#endif
                }

                const std::chrono::steady_clock::time_point stopTimePoint =
                                std::chrono::steady_clock::now();

                const std::chrono::duration<double> timeSpan = std::chrono::duration_cast<
                                std::chrono::duration<double>>(stopTimePoint - startTimePoint);

                std::cout << "Method chaining has taken " << timeSpan.count() << " seconds."
                                << std::endl;
        }

        {
            // Create an entity and modify it without method chaining. It cannot be
            // stored in a const variable.
            //
            // This approach is optimal from a performance point of view, but it is longish
            // and renders usage of a const variable impossible even if the entity
            // won't change after initialization.

                const std::chrono::steady_clock::time_point startTimePoint =
                                std::chrono::steady_clock::now();

                for (int i = 0; i < interationCount; ++i)
                {
                        Entity entity;
                        entity.modify1();
                        entity.modify1();
                        entity.modify2();
                        entity.modify2();

#ifdef DEBUGGING_OUTPUT
                        std::cout << "Entity has text " << entity.getText() << " and data "
                                        << entity.getData() << std::endl;
#endif
                }

                const std::chrono::steady_clock::time_point stopTimePoint =
                                std::chrono::steady_clock::now();

                const std::chrono::duration<double> timeSpan = std::chrono::duration_cast<
                                std::chrono::duration<double>>(stopTimePoint - startTimePoint);

                std::cout << "Modification without method chaining has taken "
                                << timeSpan.count() << " seconds." << std::endl;
        }

        return 0;
}

没有方法链接的版本在这里比另一个快大约10倍。只要我更换

#undef ENABLE_MOVING

通过

#define ENABLE_MOVING

没有方法链接的版本仅比其他版本快 1.5 倍。所以这是一个很大的改进。

我仍然想知道是否可以进一步优化代码。当我切换到

#define DEBUGGING_OUTPUT

然后我可以看到每次调用 getModified1() 或 getModified2() 都会创建新实体。移动构造的唯一优点是创建更便宜。有没有办法甚至阻止移动构造并使用方法链接在原始实体上工作?

【问题讨论】:

  • 但是移动在语义上不同于访问和访问 const ref 或 copy,在这种情况下它们是相同的,即保留原始对象。看来您的建议在功能上不一样。此外,根据 T&amp;&amp; 的实现,您实际上可能最终会得到一个用于移动的复制 ctor。
  • 嗯,你能详细说明一下吗,我不明白你的意思吗?移动构造函数really 移动成员字段data,这是加速的原因。另外我猜想创建一个临时实例然后调用右值修改方法应该给出与创建变量并调用修改方法完全相同的结果。 T&amp;&amp; 是什么意思?
  • 我的意思是你建议用 move 代替 copy 听起来不太好。它不具有相同的不变量。 T&amp;&amp; 我的意思是 T(T&amp;&amp;) 即移动 ctor。对于简单类型,它是一个副本,例如你不能真正移动intdouble。也许我误解了您要避免哪些副本。他们有很多。即使在Entity getModified1() &amp;&amp;。任何通过引入强制移动,您都可以防止编译器允许复制省略,以支持基本上再次复制的移动,实际上会降低性能。也许我误解了你的问题。对不起,如果是这样。
  • 如果您不介意修改原始实体,则只需让modify1 等人返回Entity&amp;,如return *this;。然后你可以做const Entity entity = Entity().modify1().modify1().modify2().modify2(); 并且只调用一个移动构造函数。
  • @luk32:我已将 cmets 添加到时间测量代码中以阐明我的意图/问题。你说得对,return std::move(...) 可能会使复制省略成为不可能。但是省略std::move 在这里没有帮助。如果启用调试输出(只需使用 #define DEBUGGING_OUTPUT)并在 ref 限定符重载(Entity getModified*() &amp;&amp;))中将 return std::move(*this); 更改为 return *this;,那么您会看到实体在每次调用 getModified*() &amp;&amp; 结束时被复制。这里的性能也下降了 10 倍。

标签: c++11 move-semantics method-chaining


【解决方案1】:

在 Igor Tandetnik 的帮助下,我想我可以回答我的问题了!

修改方法必须更改为返回右值引用:

#ifdef ENABLE_MOVING
Entity&& getModified1() &&
{
#ifdef DEBUGGING_OUTPUT
    std::cout << "Rvalue version of getModified1" << std::endl;
#endif

    modify1();

    return std::move(*this);
}
#endif

#ifdef ENABLE_MOVING
Entity&& getModified2() &&
{
#ifdef DEBUGGING_OUTPUT
    std::cout << "Rvalue version of getModified2" << std::endl;
#endif

    modify2();

    return std::move(*this);
}
#endif

并且初始化必须像这样发生:

const Entity entity = std::move(Entity().getModified1().getModified1().getModified2().getModified2());

那么方法链代码几乎和其他代码一样高效。区别在于对移动构造函数的一次调用和对临时实例的一次额外的析构函数调用,这可以忽略不计。

感谢您的帮助!

【讨论】:

  • 你能解释一下你为什么不赞成这个答案吗?谢谢。
  • 欢迎来到 SO!在 C++ 中使用separate Builder class 会更惯用。您的问题是您将类用作自身和构建器类,并且您致力于 consts。这意味着每次你都需要构造一个新对象,无论是通过复制还是移动构造函数,都比它需要的要慢。
  • @JMAA:我明白你的意思。仍然在这种情况下,设计对我来说似乎相当不错。将Entity 视为空间中的向量或点。修改它可能是还原矢量或平移点。所以修改后的实体是原始实体的派生。写vector n = vector(1.0, 2.0, 3.0).normalized(); 比使用单独的向量生成器类要自然得多。但正如我在对 luk32 的评论中已经提到的那样,该设计源于第三方库。我不能完全改变设计,但也许改进现有的。
  • auto normal = vector(1.0, 2.0, 3.0).normalized() 的情况下,我很困惑为什么您希望vector 对象是不可变的?
  • 我想说它是一个不变的、完全初始化的常量。很多时候,有必要通过计算生成向量的叉积并对叉积进行归一化来计算(三角形或切线空间的)法线。出于样式原因,最好将法线存储为 const 向量。
猜你喜欢
  • 2018-08-29
  • 2011-12-28
  • 2021-02-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多