Комментарии 134
void* operator new (std::size_t size, void* ptr) noexcept;
Который размещает данный объект в память указанную в «ptr» и вызывает его конструктор.
Достаточная простая и удобная оптимизация или просто стратегия хранения сложных объектов которые нуждаются в вызове конструкторов.
Я бы не рекомендовал использовать placement new без крайней необходимости (реализация GC, пулов памяти и прочих "хакерских штучек"): затуманивает смысл кода и представляет собой отличный экземпляр грабель с концом ручки как раз на уровне паха, т.к. деструктор придётся вызывать вручную, а это легко забыть сделать. К тому же, нужно самому следить, чтобы размер памяти был достаточен для размещения объекта, а не то конструктор инициализирует соседний кусок памяти.
деструктор придётся вызывать вручную, а это легко забыть сделатьНо зачем? Можно просто подсунуть unique_ptr правильный deleter:
#include <memory>
#include <type_traits>
#include <iostream>
class MyClass
{
public:
MyClass() { std::cout << "constructor\n"; }
~MyClass() { std::cout << "destructor\n"; }
void DoSomething() { std::cout << "doing something\n"; }
};
struct DeleterCallingDestructor
{
template<typename T>
void operator()(T* ptr) const { ptr->T::~T(); }
};
int main()
{
std::aligned_storage<sizeof(MyClass), alignof(MyClass)> storage;
std::unique_ptr<MyClass, DeleterCallingDestructor> ptr(new MyClass);
ptr->DoSomething();
return 0;
}
#include <memory>
#include <type_traits>
#include <iostream>
class MyClass
{
public:
MyClass() { std::cout << "constructor\n"; }
~MyClass() { std::cout << "destructor\n"; }
void DoSomething() { std::cout << "doing something\n"; }
};
struct DeleterCallingDestructor
{
template<typename T>
void operator()(T* ptr) const { ptr->T::~T(); }
};
int main()
{
std::aligned_storage<sizeof(MyClass), alignof(MyClass)>::type storage;
std::unique_ptr<MyClass, DeleterCallingDestructor> ptr(new(&storage) MyClass);
ptr->DoSomething();
return 0;
}
MyClass
. Что вы предлагаете с этим кодом делать? Копировать? Но тогда вы легко можете изменить только одно упоминание, оставив остальные два неизменными. И это будет работать или падать в зависимости от состояния звёзд.Чтобы это работало надёжно — это всё нужно завернуть в какую-нибудь конструкцию, параметризованною типом
MyClass
. Про что, собственно, и статья :-)std::shared_ptr::reset() и std::unique_ptr::reset() созданы для использования с оператором new()
Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области.
Не надо пугаться использовать new(), надо пугаться не уметь делать вменяемый дизайн.
std::make_unique появился только в c++14
В статье говорится о современном С++. Современный С++ на данный момент и есть С++14.
std::shared_ptr::reset() и std::unique_ptr::reset() созданы для использования с оператором new()
Если быть более точным, они созданы для работы с владеющими указателями. Оператор new — один из способов такой указатель создать. Так или иначе, если в С++ есть инструмент Х, это не значит, что им нужно обязательно пользоваться. В С++ вообще такая философия, что давайте сначала накидаем инструментов, а потом посмотрим, как оно полетит. Наберёмся опыта и выработаем гайдлайны, как этим на самом деле нужно пользоваться. Вот можно ловить исключения по значению, но так делать не надо, потому как слайсинг, исключение при копировании исключения и всё такое. Можно вернуть из функции константный объект по значению, но это не имеет смысла и может скорее только навредить. Можно сделать виртуальную функцию шаблонной или добавить её аргументам значения по умолчанию, но проблем не оберёшься потом. Можно объявить неконстистентные операторы сравнения, но тогда их уже нельзя будет назвать операторами сравнения. Примеров можно много набрать. Поэтому сейчас и развиваются C++ Core Guidelines. Убрать какой-то инструментарий из языка мы не можем — обратная совместимость дело святое. Остаётся оградить разработчиков от неразумного использования существующего инструментария. Именно от неразумного. Т.к. под каждый пример при желании можно подобрать специфическую ситуацию, где следует отойти от гайдлайнов. Но это другой разговор. Это особенные случаи. И в статье я указал на факт, что в особенных ситуациях без использования new и delete нам не обойтись.
Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области.
Простите, это вы о чём конкретно?
Не надо пугаться использовать new(), надо пугаться не уметь делать вменяемый дизайн.
Не вижу, как это связано. По-моему, это ортогональные вещи.
В статье говорится о современном С++. Современный С++ на данный момент и есть С++14.
Простите, но для меня, например, современный язык — это тот, который использует отрасть. Нельзя, в общем случае, просто так взять и перейти на c++14. Если компания разрабатывает продукт целиком и полностью — это одно дело. Если, как например в automotive, для создания одной машинки задействованы сотни OEM поставщиков, то данная статья даже вредна для неокрепших умов юниоров.
Простите, это вы о чём конкретно?
я про это:
Саттер и Майерс в своё время всё разложили по полочкам
Избегайте использования new и delete в коде. Воспринимайте их как низкоуровневые операции ручного управления динамической памятью.
std::shared_ptr<A> a;
....
a = std::make_shared<A>();
Не пугайтесь и не пугайте юниоров использовать new() в коде:
std::shared_ptr<A> a;
....
a.reset(new A());
но для меня, например, современный язык — это тот, который использует отрасть
Вопрос терминологии. Мне казалось логичным, что «современным» называют язык, который стандартизован и поддерживается рядом мажорных компиляторов.
Даже если кто-то что-то и сказал в прошлом, не надо считать это догмой вне зависимости от контекста и предметной области
Я не хотел называть это догмами. Без фанатизма. Да, С++ отличается широким применением и, как следствие, сильно варьируется для разных контекстов и предметных областей. Однако, можно выделить некоторые общие рекомендации, которые работают в большинстве случаев. Например, с мажорными компиляторами. Такие рекомендации вполне можно считать умолчаниями. Если в конкретном контексте или области они неприменимы — без проблем. Просто нужно понимать, почему они не применимы. Возможно, в связи со спецификой конкретного компилятора или платформы (например, не реализованы определённые оптимизации, не поддерживаются определённые инструменты). Возможно, в связи со спецификой предметной области (например, жёсткое ограничение на время отклика системы, которое требует абсолютной прозрачности потока выполнения). Но умолчания это не меняет.
Не пугайтесь и не пугайте юниоров использовать new() в коде: a.reset(new A());
В качестве исключения — до, конечно, можно. По умолчанию — нет. И в статье описано, почему.
data::Value *data = new data::Value(std::move(val));
thread.perform([this, data] () -> bool {
updateFromData(*data);
delete data;
return true;
});
В С++14 беда исправлена (можно перемещать внутрь захвата лямбды), но он ещё не везде есть.
delete
:auto* data = new data::Value(std::move(val));
thread.perform([this, data] () -> bool {
std::unique_ptr<data::Value> dataPtr(data);
updateFromData(*data);
return true;
});
Другой вариант — использовать
shared_ptr
. Во многих случаях вполне себе вариант. Время работы нити обычно несравнимо больше времени копирования shared_ptr
.В С++14, действительно, можно использовать init capture. Оставлю здесь пример для полноты:
auto data = std::make_unique<data::Value>(std::move(val));
thread.perform([this, data = std::move(data)]() -> bool {
updateFromData(*data);
return true;
});
1) давать нам объекты без оператора new
2) следить за зависимостями у выполнять роль некоего GC
3) содержать ральзичные стратегии выделения памяти
Зачем? Вы поличите в итоге Java, на котором ещё и писать неприятно. С хорошим дизайном первые два пункта просто не нужны. Последний пункт частично реализован в STL (см. параметр allocator
у контейнеров).
- Шаблоны и статический полиморфизм (policy-based design).
- RAII.
Всё это отлично описано в книге Modern C++ Design. Да, реализации GC тоже есть, но нужны они в 0.01% случаев.
Ни одна из этих трех задач не является чем-то, чем должен заниматься DI Container
Так о чём это я, спасибо за статью, надеюсь вас услышат многие динозавры и перестанут портить нам нервы.
Но меня всегда смущали лишние операции при создании/удалении объектов (особенно при удалении, где есть проверка на ref_count, то есть ветвление). Умом понимаю: всё от проклятого желания переоптимизировать код. Но из-за этого часто так и тянет ручное управление использовать где стоит и где не стоит.
Достаточно усвоить одно правило: заниматься оптимизацией нужно только по данным профайлера. Если что-то тормозит — запускаешь профайлер и смотришь, что именно. Для того, чтобы не тормозило, нужно разумно использовать алгоритмы. А низкоуровневые оптимизации нужно использовать только тогда, когда всё упирается в алгоритмы (впрочем, большого выигрыша в такой ситуации всё равно не получишь, как правило).
Вообще, у многих пишущих на C++ прямо паранойя какая-то в отношении скорости работы кода. Да и я, когда начал разрабатывать под Android, поначалу параноил по поводу тормозной Java. Потом успокоился, когда заметил, что в подавляющем большинстве случаев тормоза были вызваны небрежно написанным кодом уровня студента третьего курса.
впрочем, большого выигрыша в такой ситуации всё равно не получишь, как правилоНе знаю кто придумал это правило и чем он его мотивировал. Из моей практики: код написанный с присмотром «вполглаза» в сгенерированный ассемблер и код, написанный с подходом «если что-то тормозит — запускаешь профайлер и смотришь, что именно» до запуска профайлера отличаются по скорости раз в 10-20, после запуска и «сшибания верхов» — раза в 2-3.
Вообще, у многих пишущих на C++ прямо паранойя какая-то в отношении скорости работы кода.Что, как бы, логично. С++ — сложный, хитрый, опасный инструмент. Если вы не добиваетесь при его использовании максимума производительности и готовы терпеть замедление раза в 2-3, то, может быть, стоит взять что-то другое? Java, C#, Go, в конце-концов?
Так или иначе, вполне можно без палева подпихнуть в std свою реализацию make_unique (см. http://stackoverflow.com/a/12580468/261217):
namespace std
{
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
}
Когда компилятор будет наконец обновлён, её нужно будет удалить, а весь код будет работать точно так же. Когда мы в прошлой компании некоторое время застряли на Visual Studio 2010, мы ровно так и поступили.
Подпихнуть свои костыли можно, но это в реальных проектах дело не вполне тривиальное — надо во все файлы включать один общий хедер. Чем это городить, проще уж new воткнуть
> надо во все файлы включать один общий хедер
Достаточно часто такие хедеры имеются уже готовые. Какие-нибудь самые базовые, утилитарные. Если есть pch — ещё проще. Но, конечно, всё индивидуально. Только вы сами можете решить, какое решение лучше подходит в вашем случае. Со своей стороны я лишь надеюсь, что смог собрать в одном месте достаточно информации, чтобы можно было взвесить за и против.
К примеру, вот у нас самый распоследний CentOS 7, с ним идет gcc 4.8.Уже как минимум есть GCC 4.9, думаю скоро и GCC 5 будет доступен как в RHEL 7.
Подпихнуть свои костыли можно, но это в реальных проектах дело не вполне тривиальное — надо во все файлы включать один общий хедерЗачем? include_next никто не отменял.
В общем кто хочет работать — ищет способы, кто не хочет — ищет причину.
Если еще учесть, что у нас помимо gcc/linux/x86 есть еще кросскомпиляция на gcc/linux/arm и компиляция msvs/windows/x86 (и куча third-party библиотек, не дающих обновиться на visual studio 2015), то апгрейд компилятора становится задачей, требующей очень серьезного подхода. А ограничения имеющихся компиляторов дают о себе знать — то в одном regex криво работает, то в другом инициализация статических локальных переменных не атомарна, и т.д.
Костыль с include_next и /FI — в принципе, решение; если понадобится, то будем использовать.
Про devtoolset слышал, но не очень в курсе, как обстоят дела с установкой собранных таким образом программ на другие машины.Ставите, запускаете. Никаких дополнительных пакетов ставить не нужно: рантайм он использует старый (разница между, скажем, GCC 4.4 из RHEL 6 и GCC 5.3 там оформлена как статическая библиотека, вкомпилируемая в вашу программу). Тем devtoolset и отличается от обычного GCC, собранного «руками», собственно.
А вместо symlink'а libstdc++.so в devtoolset — это linker script, автоматически вытаскиващий что возможно из libstdc++.so, а оставшееся — из libstdc++_nonshared.a… Таким образом вы можете получить все фичи C++ 14 (ну… почти: там кой-какие баги есть, так что Boost.Hana не поиспользуешь) с рантаймом от GCC 4.8 (или даже, при необходимости, GCC 4.4 — для совместимости с RHEL 6).
Думаю что и с кросскомпиляторами можно сделать то же самое — было бы желание.
В реальном мире иногда требуется иметь возможность для копиляции под разные платформы и с разными тулчейнами. Эти требования приходят со стороны заказчика: «вот вам железо, вот вам компилятор, запустите свое рефренс приложение и мы посмотрим, будем ли мы с вами заключать контракт» (утрированно).
Поэтому и сидят в CI инструментах различные задачи под несколько компиляторов и несколько платформ и никто в здравом уме не делит мир на черное (застряли в прошлом веке) и белое (современные компиляторы) с юношеским задором.
.
std::unique_ptr ptr = new T(...);Надеюсь тут опечатка)
make_unique можно использовать уже сейчас https://isocpp.org/files/papers/N3656.txt
Майерс отлично прокомментировал это свойство программистов на С++ в своём докладе Why C++ Sails When the Vasa Sank.Кстати, на Хабре была статья с основными тезисами этого доклада: https://habrahabr.ru/company/infopulse/blog/227529/.
У вас список сокращённый, в полном списке вы так же не пишете:
- Код деструктора. Если что-то нужно освобождать в деструторе, значит это что-то ранее было инициализировано, что по сути снова new/delete.
- Код копирующего конструктора. Правило трём(пяти), и опять же — если что-то нужно нетривиально копировать, значит стоит задуматься о дизайне.
Ещё вариант, где от реализации конструкторов с деструкторами не отказаться: классы, реализующие union. Но этот инструмент очень злой и коварный, можно легко отстрелить ногу в нескольких местах по самую шею. Как правило, union это оптимизация на уровне укладки данных в памяти, что суть высшие материи, и относится скорее к чистому C.
У меня есть живой проект, который начали, кажется, еще в 2002 (или 2001?) году на Qt 1.45. Я его перетаскиваю потихоньку от версии к версии, постепенно созревая, чтобы переписать его с нуля. Если бы я лез к нему в потроха с появлением каждого нового стандарта языка — то уже переписал бы минимум дважды.
Для короткоживущего проекта (3-5 лет) С++ подходит замечательно. Для долговременного (LTS) — язык слишком изменчив, чтобы писать на нем код, который будет работать через 10-15 лет, без серьезных вмешательств.
И с жабой (Java) та же история, и много еще с чем.
Поэтому я ушел на pure C, к которому цепляю через обертки библиотеки.
Для начала, зачем вообще перетаскивать в него новые фишечки плюсов? Это legacy code, либо он работает, и вы его не трогаете, либо пишете новый по всем современным канонам. Делать микс — это самое большое зло.
Далее, 2002 год. Код на плюсах в 2002 году не так уж сильно отличался от кода на C. Во всяком случае, прогали в основном в стиле «С с классами». Поэтому когда я слышу «проект 2002» года — это скорее всего намешанные malloc и new, процедурный стиль, венгерская нотация, конструкции вида ** и *& и все в таком же духе.
Насчет LTS. Это не так. Основные претензии к мастодонтам, написанные на C — как во всем этом разобраться, если ты не бородатый дядька, который все это написал? Поэтому и начинают написать всякие проекты типа clang, призванные вытеснить gcc.
Самое сложное искусство на С++, к которому, как кажется мне, все и придираются — как писать средний код. Средний — в том плане, что в нем и новичок сможет довольно быстро разобраться, и гуру не будет плеваться. Современный C++ как раз движется в этом направлении, и это прекрасно. Все эти function, lambda, smart pointers и прочее — как раз помогают писать красивый, понятный и лаконичный код, без всяких трюков и низкоуровневых хаков. При том, что в частный случаях все трюки и хаки еще по старой совместимости доступны. По-моему, это отлично.
Гм. Я, вроде бы, такого не говорил. :) Хотя, я, конечно, пытался сделать микс, но вовремя остановился.
> Все эти function, lambda, smart pointers и прочее — как раз помогают писать красивый, понятный и лаконичный код
При условии правильного проектирования даже не базовых классов, а базовых шаблонов.
Простой пример — автомобиль. 4 колеса, кузов, двигатель внутреннего сгорания… опаньки! тесла-мобиль… что делать будем?
Если мы не заложились в самом начале на существование других двигателей, кроме ДВС, то мы попали — у нас рвется шаблон. :)
Если заложились — то мы влипаем в класс engine по полной программе.
> Основные претензии к мастодонтам, написанные на C — как во всем этом разобраться, если ты не бородатый дядька, который все это написал? Поэтому и начинают написать всякие проекты типа clang, призванные вытеснить gcc.
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.
Гм. Я, вроде бы, такого не говорил. :) Хотя, я, конечно, пытался сделать микс, но вовремя остановился.
Ну вы говорили, что вот вышел стандарт — можно лезть и переписывать. А я говорю — нафига?
Простой пример — автомобиль.
Плохой пример, из которого вообще никаких проблем не видно. Решений может быть море, начиная от шаблонной магии (мы же знаем, какие характеристики нужны нам для расчетов) до нескольких реализации ДВС, наследуемых от базового класса движка.
Конечно, нужно думать и проектировать. И это на любом языке. На С, если прогать по принципу «фигак-фигак и в продакш», происходит тоже самое — через некоторое время вообще непонятно, как это поддерживать.
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.
clang?
Ну, как по мне, то наилучшим аргументом за C++ был бы компилятор с C++, написанный (включая runtime) только на C++.
Микрософт, кстати, не так давно переписал свою CRT на С++. Так что уже свершилось. (Ну и да, даже GCC уже некоторое время написан частично на C++)
До С++11 даже проблем с дунгрейтом практически не было, если знаешь C++ ошибки быстро исправишь.
К примеру, один из моих проектов должен работать и в одной старой ОСи, в которой только только gcc 2.9*. Неудобно конечно, но код( со всеми плюшками) компилируется как с новым gcc, так и со старым.
В то время как чистый Си — скорее всего выдаст несколько варнингов.
зы. На самом деле мне больше всего не нравится, когда набор библиотек языка становится его частью. Этакий VendorLock. А C++ идет по этому пути очень радостно (или у меня паранойя разыгралась? :)
Я пользуюсь C++ проектом который начали писать в 1989. Код местами выглядит странно по сегодняшним меркам, но все компилируется. Как вам уже много раз говорили, в вашем случае проблема в Qt а не в C++. В С++ несовместимых изменений практически не было.
А сейчас что, вдруг стала нелегальной?
Все стандартные классы лежат в std::
, поэтому не могут вызвать проблем, если не делать using namespace std;
, что всегда считалось плохой практикой.
std::
они переехали в 98м году, с выходом стандарта C++98. С более ранними версиями совместимость действительно… не очень — там, в частности, все эти вещи лежали в глобальном namespace
(потому что других не было).Ну так и программы, написанные на C до ~85го года (до выхода проектов ANSI C стандарта) вы современными компиляторами, зачастую, собрать не сможете. В чём разница?
Сделайте using namespace
на уровне данной функции или вообще на уровне блока. Вызванные потенциальные проблемы будут минимальны.
И рассчитывать на то, что 15 лет назад рандомный коллега не зафигачил в проект using namespace std я бы точно не стал.Ага.
А если он вот такой код зафигачит:
Пример из реального проекта, если что… теперь и C тоже не использовать? В машинных годах писать?char *safe_malloc(size) unsigned size; { char *p; extern char *malloc(); if ((p = malloc(size)) == (char *) 0) { cleanupHandler(heap_no_mem,"safe_malloc"); } return p; }
Для короткоживущего проекта (3-5 лет) С++ подходит замечательно. Для долговременного (LTS) — язык слишком изменчив, чтобы писать на нем код, который будет работать через 10-15 лет, без серьезных вмешательств.Так вот это — бред. 100% совместимость может быть только со 100% неизменяемым язком. На любое изменение всегда можно придумать код, который будет этим изменением сломан.
Поэтому я ушел на pure C, к которому цепляю через обертки библиотеки.
Код, который написан на С или C++ аккуратно — работает отлично и через 10 лет и через 20. Код, который, я извиняюсь, написан через жопу — не работает ни там, ни там.
Так что совершенно неясно — чего вы добиваетесь переходом с С на C++.
100% совместимость может быть только со 100% неизменяемым язком. На любое изменение всегда можно придумать код, который будет этим изменением сломан
Здесь вы расширение языка не называете изменением?
autoconf
-скрипты смотрели? Они бывают как позитивными (если что-то компилируется/работает, то делаем так), так и негативными (если что-то не компилируется, то делаем эдак). Любое изменение может поломать негативный тест (просто по определению), но даже позитивные могут быть сломаны если они сделаны через «одно место» (например залазят в «зарезервированные» индентификаторы типа _Bool
).Речь идет о том, что код, написанный 15 лет назад на c++ почти наверняка не скомпилируется современными компиляторами.Вы это проверяли? Или так — вещаете о том, о чём понятия не имеете?
В то время как чистый Си — скорее всего выдаст несколько варнингов.
Я, среди прочего, гоняю на работе SPEC2000 и SPEC2006. Которые, как несложно догадаться, появились примерно как раз ~15 лет назад. Количество костылей, которые нужно пристраивать со всех сторон к тестам на C и на C++ — вполне сравнимо.
И во всех случаях это — результат использования плохого стили. Чем программа на C объявляющая внутри функции
malloc
вместо того, чтобы заголовочный файл включить (300.twolf) или не использующая include guard'ов (253.perlbmk), в сущности, отличается от программы на C++ с using namespace std
(252.eon)? Да ничем: во всех случаях кому-то нужно было очень сильно надо «дёшево» «заткнуть проблему» — а через 15 лет это аукается… Вряд ли можно это отнести к недостаткам языка: «свинья грязь везде найдёт»…На самом деле мне больше всего не нравится, когда набор библиотек языка становится его частью.А чем это плохо, собственно? Посмотрите на PHP: то, что там куча всего-всего-всего прямо сразу из коробки — всем, скорее, нравится, не нравится то, что это всё-всё-всё свалено в кучу без всякой системы… К тому же Python'у (где библиотек лишь немногим меньше, но они более-менее разумно организованы) претензий нет вообще.
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.
Но я поспорю с автором.
New/delete — это не враги, а наши очень влиятельные друзья, к помощи которых мы можем прибегать только в серьезных ситуациях. C++ без new/delete, malloc/free — это уже не C++, а хрень на постном масле. Я надеюсь, что очередной 0xXX не забудет, что C++ задумывался как язык для упрощения работы с реальным железом, так сказать высокоуровневый ассемблер… а не солянка из кучи синтаксических наворотов. Думаю, что в скором будущем 0xXX неизбежно выделится в новый язык, как в свое время C превратился в C++, ибо тем, кто глаголит на старославянском, не с руки писать один код с теми, кто не чтит мудрость предтечей :)
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete.
0xXX неизбежно выделится в новый язык
Да выделился уже: D. Просто он почти никому не нужен.
— Вы это лично проверяли?
Вот c++ / ассемблерный код, который выдает C++Builder XE3 для auto_ptr.
struct MyStruct
{
int x;
int y;
};
MyStruct* p = new MyStruct;
delete p;
std::auto_ptr ap(new MyStruct);
ap.reset();
832: MyStruct* p = new MyStruct;
00423BE6 6A08 push $08
00423BE8 E80F7C0400 call $0046b7fc
00423BED 59 pop ecx
00423BEE 8945CC mov [ebp-$34],eax
833: delete p;
00423BF1 FF75CC push dword ptr [ebp-$34]
00423BF4 E8F77B0400 call $0046b7f0
00423BF9 59 pop ecx
835: std::auto_ptr ap(new MyStruct);
00423BFA 66C745E80C00 mov word ptr [ebp-$18],$000c
00423C00 6A08 push $08
00423C02 E8F57B0400 call $0046b7fc
00423C07 59 pop ecx
00423C08 50 push eax
00423C09 8D55FC lea edx,[ebp-$04]
00423C0C 52 push edx
00423C0D E83A000000 call std::auto_ptr::auto_ptr(MyStruct *)
00423C12 83C408 add esp,$08
00423C15 FF45F4 inc dword ptr [ebp-$0c]
00423C18 66C745E81800 mov word ptr [ebp-$18],$0018
836: ap.reset();
00423C1E 6A00 push $00
00423C20 8D4DFC lea ecx,[ebp-$04]
00423C23 51 push ecx
00423C24 E883000000 call std::auto_ptr::reset(MyStruct *)
00423C29 83C408 add esp,$08
Думаю разница теперь Вам очевидна?
К сожалению, unique_ptr данная версия компилятора не поддерживает, поэтому если Вы замените auto_ptr на unique_ptr и привидете ассемблерный код своего компилятора, будет интересно посмотреть.
По шаблонам можно проследить что, к примеру, unique_ptr всегда делает лишнюю проверку вида
if (data != nullptr)
delete data;
тогда как прямой вызов delete очевидно этой проверки не проводит. Это связано с желанием авторов unique_ptr правильно хэндлить всякие ситуации типа такой
mystruct *x = new mystruct;
std::unique_ptr<mystruct> p(x);
p.reset(x); // здесь выполняется проверка что в reset не передали тот же самый указатель
По шаблонам можно проследить что, к примеру, unique_ptr всегда делает лишнюю проверку видаЗачем оно там? Читаем спецификацию на operator delete: указатель на область памяти для освобождения или нулевой указатель. Если вы находите подобный код «по шаблонам», то это значит только одно: разработчики вашего компилятора малость налажали и/или немного слишком сильно перестраховались.
if (data != nullptr) delete data;
Я дополнил ваши несколько строк до полной программы:
#include <memory>
struct MyStruct
{
int x;
int y;
};
extern void foo(MyStruct *);
void bar() {
MyStruct* p = new MyStruct;
delete p;
}
void baz() {
std::unique_ptr<MyStruct> ap(new MyStruct);
ap.reset();
}
$ g++ -O3 -std=c++14 -fno-exceptions -S test.cc -o- | c++filt .file "test.cc" .section .text.unlikely,"ax",@progbits .LCOLDB0: .text .LHOTB0: .p2align 4,,15 .globl bar() .type bar(), @function bar(): .LFB1818: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $8, %edi call operator new(unsigned long) movl $8, %esi movq %rax, %rdi addq $8, %rsp .cfi_def_cfa_offset 8 jmp operator delete(void*, unsigned long) .cfi_endproc .LFE1818: .size bar(), .-bar() .section .text.unlikely .LCOLDE0: .text .LHOTE0: .section .text.unlikely .LCOLDB1: .text .LHOTB1: .p2align 4,,15 .globl baz() .type baz(), @function baz(): .LFB1819: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $8, %edi call operator new(unsigned long) movl $8, %esi movq %rax, %rdi addq $8, %rsp .cfi_def_cfa_offset 8 jmp operator delete(void*, unsigned long) .cfi_endproc .LFE1819: .size baz(), .-baz() .section .text.unlikely .LCOLDE1: .text .LHOTE1: .ident "GCC: (GNU) 5.3.0" .section .note.GNU-stack,"",@progbits
Но что будет если структуру для чего-нибудь поиспользовать?
#include <memory>
struct MyStruct
{
int x;
int y;
};
extern void foo(MyStruct *);
void bar() {
MyStruct* p = new MyStruct;
foo(p);
delete p;
}
void baz() {
std::unique_ptr<MyStruct> ap(new MyStruct);
foo(ap.get());
ap.reset();
}
$ g++ -O3 -std=c++14 -S test2.cc -o- | c++filt .file "test2.cc" .section .text.unlikely,"ax",@progbits .LCOLDB0: .text .LHOTB0: .p2align 4,,15 .globl bar() .type bar(), @function bar(): .LFB1860: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl $8, %edi call operator new(unsigned long) movq %rax, %rbx movq %rax, %rdi call foo(MyStruct*) movq %rbx, %rdi movl $8, %esi popq %rbx .cfi_def_cfa_offset 8 jmp operator delete(void*, unsigned long) .cfi_endproc .LFE1860: .size bar(), .-bar() .section .text.unlikely .LCOLDE0: .text .LHOTE0: .section .text.unlikely .LCOLDB1: .text .LHOTB1: .p2align 4,,15 .globl baz() .type baz(), @function baz(): .LFB1861: .cfi_startproc .cfi_personality 0x3,__gxx_personality_v0 .cfi_lsda 0x3,.LLSDA1861 pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 pushq %rbx .cfi_def_cfa_offset 24 .cfi_offset 3, -24 movl $8, %edi subq $8, %rsp .cfi_def_cfa_offset 32 .LEHB0: call operator new(unsigned long) .LEHE0: movq %rax, %rdi movq %rax, %rbx .LEHB1: call foo(MyStruct*) .LEHE1: addq $8, %rsp .cfi_remember_state .cfi_def_cfa_offset 24 movq %rbx, %rdi movl $8, %esi popq %rbx .cfi_def_cfa_offset 16 popq %rbp .cfi_def_cfa_offset 8 jmp operator delete(void*, unsigned long) .L5: .cfi_restore_state movq %rax, %rbp movq %rbx, %rdi movl $8, %esi call operator delete(void*, unsigned long) movq %rbp, %rdi .LEHB2: call _Unwind_Resume .LEHE2: .cfi_endproc .LFE1861: .globl __gxx_personality_v0 .section .gcc_except_table,"a",@progbits .LLSDA1861: .byte 0xff .byte 0xff .byte 0x1 .uleb128 .LLSDACSE1861-.LLSDACSB1861 .LLSDACSB1861: .uleb128 .LEHB0-.LFB1861 .uleb128 .LEHE0-.LEHB0 .uleb128 0 .uleb128 0 .uleb128 .LEHB1-.LFB1861 .uleb128 .LEHE1-.LEHB1 .uleb128 .L5-.LFB1861 .uleb128 0 .uleb128 .LEHB2-.LFB1861 .uleb128 .LEHE2-.LEHB2 .uleb128 0 .uleb128 0 .LLSDACSE1861: .text .size baz(), .-baz() .section .text.unlikely .LCOLDE1: .text .LHOTE1: .ident "GCC: (GNU) 5.3.0" .section .note.GNU-stack,"",@progbits
delete
содержит в себе ошибку (неправильно обрабатывается случай, когда foo
бросает исключение), а версия с std::unique_ptr
этот случай обрабатывает правильно — о чём, собственно, и статья. Ну и, конечно, если мы скажем, что исключения нас не волнуют, то:$ ~/work/gcc5/bin/g++ -O3 -std=c++14 -fno-exceptions -S test2.cc -o- | c++filt .file "test2.cc" .section .text.unlikely,"ax",@progbits .LCOLDB0: .text .LHOTB0: .p2align 4,,15 .globl bar() .type bar(), @function bar(): .LFB1818: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl $8, %edi call operator new(unsigned long) movq %rax, %rbx movq %rax, %rdi call foo(MyStruct*) movq %rbx, %rdi movl $8, %esi popq %rbx .cfi_def_cfa_offset 8 jmp operator delete(void*, unsigned long) .cfi_endproc .LFE1818: .size bar(), .-bar() .section .text.unlikely .LCOLDE0: .text .LHOTE0: .section .text.unlikely .LCOLDB1: .text .LHOTB1: .p2align 4,,15 .globl baz() .type baz(), @function baz(): .LFB1819: .cfi_startproc pushq %rbx .cfi_def_cfa_offset 16 .cfi_offset 3, -16 movl $8, %edi call operator new(unsigned long) movq %rax, %rbx movq %rax, %rdi call foo(MyStruct*) movq %rbx, %rdi movl $8, %esi popq %rbx .cfi_def_cfa_offset 8 jmp operator delete(void*, unsigned long) .cfi_endproc .LFE1819: .size baz(), .-baz() .section .text.unlikely .LCOLDE1: .text .LHOTE1: .ident "GCC: (GNU) 5.3.0" .section .note.GNU-stack,"",@progbits
В общем я поигрался с компиляторами и могу сказать, что «код с обёртками» при использовании нормальных компиляторов получается практически идентичным тому, который получается без их использования. Лет 10 назад я не очень верил, что до этого дойдёт, но… факты — упрямая вещь.
А аргумент «я очень-очень забочусь о скорости работы кода на недокомпиляторах» — он немного того, шизифренией попахивает. Если вас волнует скорость кода — зачем вы используете компилятор, в котором качество кодогенерации, как всем известно, «не ахти»? А если «не очень волнует» и «есть причины использовать именно этот, не очень качественно оптимизирующий компилятор» — то зачем ловко стрелять из-под колена встав в позу из Кама-Сутры и грозя отстерить себе чего-нибудь?
Зачем оно там?
Я же написал зачем, даже пример привел. Если в reset() тупо сделать delete без проверки на указатель, то второй приведенный пример кода сломается. Менее понятно зачем схожая проверка есть в деструкторе, но похоже это дань универсальности — в unique_ptr можно, к примеру, запихать дескриптор потока с кастомным деструктором который этот поток закрывает, или свой собственный умный указатель. Есть даже целая интересная тема на тему того что гарантированное стандартом поведение (вызывать deleter для нулевого указателя или нет) отличается для unique_ptr и shared_ptr:
http://stackoverflow.com/questions/11164354/does-the-standard-behavior-for-deleters-differ-between-shared-ptr-and-unique-ptr
А так да, в реализации STL от GCC похоже аккуратно написали специализацию для «простых» случаев и это еще раз показывает всю крутизну C++. Но к сожалению не все реализации STL столь же вылизаны.
Было бы любопытно глянуть что ICC сгенерит, но лень сейчас на рабочую машину лезть и проверять )
но лень сейчас на рабочую машину лезть и проверять )
Используйте http://gcc.godbolt.org/ для таких случаев.
delete
не скрыта в недрах какой-нибудь вызываемой функции, а вставляется компилятором прямо по месту самого delete
(что логично, т.к. иначе мы не смогли бы получать преимущества от знания, что в этой точке указатель всегда ненулевой). В комментарии ниже есть искусственный пример, показывающий, что в обоих случаях код будет идентичным.Зачем оно там?В реальности, там в шаблоне конструкция вида:
if (get() != nullptr)
get_deleter()(get());
для поддержки нестандартных deleter'ов (стандарт обязывает делать такую проверку и вызывать их только для ненулевых указателей). И да, в ситуациях, когда компилятор не может доказать (в примерах выше то у него всего на виду), что указатель ненулевой, проверка останется. Но и для простого delete
компилятор прямо по месту такую проверку вставит, т.к., как вы верно указали, по стандарту delete
от нулевого указателя абсолютно легален и должен просто не делать ничего, а вызывать, к примеру, какой-нибудь нетривиальный деструктор, передав ему нулевой указатель, не самая лучшая идея. Вот синтетический пример, в обоих случаях идет проверка.Вообще, по логике, если мы уверены, что в этой точке указатель не нулевой, то мы можем «подсказать» это компилятору, написав какое-нибудь не делающее ничего действие с использованием разыменования этого указателя (скажем,
delete &*p;
). Тогда компилятор имеет право считать, что указатель всегда ненулевой, т.к. иначе происходит UB. На практике, я чуток поигрался, и компиляторы (пока?) такие подсказки не воспринимают, нужно что-то очень явное с объектом сделать (например, вывести его на экран), чтоб такая оптимизация заработала. Возможно, у кого-нибудь получится найти рабочий способ?// здесь выполняется проверка что в reset не передали тот же самый указательЯ не уверен, кстати, что подобная проверка (если я верно понял то, что в ситуации равенства обоих указателей при ней ничего не происходит) не нарушает стандарт. Стандарт (C++11 20.7.1.2.5p4) говорит нам о поведении
reset()
следующее:Effects: assignsКак видим, вышеупомянутой проверки стандарт не предусматривает, а значит в точкеp
to the stored pointer, and then if the old value of the stored pointer,old_p
, was not equal to nullptr, callsget_deleter()(old_p)
.
p.reset(x);
должен быть вызван деструктор для x
и освобождена память. Да, можно сказать, что дальше в момент вызова деструктора p
у нас возникает UB из-за повторного удаления, и поэтому мы можем делать, что захотим. Ну а вдруг такого не будет (например, ниже мы вызовем release()
)?здесь выполняется проверка что в reset не передали тот же самый указатель
Каким образом, если сравнение идёт с нулевым указателем? Собственно, на cppreference указано:
A test for self-reset, i.e. whether ptr points to an object already managed by *this, is not performed, except where provided as a compiler extension or as a debugging assert.
clang
перейти. Некоторые вещи в проектах на Windows можно собрать только MSVC, так как они завязаны на непереносимые расширения MSVC — ну так clang
и MSVC совместимы на уровне объектников, можно делать как Chromium: собирать всё сначала clang
'ом, а то, что не собралось (обычно буквально пара-тройка файлов) — уже MSVC.Но это экономия на спичках, на практике разница будет ничтожно мала
Где-то возможно востребована даже такая экономия, но ИМХО даже если Вам нужна максимальная производительность, то в 99% случаев Вы выиграете намного больше соптимизировав что-то другое.
Тут, многие пишут, что это все не правильный компилятор и он несет не правильный мед, мол «правильный» всё оптимизирует и всё будет ровно, а самое интересно, тот компилятор который им нравится — и есть самый «правильный». Какая наивность…
Код, который был приведен выше — был абсолютно тривиальный и без включенной оптимизации, дабы показать какая реальная разница между кодом реализации new/delete и auto_ptr. Компилятор легко оптимизирует такие тривиальные примеры, но в реальных программах, где для создания/удаления объектов есть еще и условия и обращения к члена класса, он совсем не Чак Норис, и может пропустить кучу апперкотов и пару хуков от программиста.
Господа, попробуйте запустить вот этот код, и проверить «крутость» своих компиляторов ;)
struct MyStruct
{
int x;
int y;
};
void test2_work(MyStruct* in_p, int& r)
{
if(in_p->x == in_p->y)
r = in_p->x + in_p->y;
}
void test2_new(MyStruct** in_p, int i)
{
*in_p = new MyStruct;
(*in_p)->x = i;
(*in_p)->y = i;
}
void test2_delete(MyStruct** in_p)
{
delete *in_p;
}
void test2_auto(std::auto_ptr& in_ap, int i)
{
in_ap.reset(new MyStruct);
in_ap->x = i;
in_ap->y = i;
}
void test2_awork(std::auto_ptr& in_ap, int& r)
{
if(in_ap->x == in_ap->y)
r = in_ap->x + in_ap->y;
}
void test2_reset(std::auto_ptr& in_ap)
{
in_ap.reset();
}
int _tmain(int argc, _TCHAR* argv[])
{
MyStruct* p_main;
std::auto_ptr ap_main;
int r=0;
int t1, t2;
int k = 20000000;
printf(«Start test auto!\r\n»);
t1 = GetTickCount();
for (int i = 0; i < k; i++)
{
test2_auto(ap_main, i);
test2_awork(ap_main, r);
test2_reset(ap_main);
}
t2 = GetTickCount();
printf(«Result auto t=%d, r=%d\r\n», t2-t1, r);
printf(«Start test new/delete!\r\n»);
t1 = GetTickCount();
for (int i = 0; i < k; i++)
{
test2_new(&p_main, i);
test2_work(p_main, r);
test2_delete(&p_main);
}
t2 = GetTickCount();
printf(«Result new/delete t=%d, r=%d\r\n», t2-t1, r);
printf("-------- Surprize -------\r\n");
printf(«Start test auto (2)!\r\n»);
t1 = GetTickCount();
{
std::auto_ptr as(new MyStruct);
for (int i = 0; i < k; i++)
{
as->x = i;
as->y = i;
test2_awork(as, r);
}
}
t2 = GetTickCount();
printf(«Result auto (2) t=%d, r=%d\r\n», t2-t1, r);
printf(«Start test new/delete (2)!\r\n»);
t1 = GetTickCount();
{
MyStruct* s = new MyStruct;
for (int i = 0; i < k; i++)
{
s->x = i;
s->y = i;
test2_work(s, r);
}
delete s;
}
t2 = GetTickCount();
printf(«Result new/delete (2) t=%d, r=%d Fatality!!!!!!\r\n», t2-t1, r);
getch();
return 0;
}
x-----------------------------x
x------- Результат -------x
x-----------------------------x
Start test auto!
Result auto t=5203, r=39999998
Start test new/delete!
Result new/delete t=2282, r=39999998
x-------- Surprize -------x
Start test auto (2)!
Result auto (2) t=2937, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=172, r=39999998 Fatality!!!
Т.е. разница больше чем в 2 раза…
Если оставить в цикле только присвоение, то компилятору и auto_ptr становится стыдно! Да, в тесте (2) можно просто объявить локальную переменную без всяких new/delete и auto_ptr, но наша цель сравнить именно их. Будет интересно увидеть результат компиляторов «конкурентов» и unique_ptr :)
А тем кто минусует мой прошлый коммент еще раз процетирую, что там написано:
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.
new/delete — не враги, а друзья, которых надо использовать с умом и только если в этом есть реальная выгода. Тот, кто сомневается в своей способности их безопасно использовать и не видит пользы, может от них полностью отказаться. А тот кто не сомневается — настоящий Чак Норис :)
И да прибудет с нами сила…
auto_ptr
уже deprecated. Зачем вы его используете для тестов?
Тут, многие пишут, что это все не правильный компилятор и он несет не правильный мед, мол «правильный» всё оптимизирует и всё будет ровно, а самое интересно, тот компилятор который им нравится — и есть самый «правильный». Какая наивность…Наивность — это у вас. У нас — годы практики. Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код. Потому что на это у них «денех нет». Разные всякие рюшечки — продать можно легко. А просто генерация более качественного кода — это не «buzzword-compliant approach». Тем более что обычно удаётся за год «отвоевать» на реальных программах 5-7%. Но 5-7% один год, второй, третий… и вдруг, лет через 10 выясняется что ваш компилятор уже неконкурентоспособен — а за эти 10 компиляторы кокурентов научились, в частности, «сворачивать» C++-обёртки и… реальные программы — уже на это закладываются!
Что делать? Ну с Embarcadero всё просто: если это — не расписка в собственном бессили, тогда что это? Обратите, кстати, внимание, на то, что BCC64 основан на clang 3.1. 3.1, Карл! Компилятор, которому недавно 4 года стукнуло! А вы не… в общем не хочу материться…
Microsoft же несколько лет назад (когда clang, наконец-то, допилили) — тоже «вдруг» прозрел и ринулся в догонку. Но пока — он ещё очень и очень отстаёт от современных качественных компиляторов.
Приличные компиляторы для x86 — это GCC и Intel сегодня. Clang хорош, но пока всё-таки отстаёт.
Компилятор легко оптимизирует такие тривиальные примеры, но в реальных программах, где для создания/удаления объектов есть еще и условия и обращения к члена класса, он совсем не Чак Норис, и может пропустить кучу апперкотов и пару хуков от программиста.А сколько «аперкотов и хуков» пропустит программист из-за того, что ему придётся писать более сложный код? И не захочет ли он использовать алгоритмы попроще для компенсации?
А вашу программку я запустил, да. Вначале пришлось заменить GetTickCount на gettimeofday и «вдарить компилятору по почкам» использованием
__attribute__((noinline))
(а то он весь ваш «сурпрайз» умудрился выкинить из программу к чёртовой матери)Результат:
Обратите внимание на ваш «suprise», кстати — но это так, лирика: современные процессоры очень плохо относятся к слишком коротким функциям — они им пайплайн сбивают и адрес возврата не успевает доползти до предсказателя ветвлений. На более старых процессорах может быть примерно аналогичный по размерам проигрыш в этом месте (а когда вы принудительно «вставляете компилятору палки в колёса и не даёте ему заняться оптимизациями, то да, можно и ~25-30% проигрыша получить).Start test auto! Result auto t=8178878, r=399999998 Start test new/delete! Result new/delete t=6458617, r=399999998 -------- Surprize ------- Start test auto (2)! Result auto (2) t=470250, r=399999998 Result new/delete (2) t=488211, r=399999998 Fatality!!!!!!
Главная проблема: с какого перепугу вы заменили „указатель“ на „указатель на указатель“? Конечно у вас замедление будет (если функция не слишком мала)!
Господа, попробуйте запустить вот этот код, и проверить «крутость» своих компиляторов ;)«Крутизна» компилятора никак не может заменить мозги. Но если не загонять компилятор «в угол» и использовать
unique_ptr
, то результат будет уже таким:Про отсутствие „сурпрайза“ мы уже говорили, а скорость двух версий — совпадает (в пределах погрешности измерений). Да и с чего им отличаться? В обоих случаях в цикле — 9 инструкций (правда разных):Start test auto! Result auto t=5899570, r=399999998 Start test new/delete! Result new/delete t=5768141, r=399999998 -------- Surprize ------- Start test auto (2)! Result auto (2) t=0, r=399999998 Result new/delete (2) t=0, r=399999998 Fatality!!!!!!
... Код для unique_ptr ... .L19: movl $8, %edi call operator new(unsigned long) movq %rax, %rdi movl %ebx, (%rdi) movl %ebx, 4(%rdi) movl $8, %esi addl $1, %ebx call operator delete(void*, unsigned long) cmpl $200000000, %ebx jne .L19 ... ... Код для new/delete ... .L20: movl $8, %edi call operator new(unsigned long) movl $8, %esi movl %ebx, (%rax) movl %ebx, 4(%rax) movq %rax, %rdi addl $1, %ebx call operator delete(void*, unsigned long) cmpl $200000000, %ebx jne .L20
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.Я тоже когда-то так думал. Но факты — упрямая вещь. В 2016м году — это дааааалеко не „очевидно“. Современные компиляторы очень, очень хорошо умеют управляться с обёртками. Если ваш код „с обёртками“ и код „без обёрток“ рализуют один и тот же алгоритм — то результат будет, как правило, идентичен. Если вы введёте лишние операции — ну так кто ж вам судья? В коде с
unique_ptr
передавать сам unique_ptr
куда-то не принято: функции, которым не нужно „хранить“ указатель должны получать простой, не „умный“ указатель, а если вы „передаёте“ им владение, то нужно передавать его (используя rvalue-ссылки).new/delete — не враги, а друзья, которых надо использовать с умом и только если в этом есть реальная выгода.Кто сказал, что это „враги“? Вовсе нет. Но в современном мире их место — там же, где и место для ассемблерных вставок: иногда, очень редко и с помощью new/delete и с помощью ассемблерных вставок можно существенно ускорить код. Но… редко, очень редко.
Лет 10 назад (когда MSVC 6 был ещё вполне „в ходу“) я и STL'ем старался не пользоваться — больно было смотреть на то, что компилятор в результате порождает. Но те времена прошли…
NULL как аргумент memcpy/memmove — это UB, поэтому если указатель засветился в вызове memcpy/memmove, мы выкинем все проверки с ним и весь код обработки NULL, а если вы делали memcpy(NULL, NULL, 0) — мы сломали весь ваш код, но вы сами виноватыНу всё-таки выкидываются не «все проверки», а «все проверки после вызова memmove». Также выкидываются проверки
this
и т.д. и т.п. А чего вы хотите? C/C++ — это не Java и не C#…Кто вообще сказал, что «будет легко»? Я не зря чуть выше писал «С++ — сложный, хитрый, опасный инструмент» — это один из довольно неприятных моментов в нём, да. Программа в которой такое встрчается была сломана с самого начала, так как никто не гарантирует того, что у вас
memcpy
не читает память до проверки длины. Такие платформы реально существуют, код там примерно такой:void *memcpy(void *dest, const void *src, size_t n) {
size_t pre_count = n & 7U;
n &= ~7U;
switch (pre_count) {
do {
case 0: *dest++ = *src++;
case 1: *dest++ = *src++;
case 2: *dest++ = *src++;
case 3: *dest++ = *src++;
case 4: *dest++ = *src++;
case 5: *dest++ = *src++;
case 6: *dest++ = *src++;
case 7: *dest++ = *src++;
} while (n-=8);
}
}
Про то, что UB — это инструкция для програмиста, а не для компилятора многие присали, в том числе и я, если ваша программа вызывает UB — приготовьтесь к подобным сюрпризам, да. Логика здесь та же самая, которая проверку a + 1 > b + 1
превращает в a > b
— но почему-то подобная оптимизация у вас нареканий не вызывает… или вызывает?Но вообще, если вас этот так волнует (например если вы работает в окружении где NULL — валидный адрес… так встройка иногда сконфигурирована) или где memmove гарантированно не ломается (хотя кто и когда это обещал — сегодня не ломается, завтра сломается), то вы всегда можете использовать опцию -fno-delete-null-pointer-checks.
Более того, указатель на конец массива — это валидный указатель, поэтому memcpy вообще не имеет права разыменовывать переданные ей указатели при n=0.
Код типа
class MyContainer {
// если контейнер пуст, то указатель на данные держим нулевым
size_t Length = 0;
int* Data = nullptr;
...
void Append(const MyContainer& other) {
Realloc(Length + other.Length);
memcpy(Data + Length, other.Data, other.Length * sizeof(int));
Length += other.Length;
}
...
}
довольно типичен. Он работает на всех платформах, он не был сломан до того, как в gcc закоммитили изменение, его не может сломать корректная реализация memcpy, он не сломан при использовании компилятора, думающего о программистах, типа VC2015.UB — это не какие-то законы природы, данные нам свыше. UB означает всего лишь «конструкции, которые стандарт пометил словами undefined behaviour». Часть из них действительно отражают то, как устроен компьютерный мир — в неинициализированной переменной может оказаться всё, что угодно, а если она размещена в регистре, то и Itanium-ный Not-a-Thing с исключением при чтении; аналогично при выходе за границы массива. Часть из них отражают законы физики Марса, и джинна выпустили из бутылки, когда фразу «это UB», после которой забыли написать «потому что на Марсе вот так» (signed overflow на системах со странными представлениями отрицательных чисел), начали интерпретировать «поэтому на Земле мы вам делать так тоже запретим» («ну если ваш код вдруг попадёт на Марс, он же уже сломан!»). А часть — просто неудачные формулировки, и интерпретировать фразу со смыслом «memcpy не обязана делать явных проверок своих аргументов на NULL» как «memcpy нельзя передавать NULL даже при копировании нуля байт» — расписка в бессилии сделать что-то приличными средствами.
С99 7.1.4 Use of library functions
If an argument to a function has an invalid value (such as a value outside the domain of the function, or a pointer outside the address space of the program, or a null pointer, or a pointer to non-modifiable storage when the corresponding parameter is not const-qualified) or a type (after promotion) not expected by a function with variable number of arguments, the behavior is undefined.
Я не знаю, как это можно по-другому интерпретировать.
Но чтобы уж совсем никаких разночтений не осталось в C11 описания фукнкция из
<string.h>
было дополнено: Where an argument declared as size_t n
specifies the length of the array for a function, n
can have the value zero on a call to that function. Unless explicitly stated otherwise in the description of a particular function in this subclause, pointer arguments on such a call shall still have valid values, as described in 7.1.4..Изивините — но разработчики GCC сделали то, что стандарт им явным образом разрешил — и не более того.
P.S. И, кстати, стало действительно видно, что моя реализация — неправильна. Посыпаю голову пеплом. Но то, что моя реализация некорректна не делает вашу программу правильной, увы.
Код уже был сломан. Ничего дополнительно компиляторы не ломали.
Наивность — это у вас. У нас — годы практики. Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код.
x-----x
— Эхх… Зачем же на личности-то переходить? ;) Если у Вас годы практики, то у вашего покорного слуги — десятки. И лет 20 назад, когда я студентом в колледже после лаб по ассемблеру, под досом запускал этот волшебный watcom c++ мы про STL даже и не слышали c интернетом туговато было. Но за-то у нас считалась модной фишка — создать текстовый файл, забить там по памяти строчку символов и после переименовав его в xxx.com выполнить «теплую» перезагрузку дос :)
Мое " Какая наивность…", относится к попытке объявлять некоторыми представителями человечества свое субъективное мнение — общепринятым. Неужели кто-то на полном серьезе протестировал все версии всех компиляторов и сравнил их все между собой на всех возможных программах? У всех компиляторов есть свои плюсы и минусы, но безапелляционно утверждать, что у кого-то отвратительный код, а у кого-то хороший… слишком большая вольность.
Возьмем наш пример с сюрпризом.
Для объективности сравнения, я все-таки скачал последний MinGW и скомпилил в нем код, вообще без каких либо изменений. Извиняюсь, прошлый раз не выложил инклуды (это про GetTickCount)
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
#include <conio.h>
#include Вот результат для auto_ptr vs new/delete с несколькими вариантами оптимизации gcc
g++ test1.cpp
Start test auto!
Result auto t=6266, r=39999998
Start test new/delete!
Result new/delete t=4687, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=938, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=359, r=39999998 Fatality!!!
g++ test1.cpp -O1
Start test auto!
Result auto t=5000, r=39999998
Start test new/delete!
Result new/delete t=4531, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=234, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=157, r=39999998 Fatality!!!
g++ test1.cpp -O2
Start test auto!
Result auto t=4563, r=39999998
Start test new/delete!
Result new/delete t=3968, r=39999998
— Surprize -------x
Start test auto (2)!
Result auto (2) t=0, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=0, r=39999998 Fatality!!!
Напомню результат с сюрпризом C++Builder XE3 (оптимизация была включена на fasters код):
Start test auto!
Result auto t=5203, r=39999998
Start test new/delete!
Result new/delete t=2282, r=39999998
x-------- Surprize -------x
Start test auto (2)!
Result auto (2) t=2937, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=172, r=39999998 Fatality!!!
Это корректное сравнение компиляторов, без ударов по почкам :)
Причем, на одном и том же компе, поэтому можно сравнить и реальные скорости итоговых бинарников.
x-------x
Выводы:
x-------x
auto_ptr Тест с созданием и удалением в цикле:
gсс 01=5000, O2=4563
cb = 5203
Если сравнивать по O1, то примерно одинаково, по O2 — gcc на 12% быстрее
x-------x
new/delete Тест с созданием и удалением в цикле:
gсс 01=4531, O2=3968
cb = 2282
Если сравнивать по O1, то bc в 2раза быстрее, по O2 — bc на 42% быстрее
Ну КАК тут можно утверждать, что в bc — код отвратительный???
И самое главное: и в bc и в gcc код new/delete быстрее кода auto_ptr
Смотрим, что с сюрпризом:
x-------x
auto_ptr Тест только с присваиванием в цикле:
gсс 01=234, O2=0
cb = 2937
Если сравнивать по O1 — то gcc в 12.5 раз быстрее, по O2 — gcc удалил(оптимизировал) весь код
x-------x
new/delete Тест только с присваиванием в цикле:
gсс 01=157, O2=0
cb = 172
Если сравнивать по O1 — то примерно одинаково, по O2 — gcc удалил(оптимизировал) весь код
Вот тут можно сказать — в bc присваивание auto_ptr в цикле выполняется чертовски долго.
Но, опять же, самое главное: и в bc и в gcc код new/delete быстрее кода auto_ptr!!! Хотя в gcc vs gcc сюрприз не такой крутой (67%) как в bc vs bc (17раз)
Общий вывод: new/delete и в bc и в gcc работают быстре чем auto_ptr.
x-------x
Идем дальше, и пробуем поменять auto_ptr на unique_ptr:
x-------x
g++ test2.cpp -std=c++14
Start test unique_ptr!
Result unique_ptr t=9875, r=39999998
Start test new/delete!
Result new/delete t=4782, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=3000, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=375, r=39999998 Fatality!!!
g++ test2.cpp -std=c++14 -O1
Start test unique_ptr!
Result unique_ptr t=4813, r=39999998
Start test new/delete!
Result new/delete t=4687, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=250, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=187, r=39999998 Fatality!!!
g++ test2.cpp -std=c++14 -O2
Start test unique_ptr!
Result unique_ptr t=4219, r=39999998
Start test new/delete!
Result new/delete t=4141, r=39999998
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=0, r=39999998
Start test new/delete (2)!
Result new/delete (2) t=0, r=39999998 Fatality!!!
x-------x
Выводы:
x-------x
Тест повторил несколько раз unique_ptr vs new/dellete = хоть и не намного, но new/dellete стабильно быстрее unique_ptr (в O1 разница более заметна чем в O2)
Ну и последнее, попробуем все-таки сделать компилятору gcc подсечку даже для режима O2 и изменим код так, чтоб он не смог додуматься до оптимизации цикла с сюрпризом в 0тиков и посмотрим, что в реальности кроется за этими нулями:
struct MyStruct
{
volatile int x;
volatile int y;
};
int k = 2000000000; //+00 иначе слишком шустро
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7672, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7641, r=-294967298
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7703, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7656, r=-294967298
— Surprize -------x
Start test unique_ptr (2)!
Result unique_ptr (2) t=7641, r=-294967298
Start test new/delete (2)!
Result new/delete (2) t=7656, r=-294967298
Итого: в O2 сюрприз (т.е. только присваивание в цикле) для new/delete и unique_ptr работает одинаково. Для создания/удаления в цикле new/delete работает немного быстрее, но разница незначительна. А вот разница new/delete vs auto_ptr заметна и в gcc и в bc auto_ptr — медленнее.
Очевидно ли, что код с оберткой медленнее, чем без нее?
Можно ли утверждать, что компилятор создаст для обертки код по скорости не уступающий ее ручному развороту?
Пожалуй, ответить можно так:
— Если без оптимизации, да очевидно, код с оберткой будет медленнее.
— Если с оптимизацией, то всё зависит от компилятора и самой программы, насколько хитро организован код и сможет ли его «понять» компилятор, лучший способ узнать — это самостоятельно проверить, что-то заранее утверждать тут действительно не стоит… Доверие к компилятору конечно растет по мере успешности таких проверок, но не стоит свое доверие принимать за «общепринятый» стандарт/качество, слишком сложные критерии для объективной оценки.
Вы меня прямо заинтриговали, я немного поигрался с вашими тестами. Во-первых, оставил только вариант с сюрпризом. Во-вторых, сделал запуски функции идентичными для голого указателя и для unique_ptr
. Потому как вы передавали unique_ptr
по ссылке, что в данном сценарии не подходит: функция ведь не участвует в определении времени жизни объекта, следовательно, и передавать объект нужно именно по голому указателю, в соответствии с C++ Core Guidelines. Сразу же оговорюсь, что если передавать объект именно как ссылку на умный указатель, результаты не меняются, я проверил.
В-третьих, заменил auto_ptr
на unique_ptr
. Ну потому что ну в конце-то концов. Наконец, я добавил ещё один вариант с более сложной функцией, далее станет ясно, почему. Протестировал это дело на gcc, clang и VS. Результаты:
Simple test with unique_ptr done in 0
Simple test with new/delete done in 0
Complex test with unique_ptr done in 851
Complex test with new/delete done in 850
Simple test with unique_ptr done in 0
Simple test with new/delete done in 0
Complex test with unique_ptr done in 1057
Complex test with new/delete done in 1046
VS 2015 (количество итераций уменьшено в 4 раза):
Simple test with unique_ptr done in 4021
Simple test with new/delete done in 575
Complex test with unique_ptr done in 4347
Complex test with new/delete done in 1233
Мы видим следующее:
- gcc и clang соптимизировали простую версию "с сюрпризом" в ноль. Просто догадались, что результат зависит только от последней итерации. Весьма впечатляюще.
- gcc и clang работают для
unique_ptr
иnew/delete
на более сложной функции одинаково. - Оптимизатор VS показал себя не очень. Чтобы уложиться в лимит времени онлайн-компиляторов пришлось даже уменьшить ему число итераций в 4 раза.
Что характерно, у меня на машине локально VS показала себя более достойно (возможно, на сервере стоит не та оптимизация, не знаю, не нашёл, где посмотреть ключи). В частности, время выполнения более сложной функции для unique_ptr
и new/delete
отличается не сильно. Но всё же new/delete
выигрывает.
Теперь, мне вообще кажется, что ваш пример не слишком показателен. В цикле из всего функционала умного указателя вы по сути использовали только operator->()
. Я в своём примере использовал только get()
(но, повторюсь, ваш я тоже проверил). Так вот, и то и другое — однострочные функции, объявленные в заголовочном файле. Такие вещи компиляторы научились инлайнить давным-давно (Visual C++, видимо, это исключение :) ). Я не уверен, что вы сможете придумать такой пример, на котором gcc или clang смогут сделать какую-то оптимизацию с голым указателем T*
, но при этом не смогут применить её над методами умного указателя unique_ptr<T>::get()
или unique_ptr<T>::operator->()
.
По результатам для компиляторов bc и vc это оказалось именно так.
Для gcc и clang — практически разницы нет, хотя в gcc все же есть небольшой перевес в сторону более быстрого new/delete.
Спасибо за ссылку на интересный инструмент для тестов, теперь можно подвести окончательные итоги:
Было два варианта тестирования:
A) Тест создания/удаления объектов в цикле в раздельных ф-циях. Т.е. в одной ф-ции объект создается, в другой используется, в третей удаляется и все это делается через один общий обычный или умный указатель.
Почему для void test2_new(MyStruct** in_p, int i) — используется указатель на указатель? — чтобы можно было выделить внутри ф-ции память для нового объекта и ассоциировать его с общим указателем. Точно также через указатель на указатель в test2_delete освобождается память под объект на который указывает общий указатель.
Почему для void test2_auto(std::unique_ptr& in_ap, int i) — используется ссылка, дабы проверить в цикле именно скорость работы reset() и ->(), а не ф-ции get(), использование которой для передачи указателя на объект в ф-цию, делает внутреннюю работу ф-ции абсолютно не связанную с «умными» указателями.
Изменения в онлайн тесте:
Оставил int — дабы компилятор использовал туже разрядность что и на сервере
Добавил volatile — дабы компилятор имел в виду, что значения могут меняться без его ведома через аппаратные прерывания или др.потоки.
Убрал из блока измерения времени ф-цию вывода cout (оставленную вами там видимо по ошибке)
http://rextester.com/NSQ67595
VC k = 20000000
Test2 unique_ptr done in 6629
Test2 with new/delete done in 2061
http://rextester.com/GGNQL2697
CLANG k=VC*4
Test2 unique_ptr done in 2291
Test2 with new/delete done in 2290
http://rextester.com/QEABX66753
GCC k=VC*4
Test2 unique_ptr done in 2478
Test2 with new/delete done in 2340
Для теста A:
VC — плохо оптимизирует unique_ptr
CLANG, GCC — хорошо (но CLANG быстрее GCC)
Абсолютное сравнение по скорости — лучше всех CLANG, потом GCC, ну и VC (в 4раза уменьшено кол-во итераций)
Б) Тест в котором указатель обычный/умный, только используется в цикле.
1. Вы повторили, но с лишними изменениями. Как я уже писал выше — использование get() полностью лишает смысла действия производимые с умным указателем внутри ф-ции, Поэтому цитирую «Теперь, мне вообще кажется, что ваш пример не слишком показателен.» становится не показательным именно из-за вашего изменения, т.к. вы вообще убрали умный указатель из ф-ции, оставив лишь для тестирования только get()
2. Ф-ция test_complex() — так же лишена смысла, т.к. мы мерим не скорость выполнения операции %, а именно доступ к членам объекта через умный/обычный указатель. Для того чтобы компилятор не оптимизировал код и вы не получали 0 и было предложено объявить члены MyStruct через volatile (тогда компилятор будет считать, что члены MyStruct могут быть изменены без его ведома и не сможет их оптимизировать).
Результаты локальных тестов я выкладывал в сообщении выше, а вот результаты online:
http://rextester.com/LRD24665
VC k = 80000000
Test3 unique_ptr done in 14452
Test3 with new/delete done in 595
http://rextester.com/JWH56302
CLANG k = VC*4
Test3 unique_ptr done in 267
Test3 with new/delete done in 269
http://rextester.com/MICE52083
GCC k = VC*4
Test3 unique_ptr done in 319
Test3 with new/delete done in 314
Для теста Б:
VC — плохо оптимизирует unique_ptr
CLANG, GCC — хорошо
Абсолютное сравнение по скорости — лучше всех CLANG, потом GCC, ну и VC ( в 4раза уменьшено кол-во итераций)
Ну и вспомним локальное сравнение auto_ptr — new/delete для BC vc GCC
Тест А:
BC
Result auto t=5203, r=39999998
Result new/delete t=2282, r=39999998
GCC -O2
Result auto t=4563, r=39999998
Result new/delete t=3968, r=39999998
BC — плохо оптимизирует auto_ptr
GCC — средне
По абсолютной скорости — BC в 2раза быстре GCC (если использовать new/delete)
Тест Б:
BC
Result auto (2) t=2937, r=39999998
Result new/delete (2) t=172, r=39999998
GCC -O2
Result auto (2) t=63, r=39999998
Result new/delete (2) t=78, r=39999998
BC — плохо оптимизирует auto_ptr
GCC — хорошо
По абсолютной скорости — а вот тут уже GCC в 2раза быстре BC (если использовать new/delete)
x-----x
В общем и целом, конечно удивляет такой провал VC. Но вывод все-таки остается тем же, доверяй, но проверяй. В BC и VC явно оптимизация не настолько крута, и поэтому для ускорения работы замена оберток на new/delete вполне может быть оправдана. Для clang и gcc, это более спорный вопрос. По вашим измененным тестам видно, что get() оптимизируется так же хорошо как reset() и ->() в моих, поэтому наверно сразу с ходу такой пример, который вы предложили привести будет сложно. Да и нужно ли? Самый лучший пример — это реальная программа для реального компилятора используемого в релизах, в которой либо можно существенно увеличить быстродействие за счет new/delete либо нельзя т.к. компилятор это сделал за вас.
В общем и целом, конечно удивляет такой провал VC.
Да, я тоже был немало удивлён и решил повторить ваши тесты у себя локально. Получил совершенно другие результаты. Тест А: и unique_ptr
и new/delete
дали одинаковое время работы. Тест Б: unique_ptr
проиграл new/delete
в полтора раза. Похоже, я предложил не самый объективный инструмент для замера скорости, прошу прощения. Я уже воспользовался формой обратной связи на сайте и сообщил о проблеме.
Но основной ваш вывод, остаётся в силе. Тест Б всё равно работает в полтора раза медленнее для умных указателей. Я зарепортил баг на Microsoft.Connect (но его пока не видно в системе; возможно, он на премодерации, а может у них просто всё лагает).
Да, если считать верхом совершенства, прости господи, watcom c++, то это может оказаться удивительным, так как в прошлом веке Borland C++ действительно ещё был «на коне», да и MSVC был неплох, да. Но с тех пор — много воды утекло. Как я уже писал: в прошлом веке я и STL'ем старался не пользоваться — больно было смотреть на то, что компилятор в результате порождает. Но те времена прошли…
Я с большим уважением отношусь к «старому» Borland'у. Borland C++ 3.1 — это вообще чудо было. Напомню что хотя Borland С++ был «базовым компилятором» для разработчиков STL (потому что остальные компиляторы тогда просто не могли все эти шаблоны разобрать вообще) — но ни о какой скорости тогда речь не шла, была задача «тупо это всё собрать хоть как-то и не упасть».
Но к 1996м году Microsoft тупо «задавил» Borland (и все остальные независимые компании: Watcom/Sybase, Zortech/Symantec, JPI/TopSpeed и прочие всякие Comeau) — мало того, что денег на разработку стало не хватать, так ещё и, в случае с Borland'ом (последний «живой» конкурент) существенную часть команды разработчиков в 1996м году сманили — но они занялись не C++, а совсем другим: считалось, что C++ «скоро уйдёт в историю» и все-все-все будут использовать managed код, который «догонит и перегонит» старичка.
Чего, разумеется, не случилось. Развитие C++ продолжилось — но так как хорошие деньги именно за скорость платили только в области HPC, то приличные компиляторы только там и остались: Intel C++, IBM XLC++, Sun/ Oracle C++ (хотя этот, в последнее время, сдал), потом GCC подтянули (примерно к середине нулевых). Напомню, что Windows на HPC никто не использует: в пресловутом Top500 ни одной Windows-системы сейчас нет, да и «в лучшие времена» их там было в районе 1% — да и, собственно, если вам уж сильно была нужна скорость — всегда можно было использовать Intel C++ (он встраивается в Visual Studio).
А дальше… дальше Apple крупно разосрался со Столлманом (после выхода в 2005м году GPLv3) и вбухал кучу денег в свой собственный компилятор, который, однако Apple не стал «держать при себе», а наоборот, всячески пропогандировал разработку «всем миром» (так что сейчас Apple уже не является даже основным контрибутором). Microsoft это слабо волновало до тех пор пока «всем миром» clang не допилили до состояния почти совместимости с MSVC (сам Apple этим не занимался, но видимо кому-то это сильно нужно было). Chromium, скажем, собрать только с помощью clang'а нельзя — но только за счёт того, что кой-какие расширения не поддерживаются, 99% кода clang собирает и это используется чтобы использовать ASAN.
В какой-то момент Microsoft понял, что стратегия «заставим всех мышей жрать кактус за неимением другой альтернативы» и «сменил пластинку». Балмера турнули, разработчики MSVC снова получили деньги под развитие C++ (думаю не стоит объяснять что замена одного человека круто развернуть компанию размера Microsoft'а не может — увольнение Баллмера было следствием, а не причиной изменения курса). Примерно с MSVC 2013 о компиляторе MSVC уже можно говорить как о «современном компиляторе C++» (то, что было до этого тянуло в лучшем случае на «муляж компилятора C++»), начиная с MSVC 2015 — его уже можно даже с другими сравнивать (хотя он и проигрывает частенько, но, в общем — движение в нужную сторону очень и очень заметно).
Всё вышеописанное — банальности. Как можно считать себя знатоком C++ и всего этого не знать — я просто не понимаю.
Я ведь когда написал «хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код» — то я ведь не издевался. Как мне казалось я просто говорил вещь, которая всем очевидна, известна и которую все учитывают при разработке — а меня вдруг обвинили в «переходе на личности».
P.S. Что касается сравнени «абсолютных скоростей new/delete» — то тут я просто не хочу затевать дискуссию. Боюсь на мат перейти. Просто хочу напомнить что скорость работы, собственно,
new
/delete
от компилятора зависит мало (основное время проводится в malloc
'е), стандартные реализации malloc'а в HPC не используются (популярны вещи типа jemalloc/TCMalloc), да и MingW разработчиков GCC мало волнует вообще (ну не используюется Windows людьми, для которых скорость работы C++ кода превращается в живые деньги — все эти Facebook'и, Google'ы и Yandex'ы используют Linux или, иногда, FreeBSD). Зачем вообще его сюда приплели — мне не очень ясно: как я уже <a href="">говорил — если вам нужен приличный компилятор под Windows — то нужно использовать clang: он пока чуть-чуть отстаёт от GCC, но зато вы всегда можете всегда использовать его только для «тяжёлого» кода, оставив тот же runtime и использовав MSVC в тех местах, где clang пока не тянет.С++ это один из рабочих инструментов, который используется в работе над проектами, какой компилятор стоит у работодателя, такой и используется. Да я не эксперт в области истории развития компиляторов, и могу судить только о тех, с которыми реально сталкивался и имею опыт «боевого» использования в коммерческих продуктах (например первые BC и еще тогда MS Torbo C в свое время активно использовали для промышленных контроллеров с ОС ROM-DOS, VC6 — для мелких прикладных задач c GUI, очень плевался от него, когда перешли на BC6 это стало с точки зрения RAD разработки по сравнению с VC6 небо и земля, скорость реализации проектов ускорилась в разы, хотя по тестам производительности, которые тогда делали BC6 где-то проигрывал VC6 15-20% где-то выигрывал, но для прикладных задач это вообще ни о чем). С gcc когда-то попробовал один раз поиграться и забросил, так как никакой практической пользы в его использовании для себя не увидел…
И мое сугубо личное мнение, которое сложилось по опыту работы — у каждого компилятора, есть свои плюсы/минусы и нельзя так просто считать некий компилятор генерящим отвратительный код, а другой генерящим нормальный. Потому что код который он генерирует зависит от того, что написано в программе и насколько хорошо именно эту программу «понимает» компилятор.
Собственно это странное мнение, что профессионалом может быть только тот, кто имел опыт работы со всеми компиляторами меня и удивляет. Но а если вы про «специалиста по всем компиляторам c++», то да таковым я не являюсь, поэтому некоторые вещи меня в этом мире могут удивить :)
Про «абсолютных скоростей new/delete» скажу кратко, то, что new использует malloc знают все (а как реализовывать malloc это дело уже компилятора). Только вот для одной и той же программы использующей new/delete на одном компиляторе получается бинарник работающий быстрее чем на другом — и это просто факт (по крайней мере для Windows).
Собственно это странное мнение, что профессионалом может быть только тот, кто имел опыт работы со всеми компиляторами меня и удивляет.Причём тут профессионализм?
Давайтё я проведу «бытовую аналогию»: будет ли таксист отлично занющий где какие камеры висят и в каких местах можно «нарваться на ментов», но при этом не знающий какие есть автопроизводители в мире и чем отличаются выпускаемые ими машины считать «профессионалом»? Да, наверно. Но когда подобный «профессионал» начинает рассуждать о том как тюнить машину для раллийных гонок — то он выглядит несколько глупо.
И мое сугубо личное мнение, которое сложилось по опыту работы — у каждого компилятора, есть свои плюсы/минусы и нельзя так просто считать некий компилятор генерящим отвратительный код, а другой генерящим нормальный.Свои плюсы и минусы есть, несомненно, у всех компиляторов. Скажем у GCC кодогенерация под x86 — лучше, чем у clang'а, а вот его же версия для arm'а — уже далеко не так хорошо себя ведёт. А есть компиляторы, которых «пугают» даже
inline
функции (на которых реально можно получить выигрыш заменой inline
-функции на макрос) в достаточно несложных ситуациях (к ним относятся в усновном устаревшие поделки типа Borland/Inprise/Embarcadero, Open Watcom'а и прочих — но также и MSVC).Однако если про устаревшие компиляторы забыть, то споры об «обёртках» сегодня имеют столько же смысла, сколько споры о том, что лучше использовать — указатель или индекс (да, представьте себе, лет 20 назад подобные споры реально имели место было — потому что, удивительно, но факт: тогдашним компиляторам это не было не всё равно): все качественные современные компиляторы уже давно этим «переболели» и однострочные обёртки типа
unqiue_ptr
— код не замедляют. Отсюда — и обсуждаемая статья.А ваши рассуждения об обёртках потому и вызвали столько негатива, что выглядят они… — как у Жванецкого: «давайте спорить о вкусе устриц и кокосовых орехов с теми, кто их ел».
Если вас волнует скорость работы вашего кода — то вы, уж наверное, знаете о том какие компиляторы чего умеют и давно выкинули в помойку компиляторы, которые неспособны генерировать приличный код, если не волнует… то, собственно, кого волнует что вы думаете о том, как будет устроен инструмент, который, как оказывается, вы даже не выбирали?
хотя по тестам производительности, которые тогда делали BC6 где-то проигрывал VC6 15-20% где-то выигрывал, но для прикладных задач это вообще ни о чемНу если для вас 15-20% — это «ни о чём», то, может быть, предоставите право судить о скорости тем, для кого 15-20% — это важно? И кто, соотвественно, следит за этим и знает откуда в современном мире тормоза вылезают часто, а откуда — редко?
С++ это один из рабочих инструментов, который используется в работе над проектами, какой компилятор стоит у работодателя, такой и используется.Такое тоже, бывает, да. Но тут, как бы, «если слаще морковки ничего не пробовал,… то и тыква — фрукт». У нас в проекте есть куча костылей для MSVC, от которого мы не можем, по некоторым причинам, пока отказаться. И выкинуть их не удаётся годами (говорят что начиная с MSVC 15 «всё будет по другому»… посмотрим — на MSVC 15 мы только-только перешли). А вот костылей для GCC и Clang'а — очень мало — и большинство из них существует только год-два пока переход на новую версию не сделает их ненужными. В частности для разработчиков GCC/Clang'а плохо «свернувшаяся» обёртка — это ошибка, которую нужно править, для разработчиков MSVC — ну я не знаю, сейчас, может ситация изменилась, но лет пять назад ничего, кроме «используйте
__forceinline
» поддержка сказать не могла (хотя и это тольком не помогало, полностью «свернуть» обёртки MSVC всё равно не мог).Только вот для одной и той же программы использующей new/delete на одном компиляторе получается бинарник работающий быстрее чем на другом — и это просто факт (по крайней мере для Windows).А вот тут — у нас уже рассуждения «суперпрофитаксиста», который баранку вертеть умеет, а том, что под капотом у машины — не знает и знать не хочет. А вы не поверите — там бывает не только двигатель, но и трансмиссия и даже, о ужас, коробка передач. И бывает так, что неплохая, в целом, машина проигрывает в ралли из-за неудачного механизма оной коробки передач.
А как реализовывать malloc это дело уже компилятораВ том-то и дело, что нет. Windows — это единственная современная операционка, где
malloc
не поставляется «из коробки». На всех остальных операционках runtime (включающий, конечно, и malloc
) — поставляется с системой. Потому ни GCC, ни Clang malloc
а просто не имеют. Microsoft, кстати, тоже до этого наконец-то дотопал, но пока MingW этот runtime не использует (потому что, в частности, «из коробки» он доступен только в Windows 10, а пользователи ещё не все на Windows 10 перешли).Я понятия не имеют что взяли разработчики MingW, чтобы создать нечто, что можно запускать на Windows — скорее всего какой-нибудь древний dmalloc. Неудивительно, что он отстаёт от
malloc
а C++Builder'а — всё-таки C++Builder разрабатывается не кучкой энтузиастов для запуска пары утилит во время разработки, а довольно большой компанией, которая когда-то имела приличную команду разработчиков…x-----x
Ну если для вас 15-20% — это «ни о чём», то, может быть, предоставите право судить о скорости тем, для кого 15-20% — это важно?
x-----x
— Предоставляю! :) Если вам очень важно, что окно с сообщением «Вы нажали не на ту кнопку!», появится на 1милисекунду позже, чем на другом компиляторе — то это ваш личный фетиш. Вы читайте внимательнее к чему относятся эти 15-20%: «для мелких прикладных задач c GUI». Для задач критичных по времени, (а на МК все такие), идет битва за каждую микросекунду и поверьте, тут уж есть, что потюнинговать, чтобы сократить время рабочего цикла, это вот как раз и есть настоящие «ралли». Причем понятиям «ошибка», «зависание»,«утечка» здесь придается не «философский» смысл — типа перезагрузил программу / ОС, и все можно жить с этим дальше, а довольно «фатальный», не закрылся вовремя клапан / не выключился движек… и все… что-то громко бабахнуло. А избежать этого помогают, только беспощадные тесты на производительность и надежность.
x-----x
«если слаще морковки ничего не пробовал,… то и тыква — фрукт».
x-----x
— Те МК, которыми мы занимаемся уже идут со встроенной ОС, поэтому выбирать тут особо не приходится. А инструменты и библиотеки под них, которые разрабатывались, отлаживались и проверялись годами никто менять на «нечто из огромного выбора», конечно не собирается.
Если инструмент проверенный, работает быстро и надежно — вы правда считаете, что его надо поменять?
x-----x
А вот тут — у нас уже рассуждения «суперпрофитаксиста», который баранку вертеть умеет, а том, что под капотом у машины — не знает и знать не хочет.
В том-то и дело, что нет. Windows — это единственная современная операционка, где malloc не поставляется «из коробки».
x-----x
— У вас, какой-то прям «дар» невнимательности — я специально написал (по крайней мере для Windows), т.е. именно «кривая» реализация malloc/free и является проблемой new/delete GCC в Win, и это был ответ на ваше более раннее утверждение: «Просто хочу напомнить что скорость работы, собственно, new/delete от компилятора зависит мало». Поверьте, то что происходит под капотом винды, меня весьма интересует.
Думаю на этом можно завершить данную дискуссию. Подведем итоги:
Мое утверждение:
То, что идеальный код, использующий new/delete будет работать быстреe авто-оберток, думаю очевидно.
— Неверно, ибо есть компиляторы, для которых это действительно очевидно, и есть компиляторы, для которых это не настолько очевидно, т.к. используется сильная оптимизация.
Утверждение GamePad64:
Тот же unique_ptr соберётся в точно такой же машинный код, что и new/delete.
— Неверно, т.к есть компиляторы в которых это по тестам, действительно так (по крайней мере по тем конкретным тестам, которые мы тут проводили). И есть компиляторы в которых это совершенно не так (доказано теми же тестами)
Утверждение khim
Хорошо известно, что MSVC и Borland/Inprise/Embarcadero — генерируют отвратительный код.
— Не выдерживает критики, т.к. здесь приведены тесты в которых код скомпилированный GCC выполняется в 2раза дольше кода BC (на Windows).
Остальные утверждения в частности:
То, что идеальный код, использующий new/delete написать сложно, тоже очевидно.
Поэтому в 99% случаев авто-обертки использовать гораздо разумнее.
Вроде бы никто не оспаривал.
Главный вывод — на данный момент, любое утверждение о компиляторах с++, должно содержать в себе перечисление тех компиляторов к которым оно относится. И если это утверждение пытается как-то сравнивать компиляторы между собой, необходимо приводить критерии и тесты на которых оно основано. Так как любой частный случай/тест не вписывающийся в общую картину — опровергает всё обобщенное утверждение.
Засим и откланиваюсь.
Это либо профнепригодность, либо троллинг — в любом случае обсуждать нечего.
Код, который был приведен выше — был абсолютно тривиальный и без включенной оптимизацииО, а я был прав. В этом примере поди снова не включили?
Смотрим на WinRT API, где вообще на каждый чих и пук у вас появляется новый shared_ptr (в виде ^) и очевидно, что Microsoft, было очень соблазнительно приучить всех пользовать make_shared, дабы сократить количество аллокаций из кучи в практически любой WinRT программе вдвое. Игра стоит свеч.
Вне WinRT мирочка картина получается несколько иная.
#1. Реклама про thread-safe безопасность make_xxx функций не более, чем реклама. На практике очень редко приходится писать выражения, в которых несколько new могут реально породить проблему с исключениями.
#2. В типичной программе на C++ на несколько десятков unique_ptr или scoped_ptr едва приходится один shared_ptr. Соответственно, главного профита от записи make_xxx в виде одного выделения из кучи вы, в подавляющем большинстве случаев использования умных указателей, не получаете.
Если в вашей программе доля shared_ptr сильно больше озвученной — это серьезный повод задуматься, вы явно что-то делаете не так.
#3. В типичной программе подавляющее большинство указателей типа unique_ptr/scoped_ptr это поля класса, а не стековые объекты. Соответственно, даже крошечного бонуса от возможности использовать auto вы тоже не получите. Более того, в списке инициализации или просто в методе, сильно проще писать «new T», чем громоздкое «std::make_unique».
Короче, включайте здравый смысл и голову и не видитесь на хайп, который устраивают граждане из Microsoft.
зы. Хотя, если думать, что вся эта история в целом подтолкнет хотя бы некоторое количество ортодоксов и консерваторов (коих, к сожалению, все еще есть огромное количество) просто к использованию умных указателей, то это, несомненно, будет большая польза… Правда, мое мнение, что лучше весь этот сброд «C/C++ професионалов», которые типа знали С и выучили C++ через два новых для себя слова — class и virtual, согнать в зоопарки и показывать там на потеху публике.
Версия с поддержкой shared_ptr, если я ничего не путаю, появилась в GCC версии 4.0, а это 2005 год. WinRT — это что-то около 2011 года. Связь где?
#1. Реклама про thread-safe безопасность make_xxx функций не более, чем реклама. На практике очень редко приходится писать выражения, в которых несколько new могут реально породить проблему с исключениями.
foo(std::make_shared<T1>(), std::make_shared<T2>());
#2. В типичной программе на C++ на несколько десятков unique_ptr или scoped_ptr едва приходится один shared_ptr. Соответственно, главного профита от записи make_xxx в виде одного выделения из кучи вы, в подавляющем большинстве случаев использования умных указателей, не получаете.
Если в вашей программе доля shared_ptr сильно больше озвученной — это серьезный повод задуматься, вы явно что-то делаете не так.
Ну вот чтобы далеко не лезть — что делают не так авторы OSG из статьи выше (у них своя реализация shared_ptr)? И как вы предлагаете взамен строить сцену из unique_ptr?
#3. В типичной программе подавляющее большинство указателей типа unique_ptr/scoped_ptr это поля класса, а не стековые объекты.
unique_ptr да, а что насчет shared_ptr?
Более того, в списке инициализации или просто в методе, сильно проще писать «new T», чем громоздкое «std::make_unique».
А namespace никто не использует, да?
В «зы» вы вообще какие-то неадекватные причинно-следственные связи делаете. Умные указатели — это хорошо, но использовать их будут те, кто пришел из мира С, а они по умолчанию тупые и недостойные зваться профессионалами, потому что выучили только class и virtual, хоть и пользуются умными указателями? Что-то каша какая-то у вас.
shared_ptr — это просто удобный инструмент для работы с долгоживущими объектами с общим доступом. Нам не нужно следить за его удалением, аккуратной передачей и прочими бестолковыми вещами. В 99% это НИКАК не влияет на производительность и потребление памяти конечного продукта. Если это удобно и эффективно, то это нужно использовать.
WinRT — это что-то около 2011 года. Связь где?
#1. Не надо путать shared_ptr и make_shared. Мой комментарий был именно о последнем в контексте «радиоактивности» new.
#2. GCC тут вообще не при чем, изначально shared_ptr это зверь из boost.
foo(std::make_shared<T1>(), std::make_shared<T2>());
Если мы говорим о старом коде, до make_shared, то ваш пример должен выглядеть примерно так
foo( std::shared_ptr<Type1>( new Type1( /* arguments for Type1 */) ), std::shared_ptr<Type2>( new Type2( /* arguments for Type2 */) ) );
Не знаю, кто как, но люди, которые моют руки перед едой, так код писать точно не будут. А напишут его, например, так:
std::shared_ptr<Type1> arg0( new Type1( /* arguments for Type1 */) );
std::shared_ptr<Type2> arg1( new Type2( /* arguments for Type2 */) )
foo(arg0, arg1);
Ой! А проблема то с безопасностью исключений как-то сама собой ушла!
что делают не так авторы OSG из статьи выше
Без понятия. А что, именно библиотека OSG является классическим усреднением для некоего типичного C++ кода? С каких пор?
Кстати, предлагаемый костыль make_ref ничего хорошего из себя не представляет, так как лишается главной силы make_shared — возможности экономить на аллокации памяти из кучи.
а что насчет shared_ptr?
Насчет make_shared я сказал — это полезный трюк, но это автоматически не означает, что new это вселенское зло и надо лепить уродливые костыли типа make_unique или make_ref. Никакой связи.
А namespace никто не использует, да?
new T() vs make_unique<T>()
Что я еще должен использовать? Может сделать двухбуквенный using, чтобы эта конструкция выглядела не так уродливо??
Что-то каша какая-то у вас
Каша у тех, кто невнимательно читает чужие комментарии.
Мой комментарий был о том, что можно и нужно смело использовать new и не комплексовать по этому поводу.
А вот тех, кто думает, что пишет на C++, и при этом не используется умные указатели нужно гнать из профессии ссаными тряпками.
shared_ptr — это просто удобный инструмент для работы с долгоживущими объектами
Не надо мне продавать shared_ptr я знаю что это и зачем. Я говорю о том, что если вместо него можно использовать unique_ptr это сильно лучше.
std::shared_ptr<Type1> arg0( new Type1( /* arguments for Type1 */) );
std::shared_ptr<Type2> arg1( new Type2( /* arguments for Type2 */) )
foo(arg0, arg1);
И положили переменные arg0 и arg1, быть может, в ту область видимости, где их быть не должно.
Ничего, кстати, некрасивого тут нет, просто пользуйтесь отступами и выравниванием:
foo( std::shared_ptr<Type1>( new Type1( /* arguments for Type1 */) ),
std::shared_ptr<Type2>( new Type2( /* arguments for Type2 */) )
);
Без понятия. А что, именно библиотека OSG является классическим усреднением для некоего типичного C++ кода? С каких пор?
А местами не очень понимаю, против какой именно философии вы выступаете.
Если речь о том, что лучше использовать unique_ptr, то вот пример — графический движок + граф сцены. Сюда можно поставить и другой графические движок, там плюс-минус все похоже.
Насчет make_shared я сказал — это полезный трюк, но это автоматически не означает, что new это вселенское зло и надо лепить уродливые костыли типа make_unique или make_ref. Никакой связи.
Конечно, не означает. Но уродливые костыли (кому как, кстати) позволяют избавиться от ненужных аллокаций в клиентском коде.
Каша у тех, кто невнимательно читает чужие комментарии.
Мой комментарий был о том, что можно и нужно смело использовать new и не комплексовать по этому поводу.
Тут я опять же не понимаю, против какой вы философии. Если вы говорите про то, что нужно использовать new T() вместо make_shared(), то, наверное, большинство проектов от этого и правда не умрут (если одна лишняя аллокация им не страшна, что скорее всего).
Если вы говорите, что нужно оперировать везде указателями как тру программист, то я категорически не согласен. При текущем положении дел это не приносит большинству проектов каких-то существенным бонусов, кроме геморроя. Это не всегда так, но в подавляющем большинстве.
Не надо мне продавать shared_ptr я знаю что это и зачем. Я говорю о том, что если вместо него можно использовать unique_ptr это сильно лучше.
Да конечно лучше, кто же спорит-то?! Нужно использовать. Но это далеко не всегда возможно (опять же, графические движки).
Не знаю, кто как, но люди, которые моют руки перед едой, так код писать точно не будут. А напишут его, например, так:
Только лучше так:
foo(std::move(arg0), std::move(arg1));
Экономим на лишнем инкременте/декременте счетчика и отвязываем время жизни объектов от времени жизни arg0
и arg1
.Как вы правильно заметили в mailing lists, есть 3 основных варианта владения памятью в Qt:
1. Хранение в стеке (тривиально)
2. Отсутствие родителя (предполагается new и delete в пользовательском коде)
3. Есть родитель (только new)
Во втором случае мы сами управляем памятью, поэтому можно использовать std::unique_ptr:
auto mainWindow = std::make_unique();
По возможности надо размещать на стеке, но если не получается, то лучше так.
В третьем случае родитель управляет памятью, но концепцию «умных указателей» можно применить и тут:
template
using unowned_ptr = T*;
template
unowned_ptr MakeQObject(QObject& parent, Args&&… args);
template
unowned_ptr MakeQObject(QWidget& parent, Args&&… args);
То есть опять-таки, мы можем обращаться с этим unowned_ptr, как с unique_ptr, и QObject корректно удалится.
Конечно, это немного текучая абстракция, так как мы всё равно должны убедиться, что не удалим родителя раньше времени. Но хоть что-то.
Чем unowned_ptr лучше «сырого» указателя? Тем, что у него информация о владении закреплена в типе. Никому в голову не придёт его удалить. Можно сделать это ошибкой, если вместо using создать специальный класс, но проще не заморачиваться на этот счёт.
Bjarne уже несколько лет толкает идею unowned_ptr в C++. Прямо сейчас он, наряду с другими полезными мелочами, лежит в Guidelines Support Library.
На практике, есть ещё 4 случай, когда объект «подцепляют» к родителю уже после его создания, но при наличии необходимых helpers, 3 вариант будет удобнее и безопаснее.
QSharedPointer<MyObject> obj =
QSharedPointer<MyObject>(new MyObject, &QObject::deleteLater);
auto obj1 = MakeQObject<MyObject>();
auto obj2 = MakeQChild<MyObject>(parent);
Собственно, один из вопросов, поднятых в статье, — можно ли все выделения памяти в куче записывать в виде:
auto obj = make<MyObject>(arguments);
Где make — подходящая функция создания умного указателя. Ответ — да, можно, причём такой код будет безопасным и унифицированным.
Такие выделения памяти также отлично сочетаются с рекомендациями Herb Sutter.
Во втором случае мы сами управляем памятью, поэтому можно использовать std::unique_ptr:
Согласен, выглядит разумно.
Чем unowned_ptr лучше «сырого» указателя? Тем, что у него информация о владении закреплена в типе. Никому в голову не придёт его удалить.
Bjarne уже несколько лет толкает идею unowned_ptr в C++. Прямо сейчас он, наряду с другими полезными мелочами, лежит в Guidelines Support Library.
Тут вы немного перепутали. В C++ Core Guidelines и в GSL реализован обратный вариант — владеющий указатель owner<T>. Сырой указатель T* считается невладеющим. Об этом Саттер и Страуструп как раз и рассказывали в своих выступлениях (кажется, на GoingNative 2015, могу поискать, если нужно).
По поводу MakeQObject
. Там фундаментальная проблема в том, что в Qt, как я понимаю, есть мода разделять создание объектов и их прицепление к родителю. Т.к. исключений там нет, это считается безопасным. Насколько такая аргументация убедительно — сказать не могу, опыт работы с Qt у меня небольшой.
В любом случае, я всячески приветствую предложить ваши идеи в тот же самый список рассылки :)
Про проблему, можно рассмотреть 2 случая:
- Объект почти сразу же присоединяется к родителю. Можно переписать через третий вариант, у наследников QObject обычно последним параметром можно передать родителя.
- Объект проделывает долгий путь до прикрепления к родителю. Вначале создаём через make_unique (или через QSharedPointer, как подсказывают), потом отбираем у unique_ptr владение и сразу же прикрепляем к родителю. Этот момент можно вынести в отдельную функцию.
Если я всё правильно понимаю, проблема решается в рамках всё тех же make-функций. Я не думаю, что второй случай очень частый, но опыт работы с Qt у меня тоже небольшой. Но идею в список рассылки попробую закинуть :)
C++ без new и delete