Как избежать копирования при выполнении цепочки методов в C ++ - PullRequest
0 голосов
/ 05 декабря 2018

Я люблю использовать цепочку методов, чтобы полностью инициализировать объекты и затем сохранять их в переменных 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;
}

Версия без метода сцепления здесь примерно в 10000 * 10 раз быстрее, чем другая.Как только я заменю

#undef ENABLE_MOVING

на

#define ENABLE_MOVING

, версия без цепочки методов останется только в 1,5 раз быстрее, чем другая.Так что это большое улучшение.

Тем не менее, мне интересно, смогу ли я оптимизировать код еще больше.Когда я переключаюсь на

#define DEBUGGING_OUTPUT

, я вижу, что для каждого вызова getModified1 () или getModified2 () создаются новые сущности.Единственным преимуществом ходового строительства является то, что создание дешевле.Есть ли способ даже предотвратить построение перемещения и работу с исходной сущностью с помощью цепочки методов?

1 Ответ

0 голосов
/ 06 декабря 2018

С помощью Игоря Тандетника, я думаю, что смогу ответить на мой вопрос!

Необходимо изменить методы модификации, чтобы они возвращали ссылки на rvalue:

#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());

Тогда код цепочки методов почти так же эффективен, как и другой код.Разница заключается в одном вызове конструктора перемещения и одном дополнительном вызове деструктора для временного экземпляра, который незначителен.

Спасибо за помощь!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...