Сколько накладных расходов на интеллектуальные указатели по сравнению с обычными указателями в C ++?

102

Сколько накладных расходов на интеллектуальные указатели по сравнению с обычными указателями в 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?
Стефан
10
Ответ - 42. (другими словами, кто знает, вам нужно профилировать свой код и понимать свое оборудование для вашей типичной рабочей нагрузки.)
Ним,
Ваше приложение должно максимально использовать интеллектуальные указатели, чтобы быть значимым.
user2672165
Стоимость использования shared_ptr в простой функции установки ужасна и приведет к многократным 100% накладным расходам.
Lothar

Ответы:

179

std::unique_ptr имеет накладные расходы на память, только если вы предоставите ему какой-нибудь нетривиальный удалитель.

std::shared_ptr всегда есть накладные расходы памяти для счетчика ссылок, хотя они очень маленькие.

std::unique_ptr имеет накладные расходы времени только во время работы конструктора (если он должен скопировать предоставленный удалитель и / или инициализировать указатель нулевым значением) и во время деструктора (чтобы уничтожить принадлежащий объект).

std::shared_ptrимеет накладные расходы времени в конструкторе (для создания счетчика ссылок), в деструкторе (для уменьшения счетчика ссылок и, возможно, уничтожения объекта) и в операторе присваивания (для увеличения счетчика ссылок). Из-за гарантий безопасности потоков std::shared_ptr, эти приращения / уменьшения являются атомарными, что приводит к дополнительным накладным расходам.

Обратите внимание, что ни у одного из них нет накладных расходов времени на разыменование (получение ссылки на принадлежащий объект), в то время как эта операция кажется наиболее распространенной для указателей.

Подводя итог, можно сказать, что есть некоторые накладные расходы, но они не должны замедлять код, если вы не будете постоянно создавать и уничтожать интеллектуальные указатели.

лисярус
источник
11
unique_ptrне имеет накладных расходов в деструкторе. Он делает то же самое, что и необработанный указатель.
Р. Мартиньо Фернандес
6
@ R.MartinhoFernandes по сравнению с самим необработанным указателем, у него есть накладные расходы времени в деструкторе, поскольку деструктор необработанного указателя ничего не делает. По сравнению с тем, как, вероятно, будет использоваться необработанный указатель, у него наверняка нет накладных расходов.
лисярус
3
Стоит отметить, что часть затрат на создание / разрушение / назначение shared_ptr связана с безопасностью потоков
Джо
1
Кроме того, как насчет конструктора по умолчанию std::unique_ptr? Если вы создаете a std::unique_ptr<int>, внутренняя int*инициализируется nullptr, нравится вам это или нет.
Мартин Дроздик
1
@MartinDrozdik В большинстве случаев вы также инициализируете нулевым указателем необработанный указатель, чтобы позже проверить его нулевое значение или что-то в этом роде. Тем не менее добавил это к ответу, спасибо.
лисярус
26

Как и в случае с производительностью любого кода, единственный действительно надежный способ получения достоверной информации - это измерение и / или проверка машинного кода.

Тем не менее, простые рассуждения говорят, что

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

  • Поскольку shared_ptrвы можете ожидать некоторые накладные расходы при первоначальном создании, поскольку это включает в себя динамическое выделение блока управления, а динамическое выделение выполняется намного медленнее, чем любая другая базовая операция в C ++ (используйте, make_sharedкогда это практически возможно, чтобы минимизировать эти накладные расходы).

  • Также shared_ptrесть некоторые минимальные накладные расходы на поддержание счетчика ссылок, например, при передаче shared_ptrпо значению, но таких накладных расходов нет для unique_ptr.

Помня о первом пункте выше, при измерении делайте это как для отладочной, так и для выпускной сборок.

Комитет по стандартизации C ++ Международного опубликовал технический отчет о работе , но это было в 2006 году, до того unique_ptrи shared_ptrбыли добавлены к стандартной библиотеке. Тем не менее, умные указатели на тот момент были уже устаревшей, поэтому в отчете учитывалось и это. Цитата из соответствующей части:

«Если доступ к значению с помощью тривиального интеллектуального указателя происходит значительно медленнее, чем доступ к нему с помощью обычного указателя, компилятор неэффективно обрабатывает абстракцию. В прошлом у большинства компиляторов были значительные недостатки абстракции, а в некоторых текущих компиляторах все еще есть. Тем не менее, по крайней мере, два компилятора имеют штраф за абстракцию ниже 1% и еще один штраф в 3%, так что устранение такого рода накладных расходов вполне соответствует современным требованиям ».

Как обоснованное предположение, по состоянию на начало 2014 года, наиболее популярные компиляторы достигли «уровня современного уровня техники».

Приветствия и hth. - Альф
источник
Не могли бы вы включить в свой ответ некоторые подробности о случаях, которые я добавил в свой вопрос?
Venemo
Это могло быть правдой 10 или более лет назад, но сегодня проверка машинного кода не так полезна, как предлагает человек выше. В зависимости от того, как инструкции конвейеризированы, векторизованы и ... и как компилятор / процессор справляется со спекуляциями, в конечном итоге зависит их скорость. Меньшее количество машинного кода не обязательно означает более быстрый код. Единственный способ определить производительность - это профилировать ее. Это может меняться в зависимости от процессора, а также от компилятора.
Байрон
Проблема, которую я видел, заключается в том, что, как только shared_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) при повсеместном использовании общего указателя.

Лотар
источник
Спасибо за Ваш ответ! На какой платформе вы работали? Можете ли вы подкрепить свои претензии некоторыми данными?
Venemo
У меня нет номера , чтобы показать, но вы можете найти некоторые в Нико Josuttis ток vimeo.com/131189627
Лотара
6
Слышали когда-нибудь std::make_shared()? Кроме того, я нахожу демонстрации вопиющего неправильного использования немного скучными ...
Дедупликатор
2
Все, что может сделать make_shared, - это уберечь вас от одного дополнительного выделения и дать вам немного больше места в кэше, если блок управления размещен перед объектом. Это не может не помочь, когда вы передаете указатель. Не в этом корень проблем.
Lothar
14

Другими словами, будет ли мой код медленнее, если я использую интеллектуальные указатели, и если да, то насколько медленнее?

Помедленнее? Скорее всего, нет, если только вы не создаете огромный индекс с помощью shared_ptrs и у вас недостаточно памяти до такой степени, что ваш компьютер начинает морщиться, как старуха, которая падает на землю невыносимой силой издалека.

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

Все преимущества умного указателя связаны с управлением. Но нужны ли накладные расходы? Это зависит от вашей реализации. Допустим, вы выполняете итерацию по массиву из 3 фаз, каждая фаза имеет массив из 1024 элементов. Создание smart_ptrдля этого процесса может быть излишним, поскольку после завершения итерации вы будете знать, что вам нужно его стереть. Таким образом, вы можете получить дополнительную память, не используя smart_ptr...

Но вы действительно хотите этого?

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

Это все равно что сказать: «У вас гарантия на программное обеспечение 3 месяца, тогда звоните мне в сервис».

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

Если да, используйте необработанный указатель.

Если вы даже не хотите об этом думать, smart_ptrэто хорошее, жизнеспособное и отличное решение.

Клаудиордгз
источник
4
хорошо, но valgrind хорошо проверяет возможные утечки памяти, так что пока вы его используете, будьте в безопасности ™
graywolf
@Paladin Да, если вы справитесь со своей памятью, smart_ptrони действительно полезны для больших команд
Claudiordgz
3
Я использую unique_ptr, он упрощает многие вещи, но мне не нравится shared_ptr, подсчет ссылок не очень эффективный
сборщик мусора,
1
@Paladin Я стараюсь использовать необработанные указатели, если могу все инкапсулировать. Если это что-то, что я буду распространять повсюду в качестве аргумента, то, возможно, я рассмотрю smart_ptr. Большинство моих unique_ptr используются в большой реализации, например, в методах main или run
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 ++.

РЕДАКТИРОВАТЬ

Как сообщил @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

Результат clang -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;
}
liqg3
источник
Я протестировал код сейчас, он всего на 10% медленнее при использовании уникального указателя.
Мохан Кумар
8
никогда не тестируйте -O0и не отлаживайте код. Выход будет крайне неэффективным . Всегда используйте хотя бы -O2(или в -O3настоящее время, потому что некоторая векторизация не выполняется -O2)
phuclv
1
Если у вас есть время и вы хотите перерыв на кофе, возьмите -O4, чтобы оптимизировать время компоновки, и все маленькие крошечные функции абстракции станут встроенными и исчезнут.
Lothar
Вы должны включить freeвызов в тест malloc и delete[]для new (или сделать переменную aстатической), потому что unique_ptrs вызываются delete[]под капотом в своих деструкторах.
RnMss