Разработка
Корутины против потоков: тест потребления памяти
Мой главный вывод из всего этого заключается в том, что даже если корутины и являются «легковесными потоками», это практически не имеет значения для Android-разработчиков.
Корутины Kotlin часто называют облегченными потоками. Причина в том, что теоретически корутина может потреблять меньше памяти, чем поток. К сожалению, официальных цифр в этом контексте я не встречал, поэтому решил создать бенчмарк для количественной оценки разницы в потреблении памяти потоками и корутинами в приложениях для Android. В этой заметке я расскажу об основных идеях и проблемах, лежащих в основе этого бенчмарка, а также поделюсь его результатами.
Корутины Kotlin в задачах, связанных с процессором и IO
Сразу же следует признать, что идея о том, что корутины всегда потребляют меньше памяти, чем потоки, в целом, вероятно, неверна. При работе с задачами, связанными с процессором, максимальная пропускная способность будет определяться количеством потоков, которые процессор может выполнять параллельно (в отличие от одновременного выполнения). Поэтому если использовать CoroutineDispatchers.Default
для работы с привязкой к процессору, то он будет занимать столько же памяти, сколько и эквивалентно настроенный пул потоков. На практике, я даже подозреваю, что фреймворк корутинов будет иметь несколько большие накладные расходы, чем пул потоков.
Для работы с IO ситуация иная. Если использовать потоки, то для каждой задачи такого типа потребуется выделенный поток, который может быть заблокирован на длительное время. В отличие от этого, фреймворк корутинов предлагает функцию приостановки, которая не позволяет неактивной корутине блокировать поток. Поэтому, например, если у вас есть 10 параллельных корутинов, которые 90% времени проводят в приостановленном состоянии (например, ожидают данных из сети), то теоретически все эти 10 задач могут выполняться на одном базовом потоке. Если бы в том же сценарии использовались непосредственно потоки, то для получения тех же результатов потребовалось бы инстанцировать и запустить для них 10 процессорных потоков.
Мой бенчмарк моделирует задачи, связанные с чистым IO, и измеряет накладные расходы «голых» (bare) потоков, короутинов и потоков в пуле потоков в этих условиях. Поскольку я не думаю, что есть основания полагать, что корутины будут занимать больше памяти, чем потоки в задачах с привязкой к процессору, я не стал проводить бенчмарк этого сценария. Если вы хотите провести бенчмаркинг работы с привязкой к процессору, вы можете модифицировать исходный код моего бенчмарка для выполнения такого типа задач. Пожалуйста, поделитесь своими результатами.
Проблемы бенчмаркинга памяти
В предыдущей статье я сравнивал задержки запуска потоков и корутинов. Написание этого бенчмарка было отнюдь не тривиальной задачей, но бенчмаркинг потребления памяти оказался гораздо более сложным.
Первопричиной большинства проблем является автоматическая сборка мусора. Видите ли, при проведении бенчмарков потребления памяти необходимо иметь контроль над памятью. Например, при запуске нового потока вы хотите быть уверены, что любое изменение объема потребляемой памяти связано именно с этим потоком. К сожалению, автоматический сборщик мусора находится вне контроля приложения и может в любой момент изменить содержимое памяти приложения. Так, можно запустить новый поток и обнаружить, что объем памяти, потребляемой приложением, на самом деле уменьшился.
Я потратил много времени, пытаясь обойти эту проблему. Например, я пытался использовать вызов System.gc()
, но, поскольку его действие не является детерминированным, это не очень помогло.
В итоге я выбрал очень сложный обходной путь в своем открытом приложении TechYourChance, который, похоже, справляется со своей задачей. Сначала я сохраняю результаты каждой итерации бенчмарка в базе данных. Затем я перезапускаю процесс приложения и возвращаюсь к тому же экрану. Затем запускаю следующую итерацию теста. После завершения всех итераций я извлекаю все результаты из базы данных и показываю их в пользовательском интерфейсе.
Предполагается, что перезапуск приложения на определенном экране без выполнения каких-либо других действий приводит приложение в то же состояние с точки зрения потребления памяти. Нет никаких веских оснований, почему это должно быть именно так, но на практике это предположение, похоже, оправдывается.
Сравнение потребления памяти корутинами и потоками
Я проводил тестирование на двух устройствах:
- Samsung Galaxy S7 — самое старое из имеющихся у меня устройств, способных работать с приложением TechYourChance.
- Samsung Galaxy S20 FE, который является моим текущим повседневным устройством.
Вот результаты:
Здесь есть несколько интересных наблюдений.
Во-первых, обратите внимание на то, как совпадает потребление памяти «голыми» потоками и пулом потоков. Это именно то, что я ожидал увидеть, поэтому есть уверенность, что данный бенчмарк относительно точен.
Мы видим четкую линейную зависимость общего потребления памяти от количества запущенных задач. График и соответствующий ряд данных дает нам объем памяти, потребляемый каждым дополнительным потоком или корутиной, который отображается под графиком.
Интересно отметить, что на разных устройствах потоки и корутины потребляют совершенно разное количество памяти (возможно, это связано и с разными версиями ОС Android). Однако, несмотря на это, соотношение потребляемой памяти между потоком и корутиной кажется относительно постоянным — 6:1. Вот оно, то самое магическое число, которое я так долго искал!
И, наконец, любопытно, что для всех трех механизмов влияние на память первых нескольких задач сильно отличается от остальных. На данный момент у меня нет хорошей гипотезы для объяснения этого факта. Если у вас есть какие-то идеи в этом контексте, пожалуйста, сообщите мне об этом в комментариях.
Заключение
Мой бенчмарк показывает, что каждый «живой» поток увеличивает потребление памяти приложением на десятки Кб, а соотношение между памятью, потребляемой потоком и корутиной, составляет 6:1. Я очень доволен этими результатами!
Обратите внимание, что даже при наличии 100 тыс. задач, связанных с IO, что не нужно практически ни одному Android-приложению, общая разница в потреблении памяти потоками и корутинами составит несколько мегабайт. Это не повод для беспокойства. Учитывая, что большинству Android-приложений требуется не более 10 одновременных задач, связанных с IO, реальная разница в большинстве случаев будет гораздо меньше.
Таким образом, мой главный вывод из всего этого заключается в том, что даже если корутины и являются «легковесными потоками», это практически не имеет значения для Android-разработчиков. Если вы не имеете дело с каким-то нетипичным уровнем параллелизма, включающим сотни задач, забудьте об этом. Сосредоточьтесь на создании отличных продуктов и написании чистого и удобного кода. Оставьте оптимизацию фоновой работы разработчикам бэкенда, которые имеют дело с сотнями или тысячами одновременных запросов ввода-вывода.
-
Интегрированные среды разработки2 недели назад
Лучшая работа с Android Studio: 5 советов
-
Новости4 недели назад
Видео и подкасты о мобильной разработке 2024.43
-
Новости3 недели назад
Видео и подкасты о мобильной разработке 2024.44
-
Исследования2 недели назад
Поможет ли новая архитектура React Native отобрать лидерство у Flutter в кроссплатформенной разработке?