Как сделать код максимально быстрым?
Функция clock имеет очень низкое разрешение, кажется 16ms. Это слишком грубо для вашей задачи.
Внутренний цикл лишний. Во внешнем цикле делите число на первый делитель, если остаток нулевой, вы нашли пару делителей.
Поиск делителей логично начинать от меньших к большим. Первый делитель ищите в диапазоне [1, sqrt(N)] .
Найдя один делитель можно искать следующие пары делителей не для исходного числа, а для числа уже разделённого. Это сильно ускорит процесс, хотя пары делителей для оригинального числа надо будет восстанавливать.
Обычно делители не ищут парами. Число разлагают на простые: факторизация целых чисел. Затем из простых можно сконструировать все нужные пары. Это самый быстрый способ.
Примеры
Оригинальная программа videx. Работает медленно, так как проверяет все пары чисел до N (число, которое факторизуем). Сложность O(N^2) . Почти все пары печатаются два раза. Если N > 46340 (= sqrt(2^31)) из-за переполнения ошибается.
Предложена user419509. Делит N на все числа до sqrt(N) . Сложность O(sqrt(N)) . N < 2^31 .
Предложена Harry. Делит N на все нечетные числа до sqrt(N) . Если делитель найден, то сокращает на него N и продолжает поиск. Сложность в худшем случае O(sqrt(N)) . N < 2^32 . Разложения на множители делаются из факторизации отдельным проходом.
Содержит таблицу простых чисел до 2^16 . Делит N на все простые числа до sqrt(N) . N < 2^32 .
Делает wheel factorization для простых 2, 3, 5, 7, 11, 13 . Делит N на все числа из "колеса" до sqrt(N) . N < 2^64 .
В таблице приведены худшие времена работы, когда N разлагается в произведение двух примерно равных простых чисел:
Немного размышлений и советов по оптимизации кода на С++
Эту статью я написал достаточно давно для своего блога, который теперь заброшен. Мне кажется, в ней есть весьма полезная информация, поэтому не хотелось бы, чтобы она просто исчезла. Очень может быть, что-то уже устарело, буду благодарен, если мне на это укажут.
Как правило, язык C++ используют там, где требуется высокая скорость работы. Но на C++ без особых усилий можно получить код, работающий медленнее какого-нибудь Python/Ruby. Именно подобным кодом оперируют многочисленные сравнения Any-Lang vs C++.
Вообще, оптимизация бывает трех типов:
- Оптимизация уже готового, проверенного и работающего кода.
- Изначально написание оптимального кода.
- Просто использование оптимальных конструкций.
Второй тип оптимизации — это изначальное проектирование кода с учетом требований к производительности. Такое проектирование не является ранней оптимизацией.
Третий тип даже не совсем оптимизация. Скорее это избегание неоптимальных языковых конструкций. Язык C++ довольно сложный, при его использовании частенько нужно знать, как реализован используемый код. Он достаточно низкоуровневый, чтобы программисту пришлось учитывать особенности работы процессоров и операционных систем.
1. Особенности языка C++
1.1. Передача аргументов
Передавайте аргументы по ссылке или по указателю, а не по значению. Передача аргументов по значению ведет к полному копированию объекта. И чем больше этот объект, тем больше придется копировать. А если объект класса выделяет память в куче, тогда совсем беда. Конечно же, простые типы можно и нужно передавать по значению. Но сложные следует передавать только по ссылке или по указателю.
1.2. Исключения
Используйте исключения только там, где это действительно необходимо. Дело в том, что исключения — достаточно тяжелый механизм. Поэтому не следует использовать их в качестве замены goto , выхода из вложенных циклов и тому подобных вещей. Простое правило: исключения генерируются только в исключительных ситуациях. Это не значит, что от них нужно отказаться совсем. Само использование исключений дает мизерные накладные расходы из-за небольшого количества дополнительного кода. Настоящее влияние на производительность оказывает только неправильное их использование.
1.3. RTTI
В коде, от которого требуется высокая скорость работы, не используйте RTTI. Механизм RTTI в большинстве компиляторов (или во всех?) реализован через сравнение строк. Чаще всего это не критически важно. Но иногда от кода может потребоваться высокая скорость работы. В этом случае следует придумать другое решение, например числовые идентификаторы классов.
1.4. Инкремент и декремент
Используйте префиксную форму инкремента и декремента. У разработчика на языке C++ должно войти в привычку везде использовать только префиксную форму ( ++i и —i ) и лишь при необходимости постфиксную форму ( i++ ). Постфиксная форма реализована за счет сохранения и возврата временного значения объекта до его изменения. Конечно, в простых случаях, в операциях со встроенными типами, компилятор сможет оптимизировать код и обойтись без создания временной переменной. Но в случае пользовательского класса, вероятно, оптимизировать не будет.
1.5. Не создавайте временные объекты — 1
Временные объекты создают, к примеру, вот таким кодом:
В данном случае создается два лишних временных объекта: std::string tmp1 = s1 + s2; и std::string tmp2 = tmp1 + s3; . Правильная конкатенация строк выглядит вот так:
1.6. Не создавайте временные объекты — 2
Переменную можно объявить в любом месте. И если эта переменная — сложный объект, то не следует объявлять его в местах, где он может и не понадобиться. Пример:
1.7. Резервирование памяти
Возвращаясь к предыдущему примеру (п. 1.5) — совсем правильный метод конкатенации должен быть таким:
Операция выделения памяти очень дорогая. И, предварительно выделив ее один раз вызовом reserve , можно сэкономить много процессорного времени. В случае STL это относится к классам vector и string .
1.8. Вообще избегайте лишней работы
Мне казалось, этот совет есть в любой книге для начинающего, да и базового понимания C++ должно хватать, чтобы понять… Однако вижу, что некоторые неопытные программисты на это натыкаются.
В строке 1 вызывается конструктор std::string(const char *) , который не знает о том, что строка пустая. Он будет пытаться выяснить ее длину, выполнить выделение памяти и цикл копирования, иметь обработчик нехватки памяти и т. д. Это дороже, чем просто
В строке 2 та же самая ситуация. Выполняется operator = (const char *s) , который также не знает о том, что «программист» всего лишь хотел получить пустую строку. Эффективней будет простой вызов:
1.9. Оценивайте стоимость вызова функции в циклах for/while
Используя STL, можно не беспокоиться о том, что вызов функции дорог:
Потому что в данном случае он дешев. Это будет эквивалентно следующему коду:
Однако это не всегда так. Частое заблуждение до C++11 было в том, что программисты ожидали такой же алгоритмической сложности от std::list::size , хотя во множестве реализаций его сложность была O(N). Особенно неприятно это смотрелось там, где вместо вызова if( list.empty() ) выполняли if( list.size() > 0 ) .
1.10. Не используйте vector там, где можно было бы обойтись list или deque
Контейнер vector предназначен для хранения в памяти непрерывной последовательности байтов. Поэтому при добавлении новых элементов, если памяти не хватит, контейнеру придется выделить новую память и копировать данные из старого места в новое. Если это происходит часто, то производительность кода может быть снижена значительно. В отличие от vector , контейнеры list или deque не хранят непрерывную последовательность данных, поэтому копирование не требуется.
С другой стороны, использование vector с предварительным резервированием (т. е. однократным выделением всей необходимой памяти) — самый быстрый и экономный способ. Потому что в случае list или deque небольшие куски памяти выделяются много раз. При выборе контейнера следует думать, какие именно операции над ним будут выполняться.
1.11. Ссылки или указатели?
Старайтесь использовать ссылки, а не указатели. Ссылки не требуют проверок. Ссылка непосредственно указывает на объект, а указатель содержит адрес, который нужно прочитать.
1.12. Список инициализации конструктора
Инициализируйте переменные в списке инициализации конструктора. В противном случае получается, что сначала они будут инициализированы, а потом им присваивается значение.
2. Компиляторы
Компилятор способен выполнять множество различных оптимизаций. Иногда ему следует помочь. Иногда, наоборот, попытка оптимизировать вручную приведет к тому, что компилятор не сможет оптимизировать код так, как сделал бы это без подобной «помощи».
2.1. Разворачивание циклов
Современные процессоры содержат несколько функциональных устройств (блоки ALU и FPU) и способны выполнять команды параллельно, т. е. за один такт на одном ядре может быть выполнено несколько команд. Поэтому разворачивание цикла позволяет выполнить операцию за меньшее число шагов. Также разворачивание уменьшает количество сравнений и условных переходов:
Должно быть развернуто во что-то вроде
Вот часть ассемблерного кода, без switch :
Видно, что в данном случае инкрементирование идет не по 4, а по 8 байт. Дополнительные условия внутри цикла или же вычисления, влияющие на счетчик цикла, приведут к невозможности развернуть цикл.
Разворачиванием циклов компилятор занимается самостоятельно. Ему следует помочь только тем, чтобы это можно было сделать. Также небольшие циклы желательно объединять в один. Но при наличии условий или большого тела цикла, наоборот, бывает лучше разбить на несколько циклов, чтобы хотя бы один из них был развернут компилятором.
2.2. Ленивость вычислений — 1
Следует помнить, что условия && (логическое И) и || (логическое ИЛИ) компилятор обрабатывает слева направо. При вычислении логического И, если первое условие ложно, второе даже не будет вычисляться. Соответственно, в логическом ИЛИ при истинности первого условия нет смысла вычислять второе. Вот простой пример:
Нам необходима строка больше трех символов, чтобы первым символом был y. При этом strlen(s) — дорогая операция, а s[0] == ‘y’ — дешевая. Соответственно, если поменять их местами, то, возможно, вычислять длину строки и не придется:
2.3. Ленивость вычислений — 2
Ленивость вычислений работает только до тех пор, пока вы не перегрузили оператор && или || . Перегруженный оператор представляет из себя вызов функции:
bool operator && (аргумент1, аргумент2)
Следовательно, все аргументы должны быть вычислены до вызова.
2.4. Switch или if?
Когда это возможно, старайтесь использовать switch . В отличие от условия if , switch частенько оптимизируется через таблицу. Пример:
А вот такой код:
2.5. Ключевое слово inline
Казалось бы, это ключевое слово как раз и придумано для того, чтобы ускорять программы. Но некоторые понимают это слишком буквально и начинают вставлять inline перед каждой функцией. Это приводит к тому, что код раздувается. Чем больше код, тем больше ему требуется памяти, и особенно памяти в кеше процессора. Современные компиляторы давно уже перестали обращать внимание на это слово и сами решают, когда стоит делать функцию встраиваемой, а когда нет. Но программисты все равно стараются раздуть код, вставляя что-нибудь вроде __forceinline . Использовать inline следует только там, где это действительно необходимо.
2.6. RVO — Return Value Optimization
Эта оптимизация позволяет компилятору C++ не создавать копию возвращаемого значения. Следует помочь компилятору использовать эту оптимизацию.
Поэтому одна точка выхода хоть и красивее, но менее эффективна:
Этот совет почти потерял актуальность, так как компиляторы научились хорошо использовать NRVO, к тому же появилась возможность перемещения. Однако не всегда NRVO может быть задействована, и не у всех объектов есть конструктор перемещения.
2.7. Выравнивание структур
В объявлении классов и структур старайтесь располагать переменные по убыванию их размера. Особенно нужно уделить внимание группировке вместе переменных, чей размер меньше 8 байт. Компиляторы, в целях оптимизации, выравнивают адреса переменных, потому что обращение к переменной с типом long по выровненному адресу занимает всего один такт процессора, а если переменная не выровнена, то два такта на архитектуре i386. На некоторых архитектурах читать по невыровненному адресу вообще нельзя. Грубо говоря, невыровненная переменная располагается в нескольких ячейках памяти: первая часть в одной и часть в следующей. Так вот, благодаря этому выравниванию переменная размером 1 байт займет 4 или 8 байт. Вот иллюстрирующий пример:
На моей машине вывод будет следующий:
Тут видно, что выравнивание велось по границе четырех байт. И совершенно одинаковые классы Foo и Bar занимают в памяти разный объем. Обычно на это можно и не обращать внимания. Но если требуется создать тысячи экземпляров класса, то вариант Bar предпочтительней. Разумеется, сам компилятор не имеет права переставлять переменные.
Конечно же, не следует рассчитывать размер каждой переменной с целью максимально плотно их упаковать. Размер переменных может зависеть от компилятора, параметров компиляции и архитектуры. Также не следует подавлять выравнивание без реальной необходимости.
3. Многопоточность
Важно знать, что написание многопоточного кода заключается не в расставлении в правильном месте объектов синхронизации, а в написании кода, который не требует синхронизации.
3.1. Атомарные операции
Вообще, почти любое обращение к ядру — это дорогая операция. Как с памятью, так и со многими другими вызовами. Чем меньше таких обращений делает программа, тем лучше. В случае синхронизации дополнительные накладные расходы создает необходимость переключать контекст при конкуренции. Поэтому, если есть большая конкуренция и синхронизация выполняется с помощью мьютекса / критической секции, накладные расходы могут быть очень серьезными. И чем больше конкуренция, тем они значительнее. Вот пример плохого кода из довольно известных программ (на момент написания статьи) LinuxDC++/StrongDC++ и, вероятно, других подобных программ, основанных на одном и том же коде:
Этот код компилируется для сборки под ОС Linux. При этом для Windows код правильный:
Разница в том, что для Linux используются критические секции, а для Windows — атомарные операции, не требующие тяжелых мьютексов.
3.2. Cache Line
Старайтесь не допускать обращений разных потоков к близко расположенным участкам памяти. К примеру, имеется вот такая структура:
и потоки, обращающиеся к одной из переменных. Если потоки будут выполняться на разных ядрах, то произойдет событие, называемое Cache line ping-pong: когда двум разным ядрам необходимо видеть изменения друг друга и для этого приходится сбрасывать кеш и запрашивать данные из оперативной памяти. В подобных случаях, когда потокам требуются разделяемые данные, надо вставить между переменными кусок памяти, который поместится в Cache-Line процессора. Сложность в том, что размер этого Cache-Line у каждого процессора свой. Я ориентируюсь на значение 128 байт:
4. Операционные системы
Это тот уровень, на который следует спускаться, только если хорошо понимаешь используемые функции. Ловушки могут подстерегать в самых неожиданных местах.
4.1. Память
Старайтесь избегать частого выделения памяти. Это очень дорогая операция. И разница между «выделить 100 Мб» и «выделить 1 Мб» небольшая. Поэтому надо стараться организовать код так, чтобы заранее выделить большой объем памяти и использовать его без обращений к ядру ОС.
Если необходимо часто выделять память, учитывайте, что встроенный в стандартную библиотеку аллокатор неэффективен, особенно в случае активных операций с памятью из параллельных потоков. Рассмотрите возможность использования альтернативного аллокатора вроде nedmalloc или TCMalloc или пулов памяти вроде Boost.Pool.
4.2. Буферизация ввода-вывода
Системные вызовы вроде read/write или ReadFile/WriteFile не используют буферизацию. Поэтому при чтении одного байта будет прочитан целый блок данных и из него отдан один-единственный байт. При чтении следующего байта снова пойдет чтение этого же самого блока. Аналогично при записи: запись одного байта приведет к немедленной записи этого байта. Это очень и очень неэффективно. Поэтому следует использовать функции, которые обеспечивают буферизацию, к примеру fread/fwrite .
5. Процессоры
5.1. RAM уже давно не RAM
RAM расшифровывается как Random Access Memory. Однако на сегодняшний день попытка использовать оперативную память как источник с быстрым случайным доступом не приведет ни к чему хорошему. Потому что доступ к памяти занимает несколько сотен тактов процессора!
И единственное, что спасает, — кеш процессора, доступ к которому стоит около десятка тактов (источник). Из этого следует, что надо стараться делать так, чтобы все нужные данные размещались в кеше процессора. Это не только данные программы, но и сам код. По возможности нужно использовать последовательный доступ к памяти. При организации структуры вроде хеш-таблицы или ассоциативной структуры ключи не должны содержать лишней информации, чтобы максимальное их количество поместилось в кеше.
5.2. Signed или unsigned?
Чаще всего программисты не задумываются о том, должна ли переменная быть со знаком или нет. Например, длина строки — она ведь не может быть отрицательной, так же как и вес или цена чего-либо и многие другие значения. Вероятно, диапазона значений для знакового числа вполне хватает для хранения нужного значения, однако еще имеется разница в инструкциях процессора. К примеру, вот этот код:
будет транслирован в
получится значительно короче:
Типичные места, где предпочтительно использовать беззнаковые числа: деление и умножение, счетчики в циклах, индексы массивов.
Типы данных с плавающей точкой не могут быть беззнаковыми. Но для них используются специальные команды процессора.
5.3. Ветвления
Конвейер процессора потому и называется конвейером, что обрабатывает непрерывный поток команд. Процессор безостановочно поставляет команды на конвейер, чтобы после выполнения одной команды он сразу же принялся за другую. Но когда встречается ветвление, т. е. условие if … else , процессор не знает, для какой ветки следует использовать команды — для if или else . Поэтому он пытается предсказать, какая из них будет использована. В случае ошибки в предсказании приходится сбрасывать данные конвейера и загружать в него новые команды, при этом конвейер простаивает.
Это означает, что чем раньше предсказатель переходов поймет, по какой ветке пойдет выполнение программы, тем меньше вероятность сброса конвейера. Обычно рекомендуется располагать наиболее вероятную ветку в самом начале (т. е. в условии if ).
6. Заключение
Итак, в этой статье мы рассмотрели некоторые способы оптимизации кода на С++. Надеюсь, какие-то из них оказались вам полезны. Если вы знаете другие способы, не упомянутые в статье, пишите их в комментариях!
C2018/Оптимизации
В большинстве случаев операции с целыми операциями быстрые, независимо от разрядности. Нежелательно использовать переменные размера больше, чем самый большой доступный размер регистра. Другими словами, неэффективно использовать 64-битные целые числа в 32-битных системах, особенно если код включает в себя умножение или деление.
Компилятор всегда будет выбирать наиболее эффективный целочисленный размер, если вы объявите переменную как int. Целые числа меньших размеров (char, short int) лишь немного менее эффективны. Во многих случаях компилятор будет преобразовывать эти типы в целые числа по умолчанию при выполнении вычислений, а затем использовать только младшие 8 или 16 бит результата. Можно считать, что преобразование типа занимает ноль или один такт.
В 64-битных системах разница между эффективностью 32-разрядных целых чисел и 64-разрядных целых чисел крайне мала, если вы не выполняете деления.
Беззаковые целые
- Преобразование между знаковыми и беззнаковыми типами бесплатно и не требует никаких дополнительных инструкций.
- Деление беззнакового целого на константу быстрее и выполняется проще, чем знакового, то же касается и взятия остатка.
- Преобразование из целого типа в вещественный быстрее для знакового, чем для беззнакового.
Операции с целыми числами
Можно считать, что сложение, вычитание, сравнение и пр. занимают один такт на большинстве процессоров.
Умножение и деление занимают больше тактов. Умножение занимает 11 тактов на процессорах Pentium 4, и 3–4 такта на других процессорах. Целочисленное деление занимает 40–80 тактов в зависимости от процессора.
Не занимайтесь ручными оптимизациями
Некоторые программисты думают, что они знают много хороших приёмов, которые могут ускорить их код, и вовсю их пихают в свои программы.
- Можно заменить умножение и деление на степени двойки битовыми сдвигами влево и вправо.
- Умножение val * 10 можно переписать через сдвиги и сумму (val<<3) + (val<<1) .
В XXI веке заниматься этим не нужно и даже может быть вредно. Любой современный компилятор для таких простых случаев сам знает, как лучше написать машинный код, чтобы работало быстрее. Используя разные приёмы, вы только ухудшаете читаемость кода на C и делаете его менее понятным и поддерживаемым. Легко допустить ошибку и не заметить её.
Инкремент и декремент
Абсолютно нет разницы по скорости между
более эффективно, чем
Во втором случае для доступа к памяти надо сначала подсчитать новое значение индекса, что может привести к задержке на пару тактов.
Но может быть и наоборот. Так, прединкремент
более эффективен, потому что компилятор заметит, что a и b равны и разместит их в одном регистре.
Вещественные числа
Современные микропроцессоры семейства x86 имеют два разных типа регистров с плавающей точкой и соответственно два разных типа инструкций. У каждого типа есть свои преимущества и недостатки.
x87 FPU
Исходный метод выполнения операций с плавающей запятой включает в себя восемь регистров с плавающей точкой, организованных как стек регистров. Эти регистры имеют длинную двойную точность (80 бит). Преимущества использования стека регистров:
- Все расчеты выполняются с высокой точностью.
- Конвертация между различными разрядностями (80, 64, 32 бита) не требуют дополнительного времени.
- Существуют внутренние инструкции для математических функций, таких как логарифмы и тригонометрические функции.
- Код компактный и занимает мало места в кеше кода.
Есть и недостатки.
- Компилятору сложно создавать регистровые переменные из-за того, что регистры образуют стек.
- Конвертация между целыми числами и вещественными неэффективна.
- Деление, квадратный корень и математические функции требуют больше времени для вычисления при использовании повышенной точности.
В расширении SSE (Streaming SIMD Extensions, 1999 год) архитектуры x86 добавлены восемь (шестнадцать для x64) 128-битных регистров, которые называются xmm0 — xmm7 (-xmm15). В версии SSE2 их возможности расширены. Один регистр может использоваться одним из следующих способов:
В более новом расширении AVX (Advanced Vector Extensions, 2008 год) эти 128-битные регистры являются младшими половинами 256-битных регистров ymm0 — ymm15.
Если 32-битный процессор x86 может и не поддерживать SSE, то для 64-битного процессора x86-64 обязательно наличие поддержки SSE2. Стековые регистры x87 поддерживаются сегодня вообще везде.
На базе этих регистров и инструкций также можно реализовывать вещественные вычисления. Операции с плавающей точкой выполняются с одинарной (32 бита) или двойной (64 бита) точностью, а промежуточные результаты всегда вычисляются с той же точностью, что и операнды. Преимущества использования векторных регистров:
- Легко создавать регистровые переменные с плавающей запятой.
- Векторные операции доступны для выполнения параллельных вычислений над векторами из двух переменных двойной точности или четырёх переменных одиночной точности в регистрах XMM. Если набор инструкций AVX доступен, то каждый вектор может содержать четыре значения двойной точности или восемь переменных одиночной точности в регистрах YMM.
Недостатки тоже есть:
- Не поддерживается 80-битная точность.
- Для выражений, в которых есть операнды разной разрядности, нужна конвертация, иногда довольно медленная.
- Математические функции реализуются в библиотеке, а не в процессоре.
Особенности
Современные компиляторы на x86-64 предпочитают использовать регистры XMM для реализации вещественной арифметики вместо x87.
Операции с double обычно не занимают больше времени, чем операции с float. Исключение — деление, квадратный корень при использовании регистров XMM будет медленнее для двойной точности, чем для одинарной. Однако одинарная точность даёт выигрыш, если есть большие массивы чисел (лучше помещаются в кеш) и применяются векторные операции.
Сложение с плавающей точкой занимает 3–6 тактов в зависимости от процессора. Умножение занимает 4–8 тактов. Деление занимает 14–45 тактов.
Не смешивайте одинарную и двойную точность при использовании регистров XMM. Конвертация между float и double занимает 2–15 тактов.
Избегайте конверсий между целыми числами и переменными с плавающей точкой, если это возможно. Для знакового целого оно занимает 4–16 тактов, для беззнакового медленнее. Предварительно кастите к знаковому, если уверены, что переполнения не будет.
Обратная конвертация из вещественного в целое на x87 занимала примерно 50–100 тактов, потому что стандарт C требует делать truncation вместо rounding и нужно было переключать эти режимы. Для SSE2 проблема не актуальна.
Ветвления
В современном процессоре важнейшую роль играет предсказатель ветвлений.
Плохо предсказываемое ветвление иногда полезно заменить на таблицу значений, например
H Использование современного С++ для повышения производительности в черновиках Из песочницы
В данной статье я хотел бы рассказать, как использование средств современных стандартов С++ позволяет повысить производительность программ без каких-либо особых усилий от программиста.
Эта статья затрагивает лишь средства языка, а не конкретные техники оптимизации (т.е. такие вещи как локальность памяти, платформозависимые оптимизации, lockfree и прочее остаются за бортом).
Ключевое слово final
Виртуальный метод класса, объявленный с использованием ключевого слова final, не может быть переопределен наследниками.
В некоторых случаях это позволяет компилятору избежать обычных расходов на вызов виртуальной функции (девиртуализация).
Ассемблерный код (здесь и далее: ggc 6.2, -O3 -std=c++14):
Не передавайте умные указатели по ссылке
Умные указатели должны использоваться для определения срока жизни объекта. Не нужно передавать умные указатели по ссылке в метод, если он не производит никаких операций с самим объектом умного указателя, а лишь с объектом, хранящимся в нём.
Используйте Rule of zero
Rule of zero гласит, что для класса, в котором не нужно явно определять деструктор, также не нужно явно определять конструкторы/операторы копирования/перемещения.
Явное определение конструктора копирования запрещает компилятору генерировать конструктор перемещения, что в некоторых случаях может значительно снизить производительность.
Предпочитайте emplace копированию
Начиная с С++11 стандартные контейнеры позволяют конструировать элемент напрямую внутри контейнера, избегая лишнего копирования.
Использования emplace быстрее не только простого копирования, но даже перемещения.
Не используйте shared_ptr, если можно обойтись unique_ptr
Использования shared_ptr несёт за собой определённые расходы. При создании, копировании, удалении shared_ptr обновляет внешний счётчик ссылок на хранимый объект. Также shared_ptr обязан быть потокобезопасным, что тоже может нести за собой соответствующие расходы. В то время как выделение и удаление памяти с использованием unique_ptr вообще никак не отличается от использования ручного управления памятью с использованием new/delete.
Спасибо за внимание!
комментарии ( 16 )
> Не передавайте умные указатели по ссылке
И в примере сравнивается «производительность» умного указателя и значения. Тогда уж надо сравнивать код, который генерируется при передаче умного указателя по значению: копирование умного указателя + разыменование.
Здесь по факту просто определение методу emplace.
Просто перевод первых строчек с http://www.cplusplus.com/reference.
А где вы продемонстрировали новые средства? Я ещё раз повторюсь — C++11 это хорошо и круто, но не ново.
У этих 70% должен быть своеобразный подход к написанию — подача материала, сравнение производительности для разных примеров (таблички там свякие, графики), объяснение причин и где выгоднее использовать один кейс, а где другой. Тогда эта статья может быть хорошей, т.к. становится более наглядной, последовательной, изложенной иначе нежели чем в доке (что может быть полезно, т.к. все усваивают материал по разному).
А отличная статья, это когда ещё ко всему прочему описываются нетривиальные вещи. (так сказать проводится ресёч, что подчёркивает профессионализм автора) Вот пример такой статьи.
Что вы объяснили этим высказыванием? Вот зачем в вашем примере вторая функция? Если я создал smart_ptr, то мне его в любом случае придётся разыменовывать, чтобы передать в эту функцию. А если я создал smart_ptr и храню ещё обычный указатель, то я дебил — т.к. смысла в этом нет (да и философии unique_ptr это противоречит — уже два указателя).
Как пример — простое приложение, которое читает конфиг из файла и на его основе создаёт объекты. На этапе компиляции параметры конструктора для создания объекта не известны, а станут известны после считывания.
Как детальный пример:
1. Вы пишете многофункциональное сетевое приложение. Один из модулей этого приложения анализирует трафик. Модуль запрашивает с удалённого сервера список политик и применяет его к потокам. Здесь не имеет смысла создавать объект «контролЁра политик», пока мы не получим параметры для его создания. Вдруг ответ не придёт или придёт пустой лист — зачем в таком случае держать в памяти объект (особенно если он большой)? Легче держать указатель и как только появятся параметры, создать его(объект).
Если честно, я не совсем понял вопрос и ваш комментарий.
Разыменовывать ничего не стоит по факту — один вызовов функции для объекта умного указателя (operator*) и вы уже имеете указатель на объект.