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

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

В частности, я спрашиваю о С++ 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);

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

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

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

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

Как известно, "хорошо в современном уровне" было достигнуто с самыми популярными компиляторами сегодня, с начала 2014 года.

Ответ 3

Мой ответ отличается от других, и мне действительно интересно, если они когда-либо профилировали код.

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) при использовании повселокального использования общего указателя.

Ответ 4

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

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

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

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

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

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

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

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

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

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

Ответ 5

Просто для взгляда и только для оператора [], он примерно в 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++.

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

Для дальнейшего пояснения я использовал gcc version 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;
}