Почему в программах следует избегать лишних обращений в память

Обновлено: 04.07.2024

В этом руководстве мы узнаем, как Python управляет памятью или как обрабатывает наши данные внутренне. Это руководство даст глубокое понимание основ управления памятью Python. Когда мы выполняем наш скрипт, в его памяти запускается много логических процессов, чтобы сделать код эффективным.

Вступление

Управление памятью в Python очень важно для разработчиков программного обеспечения для эффективной работы с любым языком программирования. Мы не можем упускать из виду важность управления памятью при реализации большого количества данных. Неправильное управление памятью приводит к замедлению работы приложения и серверных компонентов. Это также становится причиной неправильной работы. Если память не используется должным образом, предварительная обработка данных займет много времени.

В Python памятью управляет менеджер Python, который определяет, куда поместить данные приложения в память.

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

С другой стороны, когда данные больше не используются, они могут быть удалены менеджером памяти Python. Но вопрос в том, как? А откуда взялось это воспоминание?

Распределение памяти Python

  • Распределение статической памяти
  • Распределение динамической памяти

Статической памяти

Размещение стека

Структура данных Stack используется для хранения статической памяти. Это необходимо только внутри конкретной функции или вызова метода. Функция добавляется в стек вызовов программы всякий раз, когда мы ее вызываем. Назначение переменной внутри функции временно сохраняется в стеке вызова функции; функция возвращает значение, и стек вызовов переходит к текстовой задаче. Компилятор обрабатывает все эти процессы, поэтому нам не нужно об этом беспокоиться.

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

Динамической памяти

Как мы знаем, все в Python является объектом, а это означает, что динамическое распределение памяти вдохновляет управление памятью Python. Диспетчер памяти Python автоматически исчезает, когда объект больше не используется.

Распределение памяти в куче

Структура данных кучи используется для динамической памяти, которая не имеет отношения к именованию аналогов. Это тип памяти, который используется вне программы в глобальном пространстве. Одним из лучших преимуществ кучи памяти является то, что она освобождает пространство памяти, если объект больше не используется или узел удален.

В приведенном ниже примере мы определяем, как переменная функции хранится в стеке и куче.

Реализация Python по умолчанию

В основном язык Python написан на английском языке. Однако это определено в справочном руководстве, что само по себе бесполезно. Итак, нам нужен код, основанный на интерпретаторе правила в руководстве.

Преимущество реализации по умолчанию заключается в том, что она выполняет код Python на компьютере, а также преобразует наш код Python в инструкции. Итак, мы можем сказать, что реализация Python по умолчанию удовлетворяет обоим требованиям.

Программа, которую мы пишем на языке Python, сначала преобразует в байт-код компьютерных инструкций. Виртуальная машина интерпретирует этот байт-код.

Сборщик мусора Python

Как мы объясняли ранее, Python удаляет те объекты, которые больше не используются, можно сказать, что освобождает пространство памяти. Этот процесс удаления ненужного пространства памяти объекта называется сборщиком мусора. Сборщик мусора Python запускает свое выполнение вместе с программой и активируется, если счетчик ссылок падает до нуля.

Когда мы назначаем новое имя или помещаем его в контейнеры, такие как словарь или кортеж, счетчик ссылок увеличивает свое значение. Если мы переназначаем ссылку на объект, счетчик ссылок уменьшает свое значение. Он также уменьшает свое значение, когда ссылка на объект выходит за пределы области видимости или объект удаляется.

Как мы знаем, Python использует динамическое выделение памяти, которым управляет структура данных Heap. Куча памяти содержит объекты и другие структуры данных, которые будут использоваться в программе. Диспетчер памяти Python управляет выделением или отменой выделения пространства кучи с помощью функций API.

Объекты Python в памяти

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

Давайте разберемся в следующем примере.

Как видно из вышеприведенного вывода, мы присвоили значение объекту x и распечатали его. Когда мы удаляем объект x и пытаемся получить доступ в дальнейшем коде, будет ошибка, утверждающая, что переменная x не определена.

Следовательно, сборщик мусора Python работает автоматически, и программистам не нужно беспокоиться об этом, в отличие от C.

Подсчет ссылок в Python

Подсчет ссылок показывает, сколько раз другие объекты ссылаются на данный объект. Когда назначается ссылка на объект, счетчик объектов увеличивается на единицу. Когда ссылки на объект удаляются, количество объектов уменьшается. Когда счетчик ссылок становится равным нулю, диспетчер памяти Python выполняет освобождение. Давайте сделаем это простым для понимания.

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

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

Подсчет ссылок

Когда мы присваиваем значение переменной x, целочисленный объект 10 создается в памяти кучи, и его ссылка присваивается x.

y = x

В приведенном выше коде мы присвоили y = x, что означает, что объект y будет ссылаться на тот же объект, потому что Python выделил ту же ссылку на объект новой переменной, если объект уже существует с тем же значением.

Теперь посмотрим на другой пример.

Новый объект ссылки

Переменные x и y не ссылаются на один и тот же объект, потому что x увеличивается на единицу, x создает новый объект ссылки, а y по-прежнему ссылается на 10.

Преобразование сборщика мусора

Сборщик мусора Python классифицировал объекты с помощью своей генерации. Сборщик мусора Python имеет три поколения. Когда мы определяем новый объект в программе, его жизненный цикл обрабатывается первым поколением сборщика мусора. Если объект используется в другой программе, он будет стимулироваться до следующего поколения. У каждого поколения есть порог.

Сборщик мусора вступает в действие, если превышен порог количества выделений за вычетом числа отмененных.

Мы можем изменить пороговое значение вручную с помощью модуля GC. Этот модуль предоставляет метод get_threshold() для проверки порогового значения другого поколения сборщика мусора. Давайте разберемся в следующем примере.

Пороговое значение для запуска сборщика мусора можно изменить с помощью метода set_threshold().

В приведенном выше примере значение порога увеличилось для всех трех поколений. Это повлияет на частоту запуска сборщика мусора. Программистам не нужно беспокоиться о сборщике мусора, но он играет важную роль в оптимизации среды выполнения Python для целевой системы.

Сборщик мусора Python обрабатывает низкоуровневые детали для разработчика.

Важность выполнения ручной сборки мусора

Как мы обсуждали ранее, интерпретатор Python обрабатывает ссылку на объект, используемый в программе. Он автоматически освобождает память, когда счетчик ссылок становится равным нулю. Это классический подход для подсчета ссылок, если он не работает, когда программа ссылается на циклы. Цикл ссылок происходит, когда один или несколько объектов ссылаются друг на друга. Следовательно, счетчик ссылок никогда не становится нулевым.

Мы создали справочный цикл. Объект list1 ссылается на сам объект list1. Когда функция возвращает объект list1, память для объекта list1 не освобождается. Так что подсчет ссылок не подходит для решения ссылочного цикла. Но мы можем решить эту проблему, изменив сборщик мусора или производительность сборщика мусора.

Для этого мы будем использовать функцию gc.collect() для модуля gc.

Приведенный выше код даст количество собранных и освобожденных объектов.

Метод gc.collect() используется для выполнения сборки мусора по времени. Этот метод вызывается через фиксированный интервал времени для выполнения сборки мусора на основе времени.

При четной сборке мусора функция gc.collect() вызывается после возникновения события. Давайте разберемся в следующем примере.

В приведенном выше коде мы создали объект list1, на который ссылается переменная list. Первый элемент объекта списка ссылается на себя. Счетчик ссылок на объект списка всегда больше нуля, даже если он удален или выходит за рамки программы.

Управление памятью в Python

В этом разделе мы подробно обсудим архитектуру памяти C Python. Как мы обсуждали ранее, существует уровень абстракции от физического оборудования до Python. Различные приложения или Python обращаются к виртуальной памяти, созданной операционной системой.

Python использует часть памяти для внутреннего использования и необъектную память. Другая часть памяти используется для объекта Python, такого как int, dict, list и т. д.

CPython содержит распределитель объектов, который выделяет память в области объекта. Распределитель объектов получает вызов каждый раз, когда новому объекту требуется место. Распределитель предназначен для небольшого количества данных, потому что Python не задействует слишком много данных за раз. Он выделяет память, когда это абсолютно необходимо.

Стратегия выделения памяти CPython состоит из трех основных компонентов.

Распространенные способы уменьшить сложность пространства

Мы можем следовать некоторым передовым методам, чтобы уменьшить сложность пространства. Эти методы должны сэкономить достаточно места и сделать программу эффективной. Ниже приведены несколько практик в Python для распределителей памяти.

Мы определяем список на Python; распределитель памяти распределяет память кучи соответственно индексации списка. Предположим, нам нужен подсписок к данному списку, затем мы должны выполнить нарезку списка. Это простой способ получить подсписок из исходного списка. Он подходит для небольшого количества данных, но не подходит для емких данных.

Следовательно, нарезка списка генерирует копии объекта в списке, просто копирует ссылку на них. В результате распределитель памяти Python создает копию объекта и выделяет ее. Поэтому нам нужно избегать разрезания списка.

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

Разработчик должен попытаться использовать «для элемента в массиве» вместо «для индекса в диапазоне(len(array))» для экономии места и времени. Если программе не нужна индексация элемента списка, не используйте ее.

Конкатенация строк не подходит для экономии места и времени. По возможности, мы должны избегать использования знака «+» для конкатенации строк, потому что строки неизменяемы. Когда мы добавляем новую строку к существующей строке, Python создает новую строку и присваивает ее новому адресу.

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

Мы создали переменную a для ссылки на строковый объект, который является информацией о строковом значении.

Затем мы добавляем новую строку с помощью оператора «+». Python перераспределяет новую строку в памяти в зависимости от ее размера и длины. Предположим, что размер памяти исходной строки равен n байтам, тогда новая строка будет размером m байтов.

Вместо использования конкатенации строк мы можем использовать «.join(iterable_object)» или формат или%. Это оказывает огромное влияние на экономию памяти и времени.

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

В следующем примере мы реализуем итератор, который вызывает специальную функцию генератора. Ключевое слово yield возвращает текущее значение, переходя к следующему значению только на следующей итерации цикла.

  • По возможности используйте встроенную библиотеку

Если мы используем методы, которые уже были предопределены в библиотеке Python, импортируйте соответствующую библиотеку. Это сэкономит много места и времени. Мы также можем создать модуль, чтобы определить функцию и импортировать ее в текущую рабочую программу.

Заключение

Программирование на языках, которые позволяют взаимодействовать с памятью на более низком уровне, как например в C и C++, иногда доставляет немало проблем, с которыми вы раньше не сталкивались, например: segfaults (ошибки сегментации). Такие ошибки очень раздражают, и могут стать причиной множества проблем; часто они свидетельствуют о том, что вы используете память, которую не следует использовать.

Одна из самых распространённых проблем — это попытка получить доступ к памяти, которая уже освобождена. Эту память вы могли освободить сами ― функцией free, или её автоматически освободила программа (например, из стека).

Поняв некоторые аспекты, связанные с памятью, вы станете действительно лучше и умнее в программировании!

Как разделена память

Память разделена на несколько сегментов, два из наиболее важных (для этой статьи) — это стек и heap. Стек — это упорядоченная область внедрения, а heap полностью произвольная ― вы резервируете память там, где это возможно.

Стек память обладает набором средств и операций для собственной работы (это место, где сохраняется информация регистров процессора). Здесь хранится информация, касающаяся работы программы, например о вызванных функциях и созданных переменных и т.д. Этой памятью управляет программа, а не разработчик.

Сегмент heap часто используется для резервирования большого объёма памяти. Этот резерв существует столько, сколько потребуется разработчику. Другими словами, контроль памяти в сегменте heap предоставлен разработчику. Разрабатывая сложные программы, часто приходится резервировать большие части памяти. Именно для этого и предназначен сегмент heap. Мы называем это ― Динамическая память.

Каждый раз, когда вы используете malloc, чтобы разместить что-либо в памяти – вы обращаетесь к сегменту heap. Любой другой вызов, например int i;, относится к стек памяти. Очень важно это понимать, чтобы с лёгкостью находить ошибки, в том числе ошибки сегментации.

Понимание стека

Ваша программа постоянно резервирует стек память, вы об этом даже не задумываетесь. Каждая функция и локальная переменная, которую вы вызываете, попадает в стек. Большинство из того, что можно сделать со стеком ― вам следует избегать, например переполнение буфера или некорректный доступ к памяти и т.д.

Как это работает изнутри?

Стек имеет структуру данных LIFO (Last-In-First-Out) ― «последним пришёл –первым ушёл». Представьте аккуратную стопку книг. В этой стопке вы можете взять первой, ту книгу, которую положили последней. Такая структура позволяет программе управлять своими операциями и пространствами, двумя простыми командами: push и pop . Команда push добавляет значение сверху стека, а pop наоборот ― изымает значение.

Для отслеживания текущего места в памяти существует специальный регистр процессора ― Stack Pointer (указатель стека). Например, переменные или обратный адрес из функции при сохранении попадают в стек и stack pointer перемещается вверх. Завершение функции означает, что всё изымается из стека, начиная с текущего положения stack pointer, до сохранённого обратного адреса из функции. Всё просто!

Чтобы проверить, всё ли вы поняли, давайте используем следующий пример (попробуйте найти ошибку самостоятельно ☺️):

Если запустить программу, она выдаст ошибку сегментации. Почему так происходит? Кажется, что всё на своих местах! За исключением… стека.

Когда мы вызываем функцию createArray, стек сохраняет обратный адрес, создаёт arr в памяти стека и возвращает arr (массив — это просто указатель на область памяти с этой информацией). Но, так как мы не использовали malloc, arr остаётся в стек памяти. После того как мы возвращаем указатель, так как мы не контролируем операции стека, программа изымает информацию из стека и использует её для своих нужд. Когда мы пытаемся заполнить массив, после того как вернули его из функции, мы повреждаем память и получаем ошибку сегментации.

Понимание кучи (heap)

В отличии от стека, в куче сохраняется что-либо, независимо от функций и пространств, в течение необходимого времени. Для использования этой памяти, в языке C есть библиотека stdlib с функциями malloc и free.

Malloc (memory allocation) запрашивает у системы необходимый объем памяти и возвращает указатель на начальный адрес. Free сообщает системе, что запрошенная память больше не нужна и может быть использована для других задач. Выглядит действительно просто ― до тех пор, пока вы избегаете ошибок.

Поскольку система не может перезаписать то, что запросил разработчик, мы должны сами управлять этим процессом с помощью этих двух функций. Это создаёт опасность возникновения утечек памяти.

Утечка произойдёт, если память не освободить вовремя, до завершения выполнения программы или если указатели на расположение этой памяти были потеряны. Программа будет использовать больше памяти чем должна. Чтобы этого не случалось, мы должны освобождать выделенную в куче память, когда она нам не нужна.

На изображении видно, как в «bad» версии кода, мы не освобождаем память. Это приводит к растрачиванию 20 * 4 байт (размер int 64-бит) = 80 байт. Может показаться, что это незначительно, но, если это большая программа, речь может идти о гигабайтах!

Чтобы программа эффективно использовала память — важно контролировать память кучи. В тоже время, делать это нужно с осторожностью. Если память была освобождена, то попытка получения доступа к ней или её использование может привести к ошибке сегментации.

Бонус: Struct и куча

Одна из распространённых ошибок при использовании struct заключается в том, что мы просто освобождаем struct. Всё прекрасно до тех пор, пока мы не выделяем память для указателей, внутри struct. В этом случае нам нужно сначала очистить их, а потом struct.

Как я устраняю утечки памяти

Программируя на C, я часто использую struct, поэтому у меня всегда под рукой две необходимые функции: конструктор и деструктор. Это единственные функции, для которых я использую mallocs и frees в struct. Это упрощает решение проблем с утечкой памяти.

Динамическое выделение памяти для систем с ее дефицитом — это зло. Во первых, при выделение памяти в куче ее местоположение случайно, а значит если выделить память два раза для двух объектов, то они вряд ли будут лежать в соседней памяти. Поэтому работа с такими объектами будет медленнее.

Во вторых, при постоянном выделение-освобождение памяти идет ее фрагментация. Если в системе ограниченный объем памяти, то наступит момент, когда не удастся выделить новый кусок связной памяти. Причем у вас может быть свободно 50 мегабайт, а выделить не удастся даже 10.

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

2. Используйте хранилища данных

Храните объекты вместе в одном куске памяти. Особенно если у вас есть коллекция объектов, с которыми вы будете работать одновременно. Пробегаться по такой коллекции будет значительно эффективнее, если все объекты будут последовательно в памяти.

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

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

Обращение к L1 — 20 тактов.
Обращение к L2 — 100 тактов.
Обращение к общей памяти — 600 тактов.

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

Понятно, что вы не сможете выделять память под каждый объект в отдельности. Стоит иметь менеджер памяти, у которого можно запросить требуемый кусок памяти. В статье Placement new, или как создать объект в выделенной памяти я описывал как создать такой менеджер.

3. Храните одинаковые данные вместе

Допустим у вас есть некий класс. Он сложный, с большим количеством переменных. Есть так же иерархия объектов данного класса.


Что не так с этим кодом? Посмотрите на функцию. Там находится проверка на валидность данных, которая должна оптимизировать. Но эта проверка занимает 23-24 (указаны для процессора Cell, PS3) цикла процессора, в то время как вычисление выполнится за 12 тактов. Т.е. эта «оптимизация» совершенна не нужна.

Ну и конечно где нибудь будет коллекция таких объектов, например такая:


При проходе по такой коллекции для получения каждого нового элемента будет обращение к памяти. Более того, если нужно будет всего лишь найти объекты у которых m_life == false, то нужно будет грузить в память все целиком.

Решением является хранение однотипных данных в общих хранилищах. Создаем массив для Matrix4x4 и для BoundingSphere, где будут находиться данные от всех объектов. Каждый объект будет содержать указатель на свои данные. Наш объект изменится таким образом:


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

4. Работайте не с объектами а с коллекциями объектов

При работе с некоторыми коллекциями объектов нет нужды работать с объектами в отдельности. В этом случае лучше реорганизовать внутреннее устройство коллекции таким образом, чтобы вообще устранить сам объект. Допустим есть объект Ball и его коллекция Balls.

Это лучше реорганизовать так, чтобы вообще избавиться от класса Ball. Это наиболее эффективно с точки зрения использования памяти, когда однотипные данные находятся последовательно в памяти. Это значительно ускорит работу с такой коллекцией. Если иногда внешнему миру нужен экземпляр такой коллекции, то можно предусмотреть функцию GetBall.

5. Поблочное выделение памяти

Выделяйте память большими блоками, а не под каждый элемент в отдельности. Допустим вам нужно выделить память под массив таких структур и заполнить его данными (допустим из файла). Предполагается, что данные после загрузки не будут меняться:


Для начала реорганизуем структуру в соответствии с пунктом 3.


Неправильный вариант выделения памяти:


* Как видно, мы выделили память только один раз, сразу большим куском. Это уменьшает ее фрагментацию и улучшает эффективность по работе с данными.
* Проходить по массиву таких структур будет достаточно быстро (например чтобы найти элемент с нужным id), т.к. размер структуры маленький будет меньше кэшмисов.
* Проводить вычисления над данными будет так же эффективно, т.к. все данные в одном месте.

6. Учитывайте выравнивание

Учитывайте выравнивание типов. Располагайте наиболее большие типы вначале структуры. Группируйте типы с равным размером, располагая переменные подряд. Помните, что double выравнивается минимум по 8.

7. Знайте размер типов

Заведите себе подобные типы и всегда учитывайте, сколько занимают ваши переменные в памяти. Это поможет легче учитывать пункт 6.


Данные методы имеют отношение к технике, называемой Data oriented design. Можете почитать про это более подробно тут: Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf.

Не скажу за все ОС-и, но под Windows есть менеджер памяти. Когда по ходу кода встречается new, ну или что - то другое для алокации динамической памяти из кучи, менеджер памяти всё это дело конечно же отследит и при закрытие процесса он всю это память корректно освободит.

Вопрос, зачем для объектов, созданных посредствам new, следует вызывать delete в деструкторе, если за вас это сделает менеджер памяти ОС ?

__________________
Помощь в написании контрольных, курсовых и дипломных работ здесь

Зачем использовать delete в небольшой программе, если после закрытия память все равно освободится?
Можно ли использовать new без delete, если программка небольшая, а также если нет класса вообще.


Какую память освобождать в деструкторе?
Если я вызываю деконструктор класса, допустим чтобы освободить динамическую память, выделенную с.

Как правильно освобождать память в динамических структурах
Использую деструктор для освобождения памяти от указателя на начало списка. struct Node < int.


Как корректно освобождать память (удалять) объектов GDI+?
Ну собственно вопрос в шапке. Например есть Image *img; Как его правильно удалить, чтоб избежать.

Да-да-да. Ты и в самом деле никогда не слыхал о программах, постоянно динамически создающих тысячи и тысячи этих самых "объектов" и работающих в режиме 365х7х24 (365 дней в году, 7 дней в неделю, 24 часа в сутки)? Ты начал учить не с самого главного. Зачем по твоему return 0 нужен?
Неужели думаешь, что в 100% из множества случаев в течении долгих лет любая программа всегда завершится корректно? Ты и в самом деле никогда не слыхал о программах, постоянно динамически создающих тысячи

Слышал и делал. Я вероятно не так вопрос сформулировал.
Вопрос собственно заключался в том, есть ли резон удалять все динамический созданные объекты процесса перед его закрытием?

Неужели думаешь, что в 100% из множества случаев в течении долгих лет любая программа всегда завершится корректно? Вопрос собственно заключался в том, есть ли резон удалять все динамический созданные объекты процесса перед его закрытием?

Нннуууу. Строго говоря, да, когда процесс завершается, его адресное пространство освобождается. Но:
- во-первых, с моей точки зрения, хороший тон в программировании предписывает соблюдение принципа "нагадил - убери за собой!",
- во-вторых, объекты могут "держать" не только память, но и какие-нибудь ограниченные ресурсы,
- в-третьих, а кто может дать гарантию, что твой код никогда больше не будет дорабатываться или масштабироваться другим программистом? Если пишешь исключительно для себя, любимого, то можно. А в промышленном программировании сплошь и рядом бывает, что код дорабатывается и изменяется другими программистами, иногда через годы после написания его автором.

Ну и так далее. К тому же, в современном C++ использование RAII позволяет вообще забыть о "ручном" управлении new/delete. Я уж и не помню, когда мне приходилось лечить утечки памяти, - лет этак восемь-десять назад, подчищая код, доставшийся мне в наследство от других программистов.

К тому же, в современном C++ использование RAII позволяет вообще забыть о "ручном" управлении new/delete.

Не, ну что значит забыть или не забыть. Любая более менее серьёзная программа вдоль и поперёк усеяна указателями и лично они у меня повсюду участвуют в бизнес логике, даже не представляю, как можно без ручного управления памятью хоть что - то написать, так, разве что хелоу ролд на смартпоинтерах.
У меня везде указатели летают между потоками, идут проверки на нули (при удаления nullptr присваиваю) и тд, как без указателей вообще грамотно синхронизировать данные в потоках . (особенно если ты придерживаешься парадигмы non blocking или event driven программирования )

Читайте также: