diff --git a/src/09_allocations_optimizations.md b/src/09_allocations_optimizations.md index 1517d76..d1781e4 100644 --- a/src/09_allocations_optimizations.md +++ b/src/09_allocations_optimizations.md @@ -6,13 +6,13 @@ — Я хочу. И вставлю в отдельный файл. ## Аллокация памяти на Linux. -На Linux страницы памяти выделяются через [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html), а освобождают через `munmap`. Можно заметить интересный эффект: если мы просто делаем выделение памяти кучу раз, это будет относительно быстро, а когда мы начнём к выделенной памяти обращаться, ты мы зависнем надолго. Почему так? Точнее на вопрос «почему» ответ простой — потому что **ОС даёт нам память только тогда, когда мы её используем**, а вот «зачем» — интересный вопрос. Дело в том, что обычно у нас нет свободной оперативной памяти, она вся либо отдана другим программам, либо используется как дисковый кэш. Поэтому когда кто-то просит память, вам придётся сбрасывать дисковый кэш, либо выгружать кого-то в swap-файлы. А если вдруг ваша программа будет использовать выделенную память как-то потом (или не будет вообще), то давать ей память сразу невыгодно, поэтому ОС даёт её при обращении и маленькими кусочками (ведь используете вы маленькими кусочками, а `mmap` выделяет сразу кучу памяти). Кстати, **в `mmap` есть специальный флаг (`MAP_POPULATE`), который заставляет выделять память сразу**. +На Linux страницы памяти выделяются через [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html), а освобождают через `munmap`. Можно заметить интересный эффект: если мы просто делаем выделение памяти кучу раз, это будет относительно быстро, а когда мы начнём к выделенной памяти обращаться, то мы зависнем надолго. Почему так? Точнее на вопрос «почему» ответ простой — потому что **ОС даёт нам память только тогда, когда мы её используем**, а вот «зачем» — интересный вопрос. Дело в том, что обычно у нас нет свободной оперативной памяти, она вся либо отдана другим программам, либо используется как дисковый кэш. Поэтому когда кто-то просит память, вам придётся сбрасывать дисковый кэш, либо выгружать кого-то в swap-файлы. А если вдруг ваша программа будет использовать выделенную память как-то потом (или не будет вообще), то давать ей память сразу невыгодно, поэтому ОС даёт её при обращении и маленькими кусочками (ведь используете вы маленькими кусочками, а `mmap` выделяет сразу кучу памяти). Кстати, **в `mmap` есть специальный флаг (`MAP_POPULATE`), который заставляет выделять память сразу**. При этом, если сделать так, то работать это будет быстрее, чем если выделить память обычным образом, а потом к ней пообращаться, потому что с флагом не будет происходить множество переходов между userspace'ос и ОС при каждом обращении. Хорошо, а знаете ли вы, что **в `mmap` больше всего времени занимает обнуление выделенной страницы**. Зачем? А вдруг там кто-то криптографию оставил. Кто-то, кого вы только что убили, например. Но вообще под это тоже есть специальный флажок `MAP_UNINITIALIZED` (но только для анонимных страниц), который был создан специально для миниатюрных устройств. **Работает `MAP_UNINITIALIZED`, правда, только в том случае, если ОС была собрана специальным образом**. Если, кстати, делать `mmap` без флага `MAP_POPULATE`, то ещё больше времени, чем на зануление, будет тратиться на передачу обращения между ОС и userspace'ом. Как подобное ленивое выделение работает с точки зрения страничной адресации? Вот так: при запросе выделения памяти через `mmap` ОС не сразу обращается к процессору, а помечает у себя страницы как "*заказанные*". Затем, когда происходит обращение к памяти, получаем ошибку *page fault*, ОС проверяет, если страница выделена, что она мапит её в физическую память, иначе это ошибка. -Как этим можно пользоваться? Типовое использование — это не выделение одной страницы, а сразу порции памяти, которая разбивается на мелкие куски и выдается `malloc`'ом. Когда они выдаются в программе, они мапаются в физическую память. Ещё мапить сразу в физическую память не очень полезно, так как память, не принадлежащая программой, используется ОС, например, как дисковый кэш. +Как этим можно пользоваться? Типовое использование — это выделение не одной страницы, а сразу порции памяти, которая разбивается на мелкие куски и выдается `malloc`'ом. Когда они выдаются в программе, они мапаются в физическую память. Ещё мапить сразу в физическую память не очень полезно, так как память, не принадлежащая программе, используется ОС, например, как дисковый кэш. Зачем нам это знать? Это полезно, если мы что-то бенчмаркаем и выделяем большой массив, первый прогон какого-нибудь алгоритма может быть дольше остальных из-за того, что он сначала не помаплен в память. diff --git a/src/10_libraries.md b/src/10_libraries.md index e4c9775..0543497 100644 --- a/src/10_libraries.md +++ b/src/10_libraries.md @@ -24,6 +24,7 @@ int main(){ } ``` ```c++ +// five.cpp #include int sum(int a, int b); diff --git a/src/14_templates.md b/src/14_templates.md index 59527e8..ea5ca7f 100644 --- a/src/14_templates.md +++ b/src/14_templates.md @@ -165,7 +165,7 @@ void baz(int*) {} ```c++ int main() { - foo(nullptr); + baz(nullptr); } ``` Давайте подумаем, работает ли это, если мы включим `ENABLE_TEMPLATE`. А вот не работает, потому что непонятно, чему равно `T`. А вот с перегрузкой всё работает (выбирается перегрузка). Почему это так работает, хочется спросить? @@ -177,9 +177,9 @@ int main() { Подробнее про *Template argument deduction* на [cppreference](https://en.cppreference.com/w/cpp/language/template_argument_deduction). -Поэтому когда мы подставляем `nullptr`, то из него нельзя понять, на какой тип он указывает, поэтому *deduction* провалится, и мы получаем ошибку. Если же есть `void foo(int*)`, то выбирается он, как единственный подходящий. +Поэтому когда мы подставляем `nullptr`, то из него нельзя понять, на какой тип он указывает, поэтому *deduction* провалится, и мы получаем ошибку. Если же есть `void baz(int*)`, то выбирается он, как единственный подходящий. -Кстати, можно немного изменить работу с перегрузками. Можно вызывать функции не как `foo(...)`, а как `foo<>(...)`. В таком случае вы явно отбросите всё, что не является шаблоном, а значит выбирать будете только из специализаций. +Кстати, можно немного изменить работу с перегрузками. Можно вызывать функции не как `baz(...)`, а как `baz<>(...)`. В таком случае вы явно отбросите всё, что не является шаблоном, а значит выбирать будете только из специализаций. ## Non-type template parameter. @@ -439,7 +439,7 @@ int main(){ return 0; } ``` -Без *main.cpp* компилируется, так как у `a` не вызывался деструктор, поэтому он не инстанцировался. С *main.cpp* компилятор генерирует деструктор, который вызывает деструкторы всех членов класса, а там `unigue_ptr`, у которого при компиляции будет инстанцироваться деструктор. В `unique_ptr` есть специальная проверка, что если удаляется incomplete type (а у нас `object` именно таковой), то это ошибка.\ +Без *main.cpp* компилируется, так как у `a` не вызывался деструктор, поэтому он не инстанцировался. С *main.cpp* компилятор генерирует деструктор, который вызывает деструкторы всех членов класса, а там `unique_ptr`, у которого при компиляции будет инстанцироваться деструктор. В `unique_ptr` есть специальная проверка, что если удаляется incomplete type (а у нас `object` именно таковой), то это ошибка.\ Как решить проблему? Сделать объявление деструктора в *mytype.h*, а определить его там, где `object` — complete тип (то есть в *mytype.cpp*). Ещё пример: @@ -470,7 +470,7 @@ struct derived : base { В предыдущем примере тот же самый эффект: так как `derived` шаблонный, то он не инстанцируется сразу, но когда мы инстанцируем `derived`, то он создаётся как incomplete (complete он станет после подстановки базовых классов), происходит подстановка base и получаем ошибку. -В конексте обсуждённого выше может быть интересно прочитать про идиому [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern). +В контексте обсуждённого выше может быть интересно прочитать про идиому [CRTP](https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern). ### Явное инстанцирование шаблонов. diff --git a/src/16_namespaces_using_adl.md b/src/16_namespaces_using_adl.md index df7ceb8..5c45346 100644 --- a/src/16_namespaces_using_adl.md +++ b/src/16_namespaces_using_adl.md @@ -296,6 +296,11 @@ int main() { Немного best practices о том, как надо делать `swap`: ```c++ +namespace my_lib { + struct big_integer {}; + void swap(big_integer&, big_integer&) {/* ... */} +} + template void foo(T a, T b) { // ... @@ -303,6 +308,14 @@ void foo(T a, T b) { swap(a, b); // ... } + +int main() { + my_lib::big_integer a1, b1; + foo(a1, b1); // выбирается my_lib::swap + + int a2, b2; + foo(a2, b2); // выбирается std::swap +} ``` Теперь у нас получается шаблонный `std::swap` и, возможно, есть не-шаблонный ADL. - Если есть ADL, выбирается он, потому что из шаблонного и не-шаблонного выбирается второй. diff --git a/src/17_move_rvalue.md b/src/17_move_rvalue.md index 045fadf..08d4969 100644 --- a/src/17_move_rvalue.md +++ b/src/17_move_rvalue.md @@ -180,7 +180,7 @@ struct person { } ``` -Так мы получим нужно поведение за исключением того, что иногда move-конструктор вызовется лишний раз (когда делаем копию и вызываем move). Обычно это оптимизируется и не влияет на производительность, но при необходимости можно сделать разные перегрузки. +Так мы получим нужное поведение за исключением того, что иногда move-конструктор вызовется лишний раз (когда делаем копию и вызываем move). Обычно это оптимизируется и не влияет на производительность, но при необходимости можно сделать разные перегрузки. **Не стоит использовать move там, где он не нужен**: diff --git a/src/20_perfect_forwarding.md b/src/20_perfect_forwarding.md index 3b34fda..060bd8e 100644 --- a/src/20_perfect_forwarding.md +++ b/src/20_perfect_forwarding.md @@ -271,3 +271,56 @@ void write(T0 const& arg0, Ts const& ...args) { write(args...); } ``` + +## Fold Expressions + +В **C++17** появилась фича **Fold Expressions**, позволяющая сделать свёртку по элементам пака относительно бинарного оператора. + +Рассмотрим простой пример свёртки: + +```c++ +int sum(int... args) { + return (args + ...); +} + +int foo() { + return sum(1, 2, 3, 4); +} + +//Вызов функции sum(1, 2, 3, 4) раскроется в ((1 + 2) + 3) + 4 +``` + +Мотивирующий пример для метапрограммирования: попробуем научиться проверять, есть ли тип (`T`) в паке. Так как в шаблонном паке мы не можем получить доступ к конкретному элементу (до C++26), а может только отделять первые несколько элементов `Head` и все остальные `Tail`, очевидна следующая реализация: + +```c++ +template +struct has_type; + +template +struct has_type { + static constexpr bool value = std::is_same_v || + has_type::value; +// ^^^^^^^^^^^^^^^^^^^^^^^^ инстанс специализации шаблона +}; + +template +struct has_type { + static constexpr bool value = std::is_same_v; +}; +``` + +Чем плоха такая реализация? Предположим в паке `N` элементов, тогда компилятор будет проводить `N` инстансов шаблона `has_type`. Как можно это пофиксить? Вспомним, что `...` ставится там, где перечисляются элементы. В `Fold expressions` это и используется. Тогда предыдущий пример можно переписать так: + +```c++ +template +struct has_type { + static constexpr bool value = (std::is_same_v || ...); +}; +``` + +Чем это хорошо? +- Нет рекурсии. +- Быстрее время компиляции, потому что меньше инстансов и у компилятора есть возможность делать оптимизации. +- Легче читается и меньше кода. + +Подробнее про Fold expressions на [cppreference](https://en.cppreference.com/w/cpp/language/fold.html). diff --git a/src/21_decltype_auto_nullptr.md b/src/21_decltype_auto_nullptr.md index 8bd8698..ea1fcfb 100644 --- a/src/21_decltype_auto_nullptr.md +++ b/src/21_decltype_auto_nullptr.md @@ -169,7 +169,7 @@ auto f(bool flag) { // COMPILE ERROR ```c++ struct nullptr_t { template - opeartor T*() const { + operator T*() const { return 0; } } diff --git a/src/22_lambdas_type_erasure.md b/src/22_lambdas_type_erasure.md index 4da5b05..e60ea7e 100644 --- a/src/22_lambdas_type_erasure.md +++ b/src/22_lambdas_type_erasure.md @@ -25,9 +25,9 @@ struct less { // можно сделать класс со static-функцией, но это не даёт выигрыша // так как ABI устроен так, что пустые структуры можно не копировать -int foo (vector& v) { - std::sort(v.begin(), v.end(), less()); - std::sort(v.begin(), v.end(), &int_less); +void foo (vector& v) { + std::sort(v.begin(), v.end(), less()); // 1. + std::sort(v.begin(), v.end(), &int_less); // 2. } ``` @@ -375,7 +375,7 @@ auto bind(F f, Args... args) { } ``` -На смом деле, там [немного сложнее](https://en.cppreference.com/w/cpp/utility/functional/bind): можно закреплять конкретные аргументы, а остальные принимать при вызове. +На самом деле, там [немного сложнее](https://en.cppreference.com/w/cpp/utility/functional/bind): можно закреплять конкретные аргументы, а остальные принимать при вызове. ### Рекурсивный вызов лямбд