Как найти поддельные операции копирования C ++?

11

Недавно у меня было следующее

struct data {
  std::vector<int> V;
};

data get_vector(int n)
{
  std::vector<int> V(n,0);
  return {V};
}

Проблема с этим кодом заключается в том, что при создании структуры происходит копирование, и вместо этого нужно написать return return {std :: move (V)}

Есть ли линтер или анализатор кода, который обнаружил бы такие ложные операции копирования? Ни cppcheck, ни cpplint, ни clang-tidy не могут этого сделать.

РЕДАКТИРОВАТЬ: Несколько моментов, чтобы прояснить мой вопрос:

  1. Я знаю, что операция копирования произошла, потому что я использовал проводник компилятора, и он показывает вызов memcpy .
  2. Я мог бы определить, что операции копирования произошли, посмотрев на стандарт да. Но моя первоначальная неправильная идея заключалась в том, что компилятор оптимизировал бы эту копию. Я был неправ.
  3. Это (вероятно) не проблема компилятора, так как и clang, и gcc создают код, который создает memcpy .
  4. Memcpy может быть дешевым, но я не могу представить обстоятельства, когда копирование памяти и удаление оригинала дешевле, чем передача указателя с помощью std :: move .
  5. Добавление std :: move является элементарной операцией. Я полагаю, что анализатор кода сможет предложить это исправление.
Матье Дутур Сикирич
источник
2
Я не могу ответить, существует ли какой-либо метод / инструмент для обнаружения «ложных» операций копирования, однако, по моему честному мнению, я не согласен с тем, что копирование std::vectorкаким-либо образом не является тем, чем оно является . В вашем примере показана явная копия, и это вполне естественный и правильный подход (опять же imho) применить std::moveфункцию, как вы предлагаете сами, если копия не то, что вам нужно. Обратите внимание, что некоторые компиляторы могут пропустить копирование, если флаги оптимизации включены, а вектор неизменен.
Магнус
Боюсь, что слишком много ненужных копий (которые могут не повлиять), чтобы использовать это правило линтера: - / ( ржавчина использует перемещение по умолчанию, поэтому требуется явное копирование :))
Jarod42
Мои предложения по оптимизации кода в основном разбирают функцию, которую вы хотите оптимизировать, и вы обнаружите дополнительные операции копирования
camp0
Если я правильно понимаю вашу проблему, вы хотите обнаружить случаи, когда операция копирования (конструктор или оператор присваивания) вызывается для объекта, следующего за его уничтожением. Для пользовательских классов я могу представить добавление некоторого установленного флага отладки при выполнении копирования, сброса всех остальных операций и проверки в деструкторе. Тем не менее, не знаете, как сделать то же самое для не пользовательских классов, если вы не можете изменить их исходный код.
Даниэль Лангр
2
Техника, которую я использую для поиска ложных копий, заключается в том, чтобы временно сделать конструктор копирования частным, а затем проверить, где компилятор блокируется из-за ограничений доступа. (Та же цель может быть достигнута, если пометить конструктор копирования как устаревший для компиляторов, которые поддерживают такие теги.)
Eljay

Ответы:

2

Я верю, что у вас правильное наблюдение, но неверное толкование!

Копирование не произойдет при возврате значения, потому что каждый нормальный умный компилятор будет использовать (N) RVO в этом случае. В C ++ 17 это является обязательным, поэтому вы не можете увидеть ни одной копии, возвращая локально сгенерированный вектор из функции.

Хорошо, давайте немного поиграем с std::vectorтем, что будет происходить во время строительства или путем его постепенного заполнения.

Прежде всего, давайте создадим тип данных, который делает каждую копию или перемещение видимым, как этот:

template <typename DATA >
struct VisibleCopy
{
    private:
        DATA data;

    public:
        VisibleCopy( const DATA& data_ ): data{ data_ }
        {
            std::cout << "Construct " << data << std::endl;
        }

        VisibleCopy( const VisibleCopy& other ): data{ other.data }
        {
            std::cout << "Copy " << data << std::endl;
        }

        VisibleCopy( VisibleCopy&& other ) noexcept : data{ std::move(other.data) }
        {
            std::cout << "Move " << data << std::endl;
        }

        VisibleCopy& operator=( const VisibleCopy& other )
        {
            data = other.data;
            std::cout << "copy assign " << data << std::endl;
        }

        VisibleCopy& operator=( VisibleCopy&& other ) noexcept
        {
            data = std::move( other.data );
            std::cout << "move assign " << data << std::endl;
        }

        DATA Get() const { return data; }

};

А теперь давайте начнем некоторые эксперименты:

using T = std::vector< VisibleCopy<int> >;

T Get1() 
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec{ 1,2,3,4 };
    std::cout << "End init" << std::endl;
    return vec;
}   

T Get2()
{   
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec(4,0);
    std::cout << "End init" << std::endl;
    return vec;
}

T Get3()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

T Get4()
{
    std::cout << "Start init" << std::endl;
    std::vector< VisibleCopy<int> > vec;
    vec.reserve(4);
    vec.emplace_back(1);
    vec.emplace_back(2);
    vec.emplace_back(3);
    vec.emplace_back(4);
    std::cout << "End init" << std::endl;

    return vec;
}

int main()
{
    auto vec1 = Get1();
    auto vec2 = Get2();
    auto vec3 = Get3();
    auto vec4 = Get4();

    // All data as expected? Lets check:
    for ( auto& el: vec1 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec2 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec3 ) { std::cout << el.Get() << std::endl; }
    for ( auto& el: vec4 ) { std::cout << el.Get() << std::endl; }
}

Что мы можем наблюдать:

Пример 1) Мы создаем вектор из списка инициализаторов и, возможно, мы ожидаем, что мы увидим 4 раза построение и 4 хода. Но мы получаем 4 копии! Это звучит немного загадочно, но причина в реализации списка инициализаторов! Просто нельзя перемещаться из списка, так как итератор из списка являетсяconst T* что делает невозможным перемещение элементов из него. Подробный ответ на эту тему можно найти здесь: initializer_list и семантика перемещения

Пример 2) В этом случае мы получаем начальную конструкцию и 4 копии значения. В этом нет ничего особенного, и это то, что мы можем ожидать.

Пример 3) Также здесь, мы строим и некоторые шаги, как ожидалось. С моей реализацией stl вектор растет в 2 раза каждый раз. Таким образом, мы видим первую конструкцию, другую, и поскольку вектор изменяется с 1 на 2, мы видим движение первого элемента. Добавляя 3, мы видим изменение размера от 2 до 4, которое требует перемещения первых двух элементов. Все как и ожидалось!

Пример 4) Теперь мы оставляем за собой место и заполняем позже. Теперь у нас нет копии и больше нет движения!

Во всех случаях мы не видим ни перемещения, ни копирования, вообще возвращая вектор вызывающей стороне! (N) RVO происходит, и на этом этапе никаких дальнейших действий не требуется!

Вернуться к вашему вопросу:

«Как найти поддельные операции копирования C ++»

Как видно выше, вы можете ввести прокси-класс между ними для целей отладки.

Приватное копирование ctor может не сработать во многих случаях, так как у вас могут быть некоторые требуемые копии и некоторые скрытые. Как и выше, только код для примера 4 будет работать с частным копи-ctor! И я не могу ответить на вопрос, является ли пример 4 самым быстрым, поскольку мы наполняем мир миром.

Извините, что я не могу предложить общее решение для поиска «нежелательных» копий здесь. Даже если вы копаете свой код для вызовов memcpy, вы не найдете все, что также memcpyбудет оптимизировано, и вы увидите, как некоторые инструкции на ассемблере выполняют свою работу без вызова вашей библиотечной memcpyфункции.

Мой намек не в том, чтобы сосредоточиться на такой незначительной проблеме. Если у вас есть реальные проблемы с производительностью, возьмите профилировщик и измерьте. Потенциальных убийц производительности так много, что тратить много времени на ложное memcpyиспользование кажется не очень полезной идеей.

Клаус
источник
Мой вопрос академический. Да, есть много способов получить медленный код, и для меня это не является непосредственной проблемой. Однако мы можем найти операции memcpy с помощью проводника компилятора. Так что, безусловно, есть способ. Но это возможно только для небольших программ. Моя точка зрения заключается в том, что есть интерес к коду, который найдет предложения по улучшению кода. Есть анализаторы кода, которые обнаруживают ошибки и утечки памяти, почему бы не такие проблемы?
Матье Дутур Сикирич
«код, который найдет предложения по улучшению кода». Это уже сделано и реализовано в самих компиляторах. (N) Оптимизация RVO - это всего лишь один пример, который отлично работает, как показано выше. Поймать memcpy не помогло, так как вы ищете "нежелательный memcpy". «Есть анализаторы кода, которые обнаруживают ошибки и утечки памяти, почему бы не такие проблемы?» Может быть, это не (общая) проблема. И гораздо более общий инструмент для поиска «скоростных» проблем также уже присутствует: профилировщик! Мое личное ощущение, что вы ищете академическую вещь, которая не является проблемой в реальном программном обеспечении сегодня.
Клаус
1

Я знаю, что операция копирования произошла, потому что я использовал проводник компилятора, и он показывает вызов memcpy.

Поместили ли вы свое полное приложение в проводник компилятора и включили ли вы оптимизации? Если нет, то то, что вы видели в проводнике компилятора, может или не может быть тем, что происходит с вашим приложением.

Одна проблема с кодом, который вы опубликовали, заключается в том, что вы сначала создаете std::vector, а затем копируете его в экземпляр data. Было бы лучше инициализировать data с вектором:

data get_vector(int n)
{
  return {std::vector<int> V(n,0)};
}

Кроме того, если вы просто дадите обозревателю компилятора определение dataи get_vector(), и ничего больше, он будет ожидать худшего. Если вы фактически дадите ему некоторый исходный код, который использует get_vector() , то посмотрите, какая сборка сгенерирована для этого исходного кода. Посмотрите этот пример, чтобы узнать, что приведенная выше модификация плюс фактическое использование плюс оптимизация компилятора могут привести к созданию компилятором.

Г. Слипен
источник
Я только поместил в проводник компьютера вышеупомянутый код (который имеет memcpy ), иначе вопрос не будет иметь смысла. При этом ваш ответ отлично показывает разные способы создания лучшего кода. Вы предоставляете два способа: использование static и помещение конструктора непосредственно в вывод. Таким образом, эти способы могут быть предложены анализатором кода.
Матье Дутур Сикирич