Остальных же предупреждаю: Ваш взгляд на Мир C++ уже не останется прежним!
Дело в том, что я нашел изящное решение Большой Проблемы. Но оно сотрясает Основы...
Нет смысла рассказывать раньше времени, потому что вы не поверите. Я бы и сам не поверил. Так что будем читать по порядку.
С уважением, Сергей Деревяго.
А для начала давайте посмотрим на мощный финал раздела Item 27: "Familiarize yourself with alternatives to overloading on universal references" из широко известной в узких кругах книги Scott Meyers "Effective Modern C++":
class Person { public: template< typename T, typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value && !std::is_integral<std::remove_reference_t<T>>::value > > explicit Person(T&& n) : name(std::forward<T>(n)) { static_assert( std::is_constructible<std::string, T>::value, "Parameter n can't be used to construct a std::string" ); } private: std::string name; }; |
Ой!.. Что это было??
Ну, все это Чудовищное Говно, грубо говоря, призвано научить детей рисовать модные конструкторы класса Person
, эффективно инициализирующие одно единственное поле name
. Ни больше, ни меньше.
Стыдно сказать, но раньше бы мы написали
class Person { public: explicit Person(const std::string& n) : name(n) {} private: std::string name; }; |
и были бы счастливы. А почему, собственно, стыдно? А потому, что с простым и понятным кодом невозможно всучить населению книгу по имени "Effective Modern C++"!
Нет, король там конечно не голый. Король там усердно обвалян в том самом Чудовищном™, а после красиво завернут в блестящий фантик... Конфетка!
И ведь надо признать, что у них получилось: ни разу не видевшие настоящих конфет дети охотно берут то, что дали. Еще и друзьям советуют.
Но только не нам. Такой хоккей нам не нужен! Уж лучше мы будем давиться "Good Old C++", где под фантиком шоколад. Хоть и меньше блестит обертка.
А сейчас ближе к делу. Чисто технически, что же нас не устраивает?
Person
-- это его интерфейс! Выставляя наружу сам факт существования поля std::string
в классе Person
, мы делаем пользователя зависимым от деталей реализации. Как следствие, мы не сможем его без проблем заменить, например, на ders::text
. Т.к. или пользователю придется передавать другие аргументы (неудобно!), или автору продолжать создавать промежуточный string
, а потом его конвертировать в text
(неэффективно!).
Person(T&& n)
вместо привычного Person(const std::string& n)
существенно повлияет на производительность всего приложения? Ну-ну...
Так вот. Все эти бодрые enable_if_t<>
, is_base_of<>
, decay_t<>
, is_integral<>
, remove_reference_t<>
и forward<>
вокруг одного бедного имени... Никого не забыл? Как бы нам от них разом избавиться. Чтобы как в Сказке: дал коршня и разлетелись!
Увы, но в лоб не получится. Ведь эти гадости следствие, а не причина! Они неизбежно заводятся следом за паразитами. А паразитов я вам сейчас подсвечу:
class Person { public: template< typename T, typename = std::enable_if_t< !std::is_base_of<Person, std::decay_t<T>>::value && !std::is_integral<std::remove_reference_t<T>>::value > > explicit Person(T&& n) : name(std::forward<T>(n)) { static_assert( std::is_constructible<std::string, T>::value, "Parameter n can't be used to construct a std::string" ); } private: std::string name; }; |
Да, именно эти клопы-кровососы нагло впиваются в беззащитное тело кода! По нашей собственной глупости. А потом на них виснут enable_if<>
, decltype(auto)
и те остальные Чудовища™. Но все гениальное просто:
Но это же НЕВОЗМОЖНО!
А если подумать?.. А если подумать, то очень возможно. Но сначала пройдемся по ссылкам.
Как же так получилось, что все эти годы они безнаказанно портили нашу кровь? Открываем раздел 3.7 References книги Bjarne Stroustrup "The Design and Evolution of C++" и внимательно смотрим на текст:
References were introduced primarily to support operator overloading. Doug McIlroy
recalls that once I was explaining some problems with a precursor to the current operator
overloading scheme to him. He used the word reference with the startling effect
that I muttered "Thank you," and left his office to reappear the next day with the current
scheme essentially complete. Doug had reminded me of Algol68.
C passes every function argument by value, and where passing an object by value would be inefficient or inappropriate the user can pass a pointer. This strategy doesn't work where operator overloading is used. In that case, notational convenience is essential because users cannot be expected to insert address-of operators if the objects are large. For example: a = b - c;
a = &b - &c;
&b-&c already has a meaning in C, and I didn't want to change that.
It is not possible to change what a reference refers to after initialization. That is,
once a C++ reference is initialized it cannot be made to refer to a different object later;
it cannot be re-bound. I had in the past been bitten by Algol68 references where
If you want to do more complicated pointer manipulation in C++, you can use pointers. Because C++ has both pointers and references, it does not need operations for distinguishing operations on the reference itself from operations on the object referred to (like Simula) or the kind of deductive mechanism employed by Algol68.
I made one serious mistake, though, by allowing a non- |
"Я совершил одну серьезную ошибку", гы. Да их там тысячи!
Ну ладно, десятки серьезных ошибок испортили вкус C++ еще с самых пеленок. Да что говорить, там буквально вся книга о том как не надо изобретать! Точнее, тащить из других языков что попало. Но это Тема отдельной статьи.
А что же про ссылки? Ну, вы сами все видели: ссылки были добавлены для перегрузки операторов и только. И если бы Страуструп был способен изобрести/подглядеть толковую схему перегрузки операторов на указателях -- мы б никогда не нащупали эти гроздья крючков в собственной гмм... программе. Да, можно сказать и так.
Хуже того! Начав свою жизнь в узкой области операторов, ссылочки живенько разбежались по тельцу. А затем еще более темные головы, азартно почесываясь, изобрели rvalue references... Так и случилось Чудовищное™.
Вы, кстати, читали книгу Nicolai M. Josuttis "C++ Move Semantics - The Complete Guide"? Если нет, то очень рекомендую: 250 страниц про rvalue references! Не, ну буквально: вся книга строго по теме. Все двести страниц!
Просто представьте. Собрались вы запилить будку Шарику. Ну вот это вот все: режем доски, крутим дырки, гвозди заколачиваем... Тянете руку к пакету с гвоздями -- ан нет! Вот тебе, друг, книга на двести страниц "Как доставать гвозди". Не держать и не забивать. Двести честных страниц с примерами, чтобы правильно отхватить болта! А?
Дурь? Нельзя тратить двести страниц на мелочь?? А почему тогда можно на move semantics?! Это ж какие там гвозди, что хрен ухватишь! А попробуй еще забей... В общем, не удивлюсь, если вам сейчас кажется, что двойные крючки есть Вершина Чудовищного™. Самое днище.
Пристегните ремни, нам постучали снизу.
Та-дам! На сцену выходит раздел 10.4.2 "Why && for Both Ordinary Rvalues and Universal References?":
Universal/forwarding references use the same syntax as ordinary rvalue references, which is a serious source
of trouble and confusion. So why did we not introduce a specific syntax for universal references?
An alternative proposal, for example, might have been (and is sometimes discussed as a possible fix):
|
"Эти три амперсанда могут выглядеть слишком нелепо" -- да чо уж там, давай четыре! Гулять так гулять.
Как бы дико оно не звучало, мы только приобрели!
Не верите? Тогда самое время опять обратиться к Истокам. Без предрассудков.
Да, неприятно об этом думать, но надо признать, что за всю свою долгую жизнь гражданин Страуструп совершил много серьезных ошибок. И конструктор копирования есть одна из самых Чудовищных™!
В силу того, что такие конструкторы являются прямым следствием C++ value semantics, проиллюстрируем суть явления простым наглядным примером. Давайте представим, что некое многолетнее дарование (малолетнее, как вариант) решило набросить очередную либу в многострадальный boost.org: использовать value semantics для чтения файлов!
Дарование замечает, что файл можно и не копировать, если оригинал больше не нужен владельцу. В этом случае мы просто переименовываем файл на сторону и все идет своим чередом. Это победа! Переименование гораздо быстрее копирования. Интернеты захлебываются чудесными тестами, разработчики курят новый букварь. Мрачно скрипя зубами.
Прекрасно? Шикарно! Теперь многие годы все учат друг друга делать модный rename... Но не лучше ли сразу читать нужный файл?! Что-что говорите, value semantics не велит? Пожмем ей руку и пошлем вслед за ссылками!
Теперь вы тоже видите Большую Ложь. И так и хочется спросить прищурив глаз: автоматически генерируемые конструкторы копирования -- это глупость или предательство? Обширные страницы текста, десятки детальных примеров без четкого предупреждения "копирование -- Зло!", глупость или предательство?!
Тогда допустим, что надо копировать и придем к противоречию. ОК, раз move constructor (переименование файла) работает быстрее copy constructor (копирование файла), то мы попробуем его оптимизировать.
Как же устроен класс, идеально подходящий для реализации конструктора перемещения? Да, идиома pImpl. Она самая:
class Widget { public: // ... Widget(Widget&& rhs) { pImpl=rhs.pImpl; rhs.pImpl=0; } private: struct Impl; Impl* pImpl; }; |
Если все наши данные живут в отдельной структуре, то перемещение -- это просто присваивание указателя. Красота!
Красота? Ну так давайте заодно улучшим и Widget::Impl
, тоже превратив ее в указатель на имплементацию:
struct Widget::Impl { struct Impl2; Impl2* pImpl2; }; |
Тем самым мы получаем... глупость? А разве Widget::Impl
не глупость?! Что мешает пользователю Widget
самому себе создать указатели и "перемещать" их в любую удобную сторону?
Смотрите, мы один раз на куче создаем объект Widget
и никуда его больше не двигаем. Перемещаем и копируем лишь указатели на объект: обычные или умные. И дело сделано! Не нужны больше эти конструкторы нашему классу (а значит и ссылки). Они давно уже реализованы в умных указателях. Раз и навсегда.
А что же копирование? Бывают же случаи, когда нужна копия?
Конечно бывают. Но не только лишь все. И тут обычно оказывается, что у класса уже есть подходящий конструктор, с помощью которого мы можем создать "копию" передавая те же аргументы, что когда-то оригиналу. А если нет, то лучше добавить функцию copy()
, возвращающую умный указатель на созданную на куче копию. Для интерфейсов ее называют clone()
.
В любом случае, воздержитесь от определения обычного конструктора копирования и оператора присваивания, т.к. "обобщенные алгоритмы" и "узкие специалисты" сразу же начнут копировать и двигать.
Ну как можно подозревать создателей C++ в элементарной безграмотности?! Наш вариант -- подозревать с особым цинизмом!
Ща развернем еще одну конфетку, несите краги и противогаз!
А поможет нам в этом деле книга Nicolai M. Josuttis "The C++ Standard Library. Second Edition". Раздел 3.1.4 "Range-Based for
Loops". Да, это полный 3.14...
To multiply each element elem of a vector vec by 3 you can program as follows:
std::vector<double> vec; ... for ( auto& elem : vec ) { elem *= 3; }Here, declaring elem as a reference is important because otherwise the statements in the body of the
for loop act on a local copy of the elements in the vector (which sometimes also might be useful).
This means that to avoid calling the copy constructor and the destructor for each element, you should usually declare the current element to be a constant reference. Thus, a generic function to print all elements of a collection should be implemented as follows: template <typename T> void printElements (const T& coll) { for (const auto& elem : coll) { std::cout << elem << std::endl; } }Here, the range-based for statement is equivalent to the following:
{ for (auto _pos=coll.begin(); _pos != coll.end(); ++_pos) { const auto& elem = *_pos; std::cout << elem << std::endl; } } |
А? Моща?!
Только заюзал range-based loop и все друзья с нами: copy constructor и/или reference! И не выпихнешь, сомкнули морды!
Дорогие создатели! Не кажется ли вам, что человеку нужен просто итератор? Тот самый, который _pos
. А если вдруг еще понадобится *_pos
, то мы и сами его сделаем. Нет?
А вот нет! Дают только *_pos
. А раз так, пусть сами давятся своей петлей!
Резюмирую: что бы ни писали в букварях, не давайте детям range-based for
loop! У них там ошибка. Видимо в ДНК.
for
loop!
Короче,
swap()
.
int
, double
, void*
...).
std::complex
, ders::strsz
...).
Widget*
означает, что вам передали чужой объект. Он может исчезнуть сразу же после вызова.
un_ptr<Widget>
означает, что вам передали объект во владение. Вы теперь его единственный хозяин.
sh_ptr<Widget>
означает, что вам передали объект с совместным владением. Вы теперь еще один хозяин в этом же потоке (thread).
ders::mem_pool
.
ders::sh_ptr
.
mem_pool
и sh_ptr
это было бы не эффективно.
Для реальной проверки нашей смелой теории была написана минимально полезная программа ненулевой длины. Ее задачей является анализ исходного кода на повторно включаемые заголовочные файлы. Например, если ваша программа включает:
#include "file1.hpp" #include <ders/file2.hpp> #include <file3.hpp> |
То нет смысла включать <file3.hpp>
, если он уже был прямо или косвенно включен в "file1.hpp"
или <ders/file2.hpp>
. Короче, все очень просто. Пока не начнешь.
А потом вдруг оказывается, что задача со звездочкой! И даже корректный учет комментариев не является тривиальной задачей (например, строка "/*"
-- это не комментарий).
Или вот есть нюанс: все хедеры derslib первым делом включают <ders/config.hpp>
, но это не бессмысленный повтор! Т.к. можно использовать только один заголовок и config.hpp
там обязан присутствовать.
Не обошлось и без рекурсивного обхода дерева, столь любимого олимпиадниками... В общем, читайте исходники и будьте уверены, что с первого раза ничего не поймете. Но это не главное. Главное -- следовать правилам правописания! Без шума и пыли, по новоутвержденному плану.
Итак, программа inchk предназначена для поиска повторно включаемых хедеров в исходных файлах.
Параметры командной строки:
inchk -Iinclude/dir [...] source/file.cpp [...]
-Iinclude/dir |
директория для поиска заголовочных файлов. как и компилятору, ей можно задать несколько директорий. |
source/file.cpp |
исходный файл для анализа. можно задать несколько. |
Примеры использования:
inchk.exe -I..\derslib\inc *.cpp ..\derslib\src\*.cpp
./inchk -I../derslib/inc ../derslib/src/posix/*.cpp ../derslib/src/thread/*.cpp
Как вы уже догадались, текущий derslib не содержит повторов. Например, вот анализ самого incfile.cpp
:
incfile.cpp: 9: "incfile.hpp" -> incfile.hpp 10: <vector> 11: <ders/text.hpp> -> ..\derslib\inc\ders\text.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <string.h> 12: <ders/ptr.hpp> -> ..\derslib\inc\ders\ptr.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <type_traits> 12: <ders/mp_new.hpp> -> ..\derslib\inc\ders\mp_new.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <new> 12: <typeinfo> 13: <utility> 14: <assert.h> 15: <ders/mem_pool.hpp> -> ..\derslib\inc\ders\mem_pool.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <stddef.h> 10: <ders/dir.hpp> -> ..\derslib\inc\ders\dir.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <vector> 12: <ders/exception.hpp> -> ..\derslib\inc\ders\exception.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <exception> 12: <system_error> 13: <ders/tx_buf.hpp> -> ..\derslib\inc\ders\tx_buf.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <stdint.h> 12: <ders/text.hpp> -> ..\derslib\inc\ders\text.hpp 11: <ders/fd_file.hpp> -> ..\derslib\inc\ders\fd_file.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <ders/exception.hpp> -> ..\derslib\inc\ders\exception.hpp 12: <ders/rdwr.hpp> -> ..\derslib\inc\ders\rdwr.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <system_error> 12: <ders/text.hpp> -> ..\derslib\inc\ders\text.hpp 12: <ders/hash.hpp> -> ..\derslib\inc\ders\hash.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <unordered_map> 12: <unordered_set> 13: <ders/mp_allocator.hpp> -> ..\derslib\inc\ders\mp_allocator.hpp 10: <ders/config.hpp> -> ..\derslib\inc\ders\config.hpp 11: <memory> 12: <ders/mem_pool.hpp> -> ..\derslib\inc\ders\mem_pool.hpp 14: <ders/text.hpp> -> ..\derslib\inc\ders\text.hpp |
Но предыдущая версия имела проблемы:
mtprog\derslib\src\cycl_buf.cpp: 15: <ders/cycl_buf.hpp> -> mtprog\derslib\inc\ders\cycl_buf.hpp 18: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 19: <ders/sh_ptr.hpp> -> mtprog\derslib\inc\ders\sh_ptr.hpp 18: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 19: <ders/destroy.hpp> -> mtprog\derslib\inc\ders\destroy.hpp 19: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 20: <typeinfo> 21: <assert.h> 22: <ders/hard_asrt.hpp> -> mtprog\derslib\inc\ders\hard_asrt.hpp 18: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 23: <ders/mem_pool.hpp> -> mtprog\derslib\inc\ders\mem_pool.hpp 19: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 20: <new> 21: <stddef.h> 16: <assert.h> warning: mtprog\derslib\src\cycl_buf.cpp:16: <assert.h> already included 17: <string.h> mtprog\derslib\src\data_queue.cpp: 15: <ders/data_queue.hpp> -> mtprog\derslib\inc\ders\data_queue.hpp 20: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 21: <ders/data_io.hpp> -> mtprog\derslib\inc\ders\data_io.hpp 20: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 21: <ders/cycl_buf.hpp> -> mtprog\derslib\inc\ders\cycl_buf.hpp 18: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 19: <ders/sh_ptr.hpp> -> mtprog\derslib\inc\ders\sh_ptr.hpp 18: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 19: <ders/destroy.hpp> -> mtprog\derslib\inc\ders\destroy.hpp 19: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 20: <typeinfo> 21: <assert.h> 22: <ders/hard_asrt.hpp> -> mtprog\derslib\inc\ders\hard_asrt.hpp 18: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 23: <ders/mem_pool.hpp> -> mtprog\derslib\inc\ders\mem_pool.hpp 19: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 20: <new> 21: <stddef.h> 22: <ders/text.hpp> -> mtprog\derslib\inc\ders\text.hpp 20: <ders/config.hpp> -> mtprog\derslib\inc\ders\config.hpp 21: <ders/sh_ptr.hpp> -> mtprog\derslib\inc\ders\sh_ptr.hpp 16: <ders/destroy.hpp> -> mtprog\derslib\inc\ders\destroy.hpp warning: mtprog\derslib\src\data_queue.cpp:16: <ders/destroy.hpp> already included |
Самое время проверить свои проекты, проблемы как правило есть!
Даже если вы не согласны с теорией, цифры говорят за себя. В этом разделе находятся тесты производительности самых базовых возможностей derslib. Как вы скоро увидите, за прошедшие 15 лет производительность стандартных библиотек C++ заметно подтянулась и речь уже не идет про десятки и сотни раз выигрыша. Тем не менее, и пятикратное ускорение -- это Много! Это все еще другой Качественный уровень, делающий возможным невозможное (для отдельных встраиваемых платформ).
Замеры производились на двух разных ноутбуках. Время работы в секундах, а строки вида std/ders -- отношение времени: то самое количество раз, во сколько ускорились функции.
И не забудьте проверить ваш собственный компилятор, поиграться с настройками. Именно в этом практичность раздела.
std::shared_ptr
использовать ders::sh_ptr
:
perf1/main.cpp |
---|
void start_std(latch* l1, latch* l2) { shared_ptr<int> sp(new int(5)); vector<shared_ptr<int>> v; v.reserve(M); l1->arrive_and_wait(); for (int i=0; i<N; i++) { for (int j=0; j<M; j++) v.push_back(sp); v.clear(); } l2->arrive_and_wait(); } void start_ders(latch* l1, latch* l2) { mem_pool mpo, *mp=&mpo; sh_ptr<int> sp(mp, mp_new<int>(mp, 5)); vector<sh_ptr<int>> v; v.reserve(M); l1->arrive_and_wait(); for (int i=0; i<N; i++) { for (int j=0; j<M; j++) v.push_back(sp); v.clear(); } l2->arrive_and_wait(); } |
Усредненные результаты запусков представлены в следующей таблице:
Ubuntu 16, g++ 5.4.0 | Windows 10, g++ 12.2.0 | |||||
num_threads | 1 | 2 | 4 | 1 | 2 | 4 |
std | 1.244 | 1.274 | 2.456 | 1.398 | 1.413 | 1.584 |
ders | 0.279 | 0.295 | 0.391 | 0.259 | 0.249 | 0.286 |
std/ders | 4.5 | 4.3 | 6.3 | 5.4 | 5.7 | 5.5 |
Как можно видеть, функция ускорилась в 4-6 раз! Это цена atomic increment, на всякий случай добавленного в std::shared_ptr
авторами. Ну, это как возить с собой мешок песка. Мало ли.
К слову сказать, я уже объяснял, почему mutex-ы на всякий случай -- это плохая идея.
std::allocator
, чью производительность не дано превзойти простым смертным.
Нам заранее стыдно, но все же попробуем:
perf2/main.cpp |
---|
void start_std(latch* l1, latch* l2) { list<int> lst; l1->arrive_and_wait(); for (int i=0; i<N; i++) { for (int j=0; j<M; j++) lst.push_back(j); for (int j=0; j<M; j++) lst.pop_front(); } l2->arrive_and_wait(); } void start_ders(latch* l1, latch* l2) { mem_pool mpo; mp_allocator<int> mpa(&mpo); list<int, mp_allocator<int>> lst(mpa); l1->arrive_and_wait(); for (int i=0; i<N; i++) { for (int j=0; j<M; j++) lst.push_back(j); for (int j=0; j<M; j++) lst.pop_front(); } l2->arrive_and_wait(); } |
Не зря старались!
Ubuntu 16, g++ 5.4.0 | Windows 10, g++ 12.2.0 | |||||
num_threads | 1 | 2 | 4 | 1 | 2 | 4 |
std | 2.318 | 2.340 | 3.496 | 3.272 | 3.476 | 5.059 |
ders | 0.450 | 0.468 | 0.654 | 0.404 | 0.422 | 0.589 |
std/ders | 5.2 | 5.0 | 5.3 | 8.1 | 8.2 | 8.6 |
Функция ускорилась в 5-8 раз. А все потому, что ders::mem_pool
не дергает глобальный operator new()
по малейшему поводу, а выделяет память большими блоками. Очевидно, но до сих пор эффективно!
И не забудьте передать привет Уважаемым Профессионалам. Хотя... Они там давно с приветом.
Ну то есть, раз ders::sh_ptr
обеспечивает четырехкратное ускорение, а ders::mp_allocator
-- пятикратное, то надо их скомбинировать для двадцатикратного:
perf3/main.cpp |
---|
void start_std(latch* l1, latch* l2) { shared_ptr<int> sp(new int(5)); list<shared_ptr<int>> lst; l1->arrive_and_wait(); for (int i=0; i<N; i++) { for (int j=0; j<M; j++) lst.push_back(sp); for (int j=0; j<M; j++) lst.pop_front(); } l2->arrive_and_wait(); } void start_ders(latch* l1, latch* l2) { mem_pool mpo, *mp=&mpo; sh_ptr<int> sp(mp, mp_new<int>(mp, 5)); mp_allocator<sh_ptr<int>> mpa(mp); list<sh_ptr<int>, mp_allocator<sh_ptr<int>>> lst(mpa); l1->arrive_and_wait(); for (int i=0; i<N; i++) { for (int j=0; j<M; j++) lst.push_back(sp); for (int j=0; j<M; j++) lst.pop_front(); } l2->arrive_and_wait(); } |
Смотрим на результаты:
Ubuntu 16, g++ 5.4.0 | Windows 10, g++ 12.2.0 | |||||
num_threads | 1 | 2 | 4 | 1 | 2 | 4 |
std | 2.893 | 2.904 | 4.184 | 6.145 | 8.235 | 7.632 |
ders | 0.486 | 0.503 | 0.673 | 0.613 | 0.676 | 0.755 |
std/ders | 6.0 | 5.8 | 6.2 | 10.0 | 12.2 | 10.1 |
И... эх! Так оно не работает. Хоть и вышло быстрее, но конечно не в двадцать раз.
Тем самым еще раз напомним, что производительность всего приложения не следует буквально за библиотекой. Да, код существенно ускоряется, а потребление памяти заметно уменьшается. Но не в ТАК много раз!
Вы не поверите, но этой песне почти четверть века! Многие мои читатели еще не родились, когда я уже вел переписку со Страуструпом по поводу производительности fstream: "главная сила языка C -- в его способности считывать символы и решать, что с ними ничего не надо делать -- причем выполнять это быстро. Это действительно важное достоинство, которое нельзя недооценивать, и цель C++ -- не утратить его".
Боже мой, сколько пафоса!
Особенно когда знаешь, что fstream тормозили с рождения. И дела потом шли только хуже.
Изначально, в том самом разделе C++ 3rd: комментарии, более двадцати лет назад, была намеряна одиннадцатикратная разница! Но годы шли и появилась надобность проверить производительность многопоточных приложений. И она порой достигала тридцатикратной разницы!! Обратите при этом внимание, что многопоточная производительность derslib сравнивалась с изначально более быстрыми C-шными потоками FILE
, а не тормознутыми fstream.
Посмотрим, что стало сейчас:
perf4/main.cpp |
---|
void start_c(ThreadData* td) { FILE* f=fopen(td->path, "rb"); if (!f) { td->err="can't open file"; return; } td->lch1->arrive_and_wait(); for (int ch; (ch=getc(f))!=EOF; ) td->sum+=ch; td->lch2->arrive_and_wait(); fclose(f); } void start_cpp(ThreadData* td) { ifstream f(td->path, ios_base::in|ios_base::binary); if (!f) { td->err="can't open file"; return; } td->lch1->arrive_and_wait(); for (int ch; (ch=f.get())!=EOF; ) td->sum+=ch; td->lch2->arrive_and_wait(); } void start_ders(ThreadData* td) { mem_pool mpo, *mp=&mpo; error_code ec; fd_file f(mp, strsz(td->path), md_rdo, &ec); if (ec) { td->err="can't open file"; return; } buf_rd bf(mp, &f); td->lch1->arrive_and_wait(); for (int ch; (ch=bf.get())!=-1; ) td->sum+=ch; td->lch2->arrive_and_wait(); } |
Удивительный результат!
Ubuntu 16, g++ 5.4.0, 141 MB | Windows 10, g++ 12.2.0, 142 MB | |||||
num_threads | 1 | 2 | 4 | 1 | 2 | 4 |
c | 2.185 | 2.209 | 9.076 | 5.179 | 5.223 | 19.742 |
cpp | 0.759 | 0.793 | 3.704 | 1.323 | 1.365 | 3.066 |
ders | 0.234 | 0.253 | 0.877 | 0.358 | 0.368 | 0.590 |
c/ders | 9.3 | 8.7 | 10.3 | 14.5 | 14.2 | 33.5 |
cpp/ders | 3.2 | 3.1 | 4.2 | 3.7 | 3.7 | 5.2 |
Как можно видеть, Уважаемые Профессионалы блестяще справились с задачей -- они тупо замедлили C-шные FILE
! И если derslib стабильно быстрее fstream в 3-5 раз, то классический FILE
уже может отстать в 33 с половиной раза!! Это какой-то позор...
Но все же итоги хорошие: обязательно используйте ders::fd_file
c buf_rd
/buf_wr
для буферизованного чтения/записи вместо fstream и особенно FILE
! Это заметно ускорит многопоточные приложения.
Поддержка давно устаревшего стандарта была выбрана не случайно, т.к. абсолютное большинство C++ разработчиков, так или иначе, вынуждено иметь дело со старым кодом и компиляторами. А минимальной платформой является Ubuntu 16 LTS, g++ 5.4.0 (чему нет оправдания).
В общем, если читали Многопоточное программирование, то уже знаете что к чему. Я только напомню, что оптимизируя на скорость исполнения, не забывайте дефайнить NDEBUG
. Иначе все грустно.
Ну и, конечно, реальный код -- лучший учитель! Если что-то не ясно, смотрите как сделан inchk.
Содержит зависящие от платформы define
-ы, устраняющие ошибки компиляции.
list_dir()
позволяет получить список имен, принадлежащих директории.
get_info()
позволяет получить информацию о файле или директории.
name()
определена пара функций:
name(... ,std::error_code* ec)
-- возвращает ошибку в ec
.
ex_name(...)
-- возбуждает FileException
.
Прежде всего отмечу, что двадцать лет назад я изобрел исключения в виде умных указателей. Этот материал все еще актуален, так что для лучшего понимания имеет смысл перечитать раздел: для абсолютного большинства программистов принципы обработки исключений, изложенные далее, окажутся совершенно неожиданными.
Но время не стоит на месте, и на сегодняшний день мы возбуждаем исключения с помощью функции newExc()
в виде объекта sh_ptr<Exception>
:
exc/main.cpp |
---|
#include <ders/fd_file.hpp> #include <vector> using namespace std; using namespace ders; class MyException : public Exception { public: MyException(mem_pool* mp, FileLine loc, strsz msg) : Exception(mp, loc, msg) {} strsz className() const override { return strsz("MyException"); } void delete_this(mem_pool* mp) const override { delete_ptr(mp, this); } }; void g(mem_pool *mp, int i) { try { switch (i) { case 1: throw newExc<MyException>(mp, mp, FLLN, "Hello from g()"); case 2: { vector<int> v; v.at(0); } case 3: throw 3; case 4: throw newExc<ExitMsgException>(mp, mp, FLLN, "Goodbye from g()", i); } } catch (...) { throw newExc<Exception>(mp, FLLN, "Problems in g()", recatch(mp, FLLN)); } } void f(mem_pool *mp, int i) { try { g(mp, i); } catch (...) { throw newExc<Exception>(mp, FLLN, "Problems in f()", recatch(mp, FLLN)); } } int main() { mem_pool mpo, *mp=&mpo; fd_file out(mp, fd_out); for (int i=1; i<=4; i++) { try { f(mp, i); } catch (...) { auto se=recatch(mp, FLLN, false); out.ex_write(tx_buf(mp)+"\tException #"+i+":\n"+toTextAll(get(se))+"\n"); if (auto em=se->is<ExitMsgException>()) { out.ex_write(tx_buf(mp)+"\t"+em->message()+"\n"); exit(em->exitCode()); } } } } |
Результатом работы является:
Exception #1: ders::Exception [main.cpp:45], message: Problems in f() ders::Exception [main.cpp:39], message: Problems in g() MyException [main.cpp:28], message: Hello from g() Exception #2: ders::Exception [main.cpp:45], message: Problems in f() ders::Exception [main.cpp:39], message: Problems in g() ders::StdException [main.cpp:39], type="St12out_of_range", message: vector::_M_range_check: __n (which is 0) >= this->size() (which is 0) Exception #3: ders::Exception [main.cpp:45], message: Problems in f() ders::Exception [main.cpp:39], message: Problems in g() ders::ExternalException [main.cpp:39], message: Unknown exception Exception #4: ders::ExitMsgException [main.cpp:36], exitCode=4, message: Goodbye from g() Goodbye from g() |
Итак, в файле определены следующие классы исключений:
Exception
-- базовый класс всех возбуждаемых исключений. Он содержит следующую информацию:
location()
-- место возбуждения исключения.
message()
-- сообщение.
nested()
-- вложенное исключение (если есть).
ExternalException
-- внешнее исключение (т.е. не sh_ptr<Exception>
). Оно содержит следующую дополнительную информацию:
external()
-- внешнее исключение.
StdException
-- стандартное внешнее исключение (т.е. наследник std::exception
). Оно содержит следующую дополнительную информацию:
type()
-- тип исключения.
ErrorException
-- исключение с кодом ошибки. Оно содержит следующую дополнительную информацию:
error()
-- код ошибки.
FileException
-- исключение ошибки при работе с файлом. Оно содержит следующую дополнительную информацию:
path()
-- путь файла.
MsgException
-- абстрактный базовый класс для исключений-сообщений. По умолчанию, функция recatch()
автоматически перевозбуждает сообщения (параметр rethrowMsg==true
).
ExitMsgException
-- сообщение для завершения приложения. Оно содержит следующую дополнительную информацию:
exitCode()
-- код возврата.
fd_file
-- тонкая обертка над file descriptor файлами.
Он реализует интерфейсы readable
/writable
и при посимвольном чтении/записи работает во много раз быстрее стандартных потоков fstream и FILE
.
Обратите внимание, что для каждой операции name()
в классе определена пара функций:
name(... ,std::error_code* ec)
-- возвращает ошибку в ec
.
ex_name(...)
-- возбуждает FileException
.
mp_unordered_map
и mp_unordered_set
-- стандартные unordered контейнеры, использующие mp_allocator
.
latch
-- простая задвижка для одновременного старта потоков (thread).
Используется, главным образом, для тестирования производительности.
mem_pool
-- кэширующий распределитель памяти (memory pool). Его особенностями являются:
mem_pool
выделяет память блоками и самостоятельно нарезает ее на кусочки требуемого размера. Выделенные блоки памяти освобождаются только в деструкторе ~mem_pool
.
operator new()
/operator delete()
.
mp_buf
-- RAII объект для выделения и автоматического освобождения блока памяти заданного размера.
mp_allocator
-- STL аллокатор, использующий mem_pool
.
Он работает в несколько раз быстрее стандартного.
new
/delete
: функции mp_new()
и mp_delete()
. Они работают через mem_pool
.
Функция mp_delete(mem_pool* mp, T* ptr)
определяет размер объекта как sizeof(T)
. Если вы удаляете объект производного класса через указатель на один из его интерфейсов, то интерфейс должен быть унаследован от deletable
. В этом случае будет вызвана виртуальная функция delete_this()
, которая знает реальный размер.
perm_assert()
-- постоянный assert()
, не зависящий от NDEBUG
.
sh_ptr
и un_ptr
-- концептуальные аналоги std::shared_ptr
и std::unique_ptr
, работающие через mem_pool
.
Их особенностями являются:
get()
и release()
являются функциями-друзьями, а не функциями-членами.ptr.pool()
вместо ptr->pool()
была обычным делом. Сейчас это невозможно, т.к. пул указателя возвращается с помощью выражения pool(ptr).
std::weak_ptr
.
make_sh()
и make_un()
. Это только удобство, никакого уменьшения количества аллокаций (как std::make_shared
) они не дают.
И еще обратите внимание. В случае возникновения bad_alloc
в конструкторе sh_ptr(mem_pool* mp, T* ptr=0)
, переданный объект будет потерян! Это сознательная оптимизация, т.к. пользователи могут использовать промежуточный un_ptr
только там, где действительно нужно. А потом из него создавать sh_ptr
.
По этой причине, make_sh()
работает через make_un()
, а не голый mp_new()
. Тем самым, функция make_sh()
не теряет свои объекты.
buf_rd
и buf_wr
, работающие через интерфейсы readable
и writable
соответственно.
Они реализуют буферизованное посимвольное чтение/запись, позволяющее многократно ускорить работу с файлами.
Его главный класс, естественно, text
-- пример реального класса без конструктора копирования и оператора присваивания. В общем, если вы хотели бы видеть "строку без копирования" -- это тот самый случай! Фактически, text
-- это ядро строки, главный функционал. А полным функционалом является конструкция sh_ptr<text>
, т.е. sh_text
.
Концептуально,
strsz
-- это аналог const char*
.
sh_text
-- это аналог string
.
un_text
-- это строка с единственным владельцем. Ее легко превратить в sh_text
.
strsz
обладает следующими особенностями:
const char*
плюс длина. Самый общий аналог строки для определения параметров функции.
const char*
, он не владеет тем, на что указывает. Для владения нужно использовать text
(завернутый в указатель).
strsz
не использует strlen()
для определения длины "строковых литералов"
. Да, пришлось повозиться.
strsz
определены все операции поиска и сравнения.
text
обладает следующими особенностями:
mem_pool
.
str()
, возвращающую strsz
.
c_str()
, возвращающую const char*
.
tx_buf
-- текстовый буфер для создания длинной строки из отдельных фрагментов.
Он также конвертирует в текст встроенные типы (int
, void*
...).
И не спешите подумать, мол, замахнулся тут на Святое! Не замахнулся, а дал коршня.
Попало и Страуструпу. Но так уж выходит, что я уже более двадцати лет нахожу в его книгах ошибки. А он у меня ни одной.
А если серьезно, то мы здесь не гуманитарии! Нам Факты дороже, чем Мнения. Любые.
И факт в том, что ссылки -- Большая Проблема! Но без них можно жить припеваючи.
Никакая часть данного материала не может быть использована в коммерческих целях без письменного разрешения автора.