Большое использование памяти замедляет несвязанный код

Я поддерживаю код для проекта Go, который читает и записывает много данных и успешно работает в течение некоторого времени. Недавно я сделал изменение: файл CSV с примерно 2 миллионами записей загружается в карту со значениями структуры в начале программы. Эта карта используется только в части B, но выполняется первая часть A. И эта первая часть уже работает заметно медленнее, чем раньше (время обработки увеличено в четыре раза). Это очень странно, поскольку эта часть логики не изменилась. Я потратил неделю, пытаясь объяснить, как это может произойти. Вот шаги, которые я предпринял (когда я говорю о производительности, я всегда имею в виду часть А, которая не включает время загрузки данных в память и на самом деле не имеет к этому никакого отношения):

  • Программа работала на сервере внутри контейнера Docker. Но я смог воспроизвести его на своем ноутбуке без контейнера: производительность действительно снижается по сравнению с тем, когда я запускаю его без данных из файла, загруженного в память.
  • На сервере было огромное количество оперативной памяти. Хотя очевидно, что при загрузке файла используется больше памяти, ограничений нет. Я также не видел всплесков или других странных паттернов в использовании памяти и дисковом вводе-выводе. Для этих проверок я использовал pprof, htop и iotop.
  • Когда данные загружаются, но затем карта устанавливается на nil, производительность снова в порядке.
  • Загрузка данных в слайс вместо карты снижает падение производительности с х4 до х2 (но использование памяти примерно такое же, как и с картой).
  • Это заставило меня задаться вопросом, есть ли доступ к карте/срезу где-то в части A, хотя это не должно происходить. Карта хранится в поле структурного типа. Я проверил, и эта структура всегда передается по указателю (включая все горутины). Создание глобальной переменной вместо поля указателя не решило проблему.
  • Существует одна зависимость вне стандартной библиотеки. Проблема вызвана библиотекой? Это заставляет собирать мусор. Отключение этого ничего не меняет. Я нашел другую подобную библиотеку, которая не имеет отношения к делу, и использование этой в качестве замены повышает производительность, но все равно занимает больше времени при загрузке данных файла.

Здесь я построил метрики с данными в памяти и без них: Большое использование памяти замедляет несвязанный код

Что может вызвать этот эффект или как его узнать?

Не могли бы вы добавить метки к осям X и Y на графике?

Ankit Deshpande 27.05.2019 10:19

Два комментария. Во-первых, в чем конкретно заключается ваш вопрос? Хотите знать, как отследить утечку памяти? Во-вторых, вы не показали минимальный пример или код. Так что трудно догадаться, что происходит

Vorsprung 27.05.2019 10:20

"The server had a huge amount of RAM. Although obviously more memory is used when the file is loaded, no limits are hit". Как насчет ограничения размера кеша процессора? Например. возможно, загрузка данных приводит к тому, что все остальные данные выталкиваются из кеша (и замедляет работу кода, использующего другие данные, из-за промахов кеша).

Brendan 27.05.2019 13:58

Обновил вопрос с метками на осях и явным вопросом.

Socci 28.05.2019 09:02

@ Брендан Интересно, об этом я не подумал. Но я не верю, что это то, с чем я сталкиваюсь. Та часть кода, которая работает медленнее, состоит из чтения и записи файла без каких-либо предварительных данных.

Socci 28.05.2019 09:07
За пределами сигналов Angular: Сигналы и пользовательские стратегии рендеринга
За пределами сигналов Angular: Сигналы и пользовательские стратегии рендеринга
TL;DR: Angular Signals может облегчить отслеживание всех выражений в представлении (Component или EmbeddedView) и планирование пользовательских...
Sniper-CSS, избегайте неиспользуемых стилей
Sniper-CSS, избегайте неиспользуемых стилей
Это краткое руководство, в котором я хочу поделиться тем, как я перешел от 212 кБ CSS к 32,1 кБ (сокращение кода на 84,91%), по-прежнему используя...
0
5
79
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Итак, если я правильно понял, ваш поток выглядит примерно так:

  1. Прочитать 2 миллиона строк из CSV в карту -> структуру
  2. Запустите часть A (для которой не нужны данные из CSV)
  3. Запустите часть B, используя данные из CSV

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

Вероятнее всего, сборщик мусора регулярно обращается к 2 миллионам структур на карте. В зависимости от того, какое значение имеет GOGC, компонент стимуляции сборщика мусора, вероятно, будет срабатывать чаще по мере увеличения объема выделенной памяти. Поскольку эта карта отложена для последующего использования, сборщику мусора нечего делать, но в любом случае проверка данных занимает циклы. Есть ряд вещей, которые вы могли бы сделать, чтобы проверить и учесть это поведение - все эти вещи должны помочь вам исключить/подтвердить, замедляет ли вас сборка мусора.

  • Профилируйте код (очевидно, важно для диагностики) IIRC, профиль ЦП легче показывает вмешательства GC
  • Попробуйте отключить сборку мусора (debug.SetGCPercent(-1))
  • Сохраните карту в sync.Pool. Это тип, разработанный для того, чтобы вы могли хранить вещи, которыми вы будете управлять вручную, и выходить за рамки обычных циклов GC.
  • Читайте CSV только при необходимости, не читайте до "часть А"
  • Поток файла, вместо того, чтобы читать его в массивной карте. 2 миллиона строк, какой смысл читать все это в памяти, а не читать построчно?

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

Socci 28.05.2019 09:56

Я прочитал 2 миллиона записей в памяти, потому что они не в том порядке, в котором они мне нужны. До сих пор я обходил проблему, читая записи, сортируя их и записывая снова (используя gob). Затем их можно удалить из памяти и передать в потоковом режиме.

Socci 28.05.2019 09:58

Отключение сборки мусора действительно решило снижение производительности. Будет ли sync.Pool предпочтительным решением для производственной среды? У меня много данных, которые нужно хранить в памяти на протяжении всего прогона.

Socci 28.05.2019 10:37

@Socci: у меня недостаточно информации, чтобы сказать вам, является ли sync.Pool лучшим решением здесь. Получение чего-либо из пула требует утверждений типа во время выполнения, поэтому вы можете в конечном итоге обернуть данные в тип и утвердить его для интерфейса, поэтому вам нужно сделать это только один раз, так что да, это может быть решением. Я бы тоже попробовал настроить значение GOGC и сравнить

Elias Van Ootegem 28.05.2019 11:54

Другие вопросы по теме