Як знайти C ++ операції з копіюванням?


11

Нещодавно у мене було таке

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

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

Проблема з цим кодом полягає в тому, що при створенні структури відбувається копія, а рішення замість цього - написати return {std :: move (V)}

Чи є лінійний чи аналізатор коду, який би виявляв такі помилкові операції копіювання? Ні cppcheck, cpplint, ні clang-tidy не можуть цього зробити.

EDIT: Деякі моменти, щоб зробити моє питання зрозумілішим:

  1. Я знаю, що операція копіювання сталася тому, що я використовував провідник компілятора, і він показує заклик до memcpy .
  2. Я міг би визначити, що операції з копіюванням відбулися, переглянувши стандартний так. Але моя початкова помилкова думка полягала в тому, що компілятор оптимізував би цю копію. Я помилявся.
  3. Це (ймовірно) не проблема компілятора, оскільки і clang, і gcc створюють код, який створює memcpy .
  4. Memcpy може бути дешевим, але я не уявляю обставин, коли копіювання пам'яті та видалення оригіналу дешевше, ніж передача вказівника std :: move .
  5. Додавання std :: move - елементарна операція. Я б міг уявити, що аналізатор коду міг би запропонувати це виправлення.

2
Я не можу відповісти, чи існує якийсь метод / інструмент для виявлення "помилкових" операцій з копіюванням, проте, на мою чесну думку, я не погоджуюся з тим, що копіювати std::vectorбудь-якими способами не є тим, яким він має намір бути . У вашому прикладі показана явна копія, і застосувати 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*це неможливе переміщення елементів із нього. Детальну відповідь на цю тему можна знайти тут: Initilizer_list та семантика переміщення

Приклад 2) У цьому випадку ми отримуємо початкову конструкцію та 4 копії значення. Це не є особливим, і ми можемо очікувати.

Приклад 3) Також тут ми будуємо конструкцію та деякі кроки, як очікувалося. З моєю реалізацією stl вектор щоразу зростає на коефіцієнт 2. Отже, ми бачимо першу конструкцію, іншу і тому, що вектор змінює розмір від 1 до 2, ми бачимо переміщення першого елемента. Додаючи 3, ми бачимо розмір від 2 до 4, який потребує переміщення перших двох елементів. Все як очікувалося!

Приклад 4) Тепер ми резервуємо простір і заповнюємо пізніше. Тепер у нас немає жодної копії і жодного руху!

У всіх випадках ми не бачимо жодного переміщення чи копіювання, повертаючи вектор назад абоненту! (N) RVO відбувається, і подальших дій на цьому кроці не потрібно!

Назад до свого питання:

"Як знайти C ++ операції копіювання копій"

Як було показано вище, ви можете ввести клас проксі між ними для налагодження.

Зробити приватний копіювальний механізм може не працювати у багатьох випадках, оскільки у вас можуть бути потрібні копії та деякі приховані. Як вище, тільки код, наприклад, 4, буде працювати з приватним копіратором! І я не можу відповісти на запитання, якщо приклад 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 ), інакше питання не має сенсу. Це означає, що ваша відповідь чудова в тому, що показує різні способи створення кращого коду. Ви пропонуєте два способи: використання статичного та введення конструктора безпосередньо у висновок. Отже, ці способи можуть бути запропоновані аналізатором коду.
Матьє Дутур Сікіріч
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.