Скільки коштує накладних витрат на розумні вказівники порівняно із звичайними вказівниками в C ++?


101

Скільки коштує накладних витрат на розумні вказівники порівняно із звичайними вказівниками в C ++ 11? Іншими словами, чи буде мій код повільнішим, якщо я використовую розумні вказівники, і якщо так, то наскільки повільнішим?

Зокрема, я запитую про C ++ 11 std::shared_ptrі std::unique_ptr.

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

Наприклад, я повертаю розумний вказівник із функції замість звичайного вказівника:

std::shared_ptr<const Value> getValue();
// versus
const Value *getValue();

Або, наприклад, коли одна з моїх функцій приймає розумний вказівник як параметр замість звичайного вказівника:

void setValue(std::shared_ptr<const Value> val);
// versus
void setValue(const Value *val);

8
Єдиний спосіб дізнатись - це порівняти свій код.
Василь Старинкевич

Якого ви маєте на увазі? std::unique_ptrабо std::shared_ptr?
stefan

10
Відповідь - 42. (іншими словами, хто знає, вам потрібно профайлювати свій код та розуміти на своєму обладнанні типове робоче навантаження.)
Нім,

У вашій програмі необхідно використовувати надзвичайно використання розумних покажчиків, щоб воно було суттєвим.
user2672165

Вартість використання shared_ptr у простій функції встановлення жахлива і додасть багаторазові накладні витрати.
Лотар

Відповіді:


176

std::unique_ptr має накладні витрати на пам’ять, лише якщо ви надаєте їй якийсь нетривіальний видаляч.

std::shared_ptr завжди має накладні витрати на пам'ять для лічильника посилань, хоча це дуже мало.

std::unique_ptr має накладні витрати часу лише під час конструктора (якщо йому потрібно скопіювати наданий делетор та / або нульову ініціалізацію покажчика) та під час деструктора (для знищення об’єкта, що належить).

std::shared_ptrмає накладні витрати часу в конструкторі (для створення лічильника посилань), в деструкторі (для зменшення лічильника посилань і, можливо, знищення об'єкта) та в операторі присвоєння (для збільшення лічильника посилань). Завдяки гарантіям безпеки потоків std::shared_ptr, ці прирощення / зменшення є атомними, що додає ще кілька накладних витрат.

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

Підводячи підсумок, є деякі накладні витрати, але це не повинно сповільнювати код, якщо ви постійно не створюєте та не знищуєте розумні вказівники.


11
unique_ptrне має накладних витрат у деструкторі. Це робиться точно так само, як і з необробленим покажчиком.
Р. Мартіньо Фернандес

6
@ R.MartinhoFernandes порівнюючи з самим необробленим покажчиком, він має час накладних витрат у деструкторі, оскільки сировина-деструктор не робить нічого. Якщо порівнювати, як би, мабуть, використовувався необроблений покажчик, він, безумовно, не має накладних витрат.
lisyarus

3
Варто зауважити, що частина вартості будівництва / знищення / присвоєння shared_ptr пов'язана з безпекою нитки
Джо

1
Крім того, як щодо конструктора за замовчуванням std::unique_ptr? Якщо ви побудуєте a std::unique_ptr<int>, внутрішній файл int*ініціалізується, nullptrподобається вам це чи ні.
Мартін Дроздік

1
@MartinDrozdik У більшості ситуацій ви також можете ініціалізувати необроблений покажчик, щоб перевірити його нікчемність пізніше, чи щось подібне. Тим не менше, додав це до відповіді, дякую.
lisyarus

26

Як і у випадку з усією ефективністю коду, єдиним дійсно надійним засобом отримання важкої інформації є вимірювання та / або перевірка машинного коду.

Тим не менш, прості міркування говорять про це

  • Ви можете очікувати деяких накладних витрат у збірках налагодження, оскільки, наприклад, operator->потрібно виконати як виклик функції, щоб ви могли в нього вступити (це, в свою чергу, пов'язано із загальною відсутністю підтримки для позначення класів та функцій як не налагоджувальних).

  • Оскільки shared_ptrви можете очікувати певних накладних витрат при початковому створенні, оскільки це передбачає динамічне розподіл керуючого блоку, і динамічне розподіл відбувається набагато повільніше, ніж будь-яка інша основна операція в C ++ (використовуйте, make_sharedколи це можливо, щоб мінімізувати ці накладні витрати).

  • Крім того, для shared_ptrмінімальних накладних витрат у підтриманні еталонного підрахунку, наприклад, при передачі shared_ptrзначення за значенням, але немає таких накладних витрат для unique_ptr.

Маючи на увазі першу точку вище, коли ви вимірюєте, зробіть це як для налагодження, так і для версій версій.

Міжнародний комітет зі стандартизації C ++ опублікував технічний звіт про ефективність роботи , але це було у 2006 році, раніше, unique_ptrі вони shared_ptrбули додані до стандартної бібліотеки. І все-таки розумні покажчики були старою шапкою на той момент, тому звіт також вважав це. Процитувавши відповідну частину:

«Якщо доступ до значення через тривіальний розумний вказівник значно повільніший, ніж доступ до нього через звичайний вказівник, компілятор неефективно обробляє абстракцію. Раніше більшість компіляторів застосовували суворі покарання за абстракцію, і деякі нинішні компілятори все ще роблять це. Однак, як повідомляється, щонайменше двом укладачам передбачено покарання за абстракцію нижче 1%, а інше - 3%, тому усунення подібних накладних витрат цілком належить до найсучасніших "

Як поінформована здогадка, на початку 2014 року найпопулярніші компілятори досягли «суто сучасного рівня».


Не могли б ви включити у свою відповідь детальну інформацію про випадки, які я додав до свого запитання?
Венемо

Це могло бути правдою 10 і більше років тому, але сьогодні перевірка машинного коду не така корисна, як припускає людина вище. Залежно від того, як інструкції конвеєруються, векторизуються ... і від того, як компілятор / процесор має справу зі спекуляцією, в кінцевому рахунку залежить, наскільки це швидко. Менший код машинного коду не обов'язково означає швидший код. Єдиний спосіб визначити ефективність - це його профіль. Це може змінюватися залежно від процесора, а також для кожного компілятора.
Байрон,

Проблема, яку я бачив, полягає в тому, що як тільки спільні_ptrs використовуються на сервері, то використання shared_ptrs починає поширюватися, і незабаром shared_ptrs стає технікою управління пам'яттю за замовчуванням. Отже, ви повторили 1-3% покарань за абстракцію, які беруть знову і знову.
Натан Доромал,

Я думаю, що порівняльний аналіз збірки налагодження є повною і цілковитою втратою часу
Пол Чайлдс,

26

Моя відповідь відрізняється від інших, і мені справді цікаво, чи вони коли-небудь профілювали код.

shared_ptr має значні накладні витрати на створення через виділення пам'яті для керуючого блоку (який зберігає лічильник посилань і список покажчиків на всі слабкі посилання). Це також має величезну обсяг пам'яті через це і той факт, що std :: shared_ptr завжди є двома вказівними кортежами (один до об'єкта, один до керуючого блоку).

Якщо ви передаєте shared_pointer функції як параметр значення, то він буде щонайменше в 10 разів повільніше, ніж звичайний виклик і створює безліч кодів у сегменті коду для розмотування стека. Якщо ви передаєте його за посиланням, ви отримуєте додатковий непрямий характер, який також може бути набагато гіршим з точки зору продуктивності.

Ось чому ви не повинні цього робити, якщо ця функція дійсно не бере участь в управлінні власністю. В іншому випадку використовуйте "shared_ptr.get ()". Він не призначений для того, щоб переконатися, що ваш об'єкт не загинув під час звичайного виклику функції.

Якщо ви збожеволієте і застосуєте shared_ptr на невеликих об'єктах, як абстрактне дерево синтаксису в компіляторі, або на невеликих вузлах будь-якої іншої структури графіків, ви побачите величезне зниження продуктивності та величезне збільшення пам'яті. Я бачив систему аналізатора, яку переписали незабаром після появи на ринку C ++ 14 і до того, як програміст навчився правильно використовувати розумні вказівники. Перезапис був на величину повільніше, ніж старий код.

Це не срібна куля, і сировинні покажчики теж непогані за визначенням. Погані програмісти - це погано, а поганий дизайн - погано. Проектуйте обережно, розробляйте з чітким володінням і намагайтеся використовувати shared_ptr здебільшого на межі API підсистеми.

Якщо ви хочете дізнатись більше, ви можете подивитися хороший виступ Ніколая М. Джосуттіса про "Справжню ціну спільних покажчиків у C ++" https://vimeo.com/131189627
Це глибоко заглиблюється в деталі реалізації та архітектуру процесора для бар'єрів запису, атомні замки тощо. Після прослуховування ви ніколи не будете говорити про дешевість цієї функції. Якщо ви просто хочете довести величину повільніше, пропустіть перші 48 хвилин і спостерігайте, як він запускає приклад коду, який працює до 180 разів повільніше (компілюється з -O3) при використанні повсюдного спільного вказівника.


Дякую за вашу відповідь! На якій платформі ви зареєструвались? Чи можете ви підкріпити свої претензії деякими даними?
Венемо,

У мене немає номера для показу, але ви можете знайти його в розмові Ніко Джосуттіса vimeo.com/131189627
Лотар

6
Ви коли-небудь чули std::make_shared()? Крім того, я вважаю, що демонстрації грубих зловживань є трохи нудними ...
Дедулікатор

2
Все, що "make_shared" може зробити, це захистити вас від одного додаткового розподілу та дати вам трохи більше місцевості кешу, якщо блок управління розміщений перед об'єктом. Це зовсім не може не допомогти, коли ви передаєте вказівник навколо. Це не корінь проблем.
Лотар

14

Іншими словами, чи буде мій код повільніше, якщо я використовую смарт-покажчики, а якщо так, то наскільки повільніше?

Повільніше? Швидше за все, ні, якщо ви не створюєте величезний індекс, використовуючи shared_ptrs, і у вас недостатньо пам’яті до того моменту, коли ваш комп’ютер починає зморщуватися, як стара леді, яку невдалою силою вдарив об землю здалеку.

Що може зробити ваш код повільнішим, це мляві пошуки, непотрібна обробка циклу, величезні копії даних та багато операцій запису на диск (як сотні).

Переваги розумного вказівника пов’язані з управлінням. Але чи потрібні накладні витрати? Це залежить від вашої реалізації. Скажімо, ви перебираєте масив із 3 фаз, кожна фаза має масив з 1024 елементів. Створення smart_ptrдля цього процесу може бути надмірним, оскільки після завершення ітерації ви будете знати, що її потрібно стерти. Таким чином, ви можете отримати додаткову пам'ять від використання smart_ptr...

Але чи справді ти хочеш це зробити?

Одноразовий витік пам’яті може призвести до того, що ваш продукт вчасно вийде з ладу (припустимо, ваша програма витікає 4 мегабайта щогодини, щоб зламати комп’ютер пішли місяці, тим не менше, він зламається, ви це знаєте, тому що витік є) .

Це все одно, що сказати: "Ви гарантуєте програмне забезпечення протягом 3 місяців, тоді зателефонуйте мені для обслуговування".

Отже, врешті-решт, це справді питання ... чи можна впоратися з цим ризиком? з використанням необробленого вказівника для обробки вашої індексації над сотнями різних об'єктів варто втратити контроль над пам'яттю.

Якщо відповідь так, тоді використовуйте сирий вказівник.

Якщо ви навіть не хочете розглянути це, smart_ptrце хороше, життєздатне та дивовижне рішення.


4
Гаразд, але valgrind добре перевіряє можливі витоки пам’яті, тож поки ви ним користуєтесь, ви повинні бути в безпеці ™
сірий вовк

@Paladin Так, якщо ти вмієш працювати з пам’яттю, smart_ptrсправді корисний для великих команд
Claudiordgz

3
Я використовую унікальний_ptr, він спрощує багато речей, але не любить shared_ptr, підрахунок посилань не дуже ефективний GC, і його не ідеально
Graywolf

1
@Paladin Я намагаюся використовувати сирі вказівники, якщо можу все інкапсулювати. Якщо це щось, що я буду передавати всюди, як аргумент, то, можливо, я розгляну smart_ptr. Більшість моїх унікальних_птрів використовуються у великій реалізації, як основний чи запущений метод
Claudiordgz

@Lothar Я бачу, що ви перефразовували одну з речей, про які я сказав у вашій відповіді: Thats why you should not do this unless the function is really involved in ownership management... чудова відповідь, дякую, схвалено
Claudiordgz

0

Тільки для огляду і лише для []оператора він на 5 разів повільніше, ніж необроблений покажчик, як показано в наступному коді, який був складений з використанням gcc -lstdc++ -std=c++14 -O0та виведення цього результату:

malloc []:     414252610                                                 
unique []  is: 2062494135                                                
uq get []  is: 238801500                                                 
uq.get()[] is: 1505169542
new is:        241049490 

Я починаю вивчати c ++, я це зрозумів: завжди потрібно знати, що ти робиш, і витрачати більше часу, щоб знати, що робили інші у твоєму c ++.

EDIT

На думку @Mohan Kumar, я надав більше деталей. Версія gcc полягає в тому 7.4.0 (Ubuntu 7.4.0-1ubuntu1~14.04~ppa1), що наведений вище результат був отриманий, коли -O0використовується, однак, коли я використовую прапор '-O2', я отримав такий:

malloc []:     223
unique []  is: 105586217
uq get []  is: 71129461
uq.get()[] is: 69246502
new is:        9683

Потім перейшов до clang version 3.9.0, -O0було:

malloc []:     409765889
unique []  is: 1351714189
uq get []  is: 256090843
uq.get()[] is: 1026846852
new is:        255421307

-O2 було:

malloc []:     150
unique []  is: 124
uq get []  is: 83
uq.get()[] is: 83
new is:        54

Результат удару -O2дивовижний.

#include <memory>
#include <iostream>
#include <chrono>
#include <thread>

uint32_t n = 100000000;
void t_m(void){
    auto a  = (char*) malloc(n*sizeof(char));
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}
void t_u(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

void t_u2(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    auto tmp = a.get();
    for(uint32_t i=0; i<n; i++) tmp[i] = 'A';
}
void t_u3(void){
    auto a = std::unique_ptr<char[]>(new char[n]);
    for(uint32_t i=0; i<n; i++) a.get()[i] = 'A';
}
void t_new(void){
    auto a = new char[n];
    for(uint32_t i=0; i<n; i++) a[i] = 'A';
}

int main(){
    auto start = std::chrono::high_resolution_clock::now();
    t_m();
    auto end1 = std::chrono::high_resolution_clock::now();
    t_u();
    auto end2 = std::chrono::high_resolution_clock::now();
    t_u2();
    auto end3 = std::chrono::high_resolution_clock::now();
    t_u3();
    auto end4 = std::chrono::high_resolution_clock::now();
    t_new();
    auto end5 = std::chrono::high_resolution_clock::now();
    std::cout << "malloc []:     " <<  (end1 - start).count() << std::endl;
    std::cout << "unique []  is: " << (end2 - end1).count() << std::endl;
    std::cout << "uq get []  is: " << (end3 - end2).count() << std::endl;
    std::cout << "uq.get()[] is: " << (end4 - end3).count() << std::endl;
    std::cout << "new is:        " << (end5 - end4).count() << std::endl;
}

Я перевірив код зараз, це лише 10% повільно при використанні унікального вказівника.
Мохан Кумар

8
ніколи не -O0тестувати з кодом або налагоджувати. Вихід буде вкрай неефективним . Завжди використовуйте принаймні -O2(або в -O3наш час, оскільки певна векторизація не виконується -O2)
phuclv

1
Якщо у вас є час і ви хочете взяти кава-брейк, візьміть -O4, щоб отримати оптимізацію часу на зв’язок, а всі маленькі крихітні функції абстракції стануть вбудованими та зникнуть.
Лотар,

Ви повинні включити freeвиклик у тест malloc, а також delete[]для нового (або зробити змінну aстатичним), оскільки unique_ptrs викликають delete[]під капотом, у своїх деструкторах.
RnMss
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.