Выделение памяти
Обзор
В приложениях Swift на стороне сервера выделение памяти является фундаментальным для различных задач, таких как создание объектов, манипулирование структурами данных и управление ресурсами. Swift выделяет ресурсы памяти по мере необходимости и предоставляет встроенные механизмы управления памятью, такие как автоматическое подсчёт ссылок (ARC), для обработки выделения, освобождения и владения памятью.
Выделение памяти помогает оптимизировать использование памяти, выделяя точное количество памяти, необходимое для каждого объекта или структуры данных, уменьшая потери памяти и повышая производительность приложения. Однако выделение памяти в Swift может быть дополнено для обеспечения требований к выравниванию памяти для типов или структур данных, которые должны быть эффективно доступны аппаратным средствам, уменьшая риск проблем с невыровненным доступом к памяти и повышая производительность.
Кроме того, правильное управление выделением памяти предотвращает утечки памяти и гарантирует, что память освобождается, когда она больше не нужна. Это помогает поддерживать стабильность и надёжность приложений на сервере.
Куча и стек
В общем случае, Swift имеет два основных места для выделения памяти: Куча и Стек.
Swift автоматически выделяет память в структуре данных куча или стек.
Для высокопроизводительных программ на Swift, понимание источника выделения памяти в куче и уменьшение количества выделений, предоставляемых вашей программой, является важнейшим фактором. Выявление этих вопросов аналогично выявлению других вопросов производительности, таких как:
- Где ресурсы выделяются до оптимизации производительности?
- Какие типы ресурсов используются? ЦП? Память? Выделение памяти в куче?
Примечание: Хотя выделение памяти в куче может быть относительно дорогим с точки зрения вычислительной нагрузки, оно предоставляет гибкость и возможности динамического управления памятью, необходимые для задач, таких как работа с переменной длиной или динамическими структурами данных.
Профилирование
Вы можете использовать различные инструменты и техники для профилирования кода Swift, в зависимости от конкретных потребностей вашего проекта. Некоторые из часто используемых техник профилирования включают:
- Использование инструментов профилирования, предоставляемых поставщиком ОС, таких как Instruments на macOS или
perfна Linux. - Добавление ручных замеров времени, используя методы, такие как добавление временных меток перед и после критических разделов кода.
- Использование библиотек и фреймворков для профилирования производительности Swift, таких как SwiftMetrics или XCGLogger.
Для macOS вы можете использовать инструмент Выделение памяти в Xcode Instruments, чтобы проанализировать и оптимизировать использование памяти в ваших приложениях. Инструмент Выделение памяти отслеживает размер и количество всех выделений памяти в куче и анонимной виртуальной памяти и группирует их по категориям.
Если ваши рабочие нагрузки в производстве выполняются на Linux, а не macOS, количество выделений может значительно отличаться в зависимости от вашей конфигурации.
В данном документе основное внимание уделяется количеству выделений памяти в куче, а не их размеру.
Начало работы
Оптимизатор Swift создаёт более быстрый код и выделяет меньше памяти в режиме release. Профилируя код Swift в режиме release и оптимизируя результаты, вы можете добиться лучшей производительности и эффективности в своих приложениях.
Выполните следующие шаги:
Шаг 1. Скомпилируйте свой код в режиме release, выполнив следующую команду:
swift run -c release
Шаг 2. Установите perf, чтобы профилировать свой код для вашей среды, собирая данные, связанные с производительностью, и оптимизируя производительность ваших приложений Swift на сервере.
Шаг 3. Клонируйте проект FlameGraph, чтобы сгенерировать визуализацию графика пламени, которая поможет вам быстро определить узкие места в кодовой базе, визуализировать пути вызовов, понять поток выполнения и оптимизировать производительность. Для генерации графика пламени вам необходимо клонировать репозиторий FlameGraph на вашем компьютере или в контейнере, сделав его доступным по адресу ~/FlameGraph.
Выполните эту команду для клонирования репозитория https://github.com/brendangregg/FlameGraph в ~/FlameGraph:
git clone https://github.com/brendangregg/FlameGraph
При выполнении в Docker используйте эту команду для привязки к монтированию репозитория FlameGraph в контейнер:
docker run -it --rm \
--privileged \
-v "/path/to/FlameGraphOnYourMachine:/FlameGraph:ro" \
-v "$PWD:PWD" -w "$PWD" \
swift:latest
Визуально выделяя наиболее часто вызываемые функции или функции, потребляющие больше всего времени обработки, вы можете сконцентрировать усилия по оптимизации на улучшении производительности критических путей кода.
Инструменты
Вы можете выявить области для оптимизации и принять обоснованные решения для улучшения производительности и эффективности вашего кода Swift на сервере, используя инструмент Linux perf.
Инструмент perf — это инструмент профилирования и анализа производительности, доступный в системах Linux. Хотя он не специфичен для Swift, он может быть полезен для профилирования кода Swift на сервере по следующим причинам:
- Низкая нагрузка, что означает, что он может собирать данные о производительности с минимальным влиянием на выполнение вашего кода Swift.
- Богатый набор функций, таких как профилирование ЦП, профилирование памяти и выборка событий.
- Генерация графика пламени, чтобы вы могли понять относительное время, затрачиваемое в разных частях кода, и выявить узкие места в производительности.
- Профилирование на уровне системы собирает данные о производительности на уровне ядра, анализирует события на уровне всей системы и понимает влияние других процессов или компонентов системы на производительность вашего приложения Swift.
- Гибкость и расширяемость позволяют настраивать типы событий, которые вы хотите профилировать, устанавливать частоты выборки, задавать фильтры и многое другое.
Совет 1: Если вы запускаете
perfв контейнере Docker, вам потребуется привилегированный контейнер для предоставления необходимых разрешений и доступа к инструменту для сбора данных о производительности.
Совет 2: Предполагайте команды с префиксом
sudo, если вам нужен доступroot. См. Получениеperfв действие для получения дополнительной информации.
Установка зонда perf пользователя
Как уже упоминалось, примерные программы в этом документе сосредоточены на подсчёте количества выделений.
Большинство выделений используют функцию malloc программы Swift на Linux. Установка зондов perf пользователя на функцию выделения предоставляет информацию о том, когда вызывается функция выделения.
В этом случае зонд пользователя был установлен для всех функций выделения, поскольку Swift использует другие функции, такие как calloc и posix_memalign.
# figures out the path to libc
libc_path=$(readlink -e /lib64/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6)
# delete all existing user probes on libc (instead of * you can also list them individually)
perf probe --del 'probe_libc:*'
# installs a probe on `malloc`, `calloc`, and `posix_memalign`
perf probe -x "$libc_path" --add malloc --add calloc --add posix_memalign
Впоследствии событие в perf будет срабатывать всякий раз, когда вызывается одна из функций выделения.
Вывод должен выглядеть так:
Added new events:
probe_libc:malloc (on malloc in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
probe_libc:calloc (on calloc in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
probe_libc:posix_memalign (on posix_memalign in /usr/lib/x86_64-linux-gnu/libc-2.31.so)
[...]
Здесь вы можете увидеть, что perf вызывает новые события probe_libc:malloc; probe_libc:calloc каждый раз, когда вызывается соответствующая функция.
Чтобы подтвердить, что зонд пользователя probe_libc:malloc работает, выполните эту команду:
perf stat -e probe_libc:malloc -- bash -c 'echo Hello World'
Вывод должен быть похожим на этот:
Hello World
Performance counter stats for 'bash -c echo Hello World':
1021 probe_libc:malloc
0.003840500 seconds time elapsed
0.000000000 seconds user
0.003867000 seconds sys
В данном случае, похоже, что зонд пользователя вызывал функции выделения 1021 раз.
Важно: Если зонд вызывал функции выделения 0 раз, это указывает на ошибку.
Выполнение анализа выделения
Выполняя анализ выделения, вы можете лучше понять схемы использования памяти в вашем приложении и выявить и исправить проблемы с памятью, такие как утечки или неэффективное использование, в конечном счёте улучшая производительность и стабильность вашего кода.
Пример программы
После того, как вы подтвердили, что зонд пользователя на malloc работает, вы можете проанализировать выделения программы. Например, вы можете проанализировать программу, которая выполняет десять последовательных HTTP-запросов, используя AsyncHTTPClient.
Анализ программы с использованием AsyncHTTPClient может помочь оптимизировать её производительность, улучшить обработку ошибок, обеспечить правильную конкурентность и многопоточность, повысить удобочитаемость и поддерживаемость кода и оценить соображения по масштабируемости.
Вот пример исходного кода программы с приведенными зависимостями:
dependencies: [
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-nio.git", from: "2.29.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.4.2"),
],
Пример программы, использующей AsyncHTTPClient:
import AsyncHTTPClient
import NIO
import Logging
let urls = Array(repeating:"http://httpbin.org/get", count: 10)
var logger = Logger(label: "ahc-alloc-demo")
logger.info("running HTTP requests", metadata: ["count": "\(urls.count)"])
MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { eventLoop in
let httpClient = HTTPClient(eventLoopGroupProvider: .shared(eventLoop),
backgroundActivityLogger: logger)
func doRemainingRequests(_ remaining: ArraySlice<String>,
overallResult: EventLoopPromise<Void>,
eventLoop: EventLoop) {
var remaining = remaining
if let first = remaining.popFirst() {
httpClient.get(url: first, logger: logger).map { [remaining] _ in
eventLoop.execute { // for shorter stacks
doRemainingRequests(remaining, overallResult: overallResult, eventLoop: eventLoop)
}
}.whenFailure { error in
overallResult.fail(error)
}
} else {
return overallResult.succeed(())
}
}
let promise = eventLoop.makePromise(of: Void.self)
// Kick off the process
doRemainingRequests(urls[...],
overallResult: promise,
eventLoop: eventLoop)
promise.futureResult.whenComplete { result in
switch result {
case .success:
logger.info("all HTTP requests succeeded")
case .failure(let error):
logger.error("HTTP request failure", metadata: ["error": "\(error)"])
}
httpClient.shutdown { maybeError in
if let error = maybeError {
logger.error("AHC shutdown failed", metadata: ["error": "\(error)"])
}
eventLoop.shutdownGracefully { maybeError in
if let error = maybeError {
logger.error("EventLoop shutdown failed", metadata: ["error": "\(error)"])
}
}
}
}
}
logger.info("exiting")
Если вы работаете с программой как с пакетом Swift, сначала скомпилируйте её в режиме release, используя следующую команду:
swift build -c release
Бинарный файл под названием .build/release/your-program-name должен отображаться и может быть проанализирован для получения количества выделений памяти.
Подсчёт выделений памяти
Подсчёт выделений и визуализация их в виде графика помогают анализировать использование памяти, профилировать потребление памяти, оптимизировать производительность, рефакторить и оптимизировать код, а также отлаживать проблемы, связанные с памятью в вашей программе.
Прежде чем визуализировать выделения в виде графика «пламени», начните с анализа, используя бинарный файл для получения количества выделений, выполнив команду:
perf stat -e 'probe_libc:*' -- .build/release/your-program-name
Эта команда инструктирует perf запустить вашу программу и подсчитать количество срабатываний пользовательского зонда probe_libc:malloc или выделения памяти в рамках вашего приложения.
Вывод должен выглядеть примерно так:
Performance counter stats for '.build/release/your-program-name':
68 probe_libc:posix_memalign
35 probe_libc:calloc_1
0 probe_libc:calloc
2977 probe_libc:malloc
[...]
В данном случае программа выполнила выделение 2977 раз через malloc и небольшое количество раз через другие функции выделения.
Важно отметить, что команда -e probe_libc:* используется вместо индивидуального перечисления каждого события, например:
-e probe_libc: mallocprobe_libc:callocprobe_libc:calloc_1probe_libc:posix_memalign
Подсказка: Этот подход предполагает, что у вас нет других
perfустановленных пользовательских зондов. Если установлены другиеperfпользовательские зонды, вам нужно указать каждое событие индивидуально.
Сбор исходных данных
Сбор исходных данных имеет решающее значение для получения точного представления поведения системы, проведения подробного анализа производительности и отладки, анализа тенденций, обеспечения гибкости профилирования и руководства оптимизацией производительности.
Команда perf не позволяет создавать живые графики во время выполнения программы. Однако инструмент Linux Perf предоставляет утилиту perf record, которая собирает события производительности для последующего анализа. Собранные данные затем могут быть преобразованы в график.
В общем случае, команда perf record может использоваться для запуска программы, а libc_probe:malloc — для сбора информации, как показано ниже:
perf record --call-graph dwarf,16384 \
-m 50000 \
-e 'probe_libc:*' -- \
.build/release/your-program-name
Разобрав эту команду, получаем следующее:
- Команда
perf recordинструктируетperfзаписывать данные. - Команда
--call-graph dwarf,16384инструктируетperfиспользовать информацию DWARF для создания графов вызовов. Она также устанавливает максимальный размер дампов стека в 16 КБ, чего должно хватить для полных стековых следов.- Хотя использование DWARF медленное (см. ниже), оно создает лучшие графы вызовов.
-
-m 50000указывает размер буфера кольца, который используетperfи выводит его кратнымиPAGE_SIZE(обычно 4 КБ).- Значительный буфер необходим при использовании DWARF, чтобы предотвратить потерю данных.
-
-e 'probe_libc:*'записывает данные, когда срабатываютmalloc;calloc; и другиеmalloc/calloc/...пользовательские зонды.- Событие «срабатывание» происходит, когда зонд активируется или выполняется, собирая релевантную информацию об выделении для дальнейшего анализа и отладки.
Вывод вашей программы должен выглядеть примерно так:
<your program's output>
[ perf record: Woken up 2 times to write data ]
[ perf record: Captured and wrote 401.088 MB perf.data (49640 samples) ]
Размещая пользовательские зонды в стратегических точках вашего кода, вы можете отслеживать и регистрировать события выделения, чтобы получить представление о шаблонах выделения памяти, выявить потенциальные проблемы с производительностью или утечки памяти, а также проанализировать использование памяти в вашем приложении.
Важно: если вывод
perfвозвращаетlost chunksи выполняетcheck the IO/CPU overload!запрос, см. Преодоление потери фрагментов данных ниже.
Создание графов «пламени»
После успешного сбора данных с помощью perf record, вы можете выполнить следующую команду для создания файла SVG с графиком «пламени»:
perf script | \
/FlameGraph/stackcollapse-perf.pl - | \
swift demangle --simplified | \
/FlameGraph/flamegraph.pl --countname allocations \
--width 1600 > out.svg
Вот подробный анализ этой команды:
- Команда
perfпреобразует бинарную информацию в текстовый вид, которыйperf recordзафиксировала. - Команда
stackcollapse-perfпреобразует стеки, сгенерированныеperf script, в правильный формат для графов «пламени». - Команда
swift demangle --simplifiedпреобразует имена символов в удобочитаемый формат. - Последние две команды создают график «пламени» на основе количества выделений.
После завершения команды создается файл SVG, который можно открыть в браузере.
Примечание: Длительное время выполнения может потребоваться в зависимости от размера данных, сложности алгоритма, ограничений ресурсов, таких как мощность процессора или объем памяти, плохо оптимизированного или неэффективного кода, внешних сервисов, API или задержек сети, вызывающих замедление.
Чтение графов «пламени»
Этот график «пламени» — непосредственный результат примера программы в этом разделе. Наведите курсор на фреймы стека для получения дополнительной информации или щелкните по любому фрейму стека для увеличения отображения поддерева.
-
При интерпретации графов «пламени» ось X означает количество, а не время. Расположение стека (слева или справа) не определяется временем существования стека, в отличие от диаграмм «пламени».
- Этот график «пламени» не является графиком CPU, а графиком выделения, где одна выборка соответствует одному выделению, а не времени, затраченному на процессор.
-
Широкие фреймы стека не (обязательно) выделяют напрямую, что означает, что функция или что-то, что функция вызвала, выполнила выделение многократно.
- Например,
BaseSocketChannel.readable— это широкий фрейм, но его функция не выделяет напрямую. Вместо этого она вызвала другие функции, такие как другие части SwiftNIO и AsyncHTTPClient, которые выполняли значительное выделение.
- Например,
Графики «пламени» выделения на macOS
Хотя большая часть этого руководства посвящена инструменту perf, вы можете создавать такие же графики на macOS.
Шаг 1. Для начала соберите исходные данные, используя фреймворк DTrace, выполнив эту команду:
sudo dtrace -n 'pid$target::malloc:entry,pid$target::posix_memalign:entry,pid$target::calloc:entry,pid$target::malloc_zone_malloc:entry,pid$target::malloc_zone_calloc:entry,pid$target::malloc_zone_memalign:entry { @s[ustack(100)] = count(); } ::END { printa(@s); }' -c .build/release/your-program > raw.stacks
Как и пользовательские зонды perf Linux, DTrace также использует зонды. Предыдущая команда инструктирует dtrace агрегировать количество вызовов эквивалентов функций выделения:
mallocposix_memaligncallocmalloc_zone_*
Примечание: на платформах Apple Swift использует несколько больше функций выделения, чем Linux.
Шаг 2. После сбора данных выполните эту команду для создания файла SVG:
cat raw.stacks |\
/FlameGraph/stackcollapse.pl - | \
swift demangle --simplified | \
/FlameGraph/flamegraph.pl --countname allocations \
--width 1600 > out.svg
Вы заметите, что эта команда похожа на вызов perf, за исключением:
- Команда
cat raw.stacksзаменяет командуperf script, так какdtraceуже включает в себя текстовый файл данных. - Команда
stackcollapse.pl, которая анализирует выходные данные агрегацииdtrace, заменяет командуstackcollapse-perf.pl, которая анализирует выводperf script.
Другие приёмы профилирования
Шаблоны выделения памяти в Swift
Оптимизация выделений памяти и повышение эффективности кода на основе информации, предоставляемой графиком «пламени», может помочь сделать ваш код Swift более производительным и визуально привлекательным. Форма выделений в Swift может меняться в зависимости от типа выделяемой памяти и способа её использования.
Некоторые распространённые формы выделения в Swift:
- Выделение одиночных объектов
- Выделение коллекций
- Строки
- Стеки вызовов функций
- Сущности протоколов
- Структуры и классы
Например, экземпляр класса (который выделяет память) вызывает swift_allocObject, который вызывает swift_slowAlloc, который вызывает malloc, содержащий пользовательский зонд.
“Оформление” шаблонов выделения
Чтобы ваш график пламени выглядел хорошо (после разбора свёрнутых стеков) вставьте следующий код в Linux perf script код (выше) следующим образом:
- Удалите
specializedи замените его наswift_allocObject. - Вызовите
swift_slowAlloc, который вызываетmalloc. - Используйте
Aдля выделения памяти.
Эти изменения должны выглядеть так:
sed -e 's/specialized //g' \
-e 's/;swift_allocObject;swift_slowAlloc;__libc_malloc/;A/g'
Чтобы создать визуально привлекательный график пламени в формате SVG при анализе распределения памяти в Swift, используйте полную команду:
perf script | \
/FlameGraph/stackcollapse-perf.pl - | \
swift demangle --simplified | \
sed -e 's/specialized //g' \
-e 's/;swift_allocObject;swift_slowAlloc;__libc_malloc/;A/g' | \
/FlameGraph/flamegraph.pl --countname allocations --flamechart --hash \
> out.svg
Преодоление потерь фрагментов данных
При использовании perf с разбором стека вызовов DWARF, вы можете столкнуться с этой проблемой:
[ perf record: Woken up 189 times to write data ]
Warning:
Processed 4346 events and lost 144 chunks!
Check IO/CPU overload!
[ perf record: Captured and wrote 30.868 MB perf.data (3817 samples) ]
Если perf указывает, что потеряно несколько фрагментов, это означает потерю данных. Когда perf теряет данные, вы можете использовать следующие варианты для решения проблемы:
- Уменьшите объём работы вашей программы.
- Для каждого выделения памяти
perfзаписывает трассировку стека.
- Для каждого выделения памяти
- Уменьшите максимальный сброс стека, который
perfзаписывает, изменив параметр--call-graph dwarf. Например, измените на:--call-graph dwarf,2048- По умолчанию записывается максимум 4096 байт, отображая глубокие стеки. Если вам не нужен объёмный вывод, вы можете уменьшить это число. Однако, график пламени может отображать
[unknown]кадра стека, что означает, что существуют пропущенные кадры стека (в байтах).
- По умолчанию записывается максимум 4096 байт, отображая глубокие стеки. Если вам не нужен объёмный вывод, вы можете уменьшить это число. Однако, график пламени может отображать
- Увеличьте значение параметра
-m, который представляет размер кольцевого буфера, которыйperfиспользует в памяти и отображает кратноPAGE_SIZE(обычно 4 КБ). - Замените команду
--call-tree dwarfна--call-tree fp, чтобы сгенерировать отчёт о дереве вызовов, который предоставляет иерархический вид вызовов функций в программе, показывая, как вызываются функции и взаимосвязи между различными функциями.
В целом, эти методы помогают понять поведение вашей программы, определить узкие места и улучшить производительность ваших приложений Swift.
The Swift Programming Language, Copyright © 2014-2025 Apple Inc.
Swift and the Swift logo are trademarks of Apple Inc.
Documentation for Swift 6.0.3
https://www.swift.org/documentation/server/guides/allocations.html