Трехцветное инкрементальное обновление GC: нужно ли сканировать каждый стек дважды? - PullRequest
27 голосов
/ 02 марта 2010

Позвольте мне дать вам краткое введение в трехцветный сборщик мусора (на тот случай, если кто-то его прочитает, но никогда о нем не слышал); если вам все равно, пропустите его и перейдите к проблеме.

Как работает трехцветный ГХ

В трехцветном ГХ объект имеет один из трех возможных цветов; белый, серый и черный. Трехцветный GC можно описать следующим образом:

  1. Все объекты изначально белые.

  2. Все объекты достижимы, потому что глобальная переменная или переменная стека ссылаются на нее («корневые объекты»), окрашены в серый цвет.

  3. Мы берем любой серый объект, находим все ссылки на белые объекты и окрашиваем эти белые объекты в серый цвет. Затем мы окрашиваем сам объект в черный цвет.

  4. Мы продолжаем на шаге 3, пока у нас есть серые объекты.

  5. Если у нас больше нет серых объектов, все остальные объекты либо белые, либо черные.

  6. Доказано, что все черные объекты достижимы и должны оставаться в живых. Все белые объекты недоступны и могут быть удалены.

Пока что это не слишком сложно ... по крайней мере, если GC - это StW (Stop the World), то есть он приостановит все потоки во время сбора мусора. Если это одновременно, трехцветный GC имеет инвариант, который должен всегда выполняться:

Черный объект не должен ссылаться на белый объект!

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

Если потоки не приостановлены, потоки могут выполнить код, который нарушит этот инвариант. Есть несколько способов предотвратить это:

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

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

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

Существует альтернатива обеим методам, называемая SatB (Снимок в начале). Этот вариант работает немного по-другому, учитывая тот факт, что на самом деле нет необходимости постоянно поддерживать инвариант, поскольку не имеет значения, относится ли черный объект к белому, пока GC знает, что этот белый объект раньше и все еще доступен во время текущего цикла GC (либо потому, что все еще серые объекты ссылаются и на этот белый объект, либо потому, что ссылка на этот белый объект помещается в явный стек, который также учитывается GC при запуске из серых предметов). Коллекторы SatB чаще используются на практике, потому что они имеют некоторые преимущества, но ИМХО их сложнее реализовать.

Я имею в виду пошаговое обновление GC, в котором используется вариант 2: всякий раз, когда код пытается заставить черный объект указывать на белый объект, он немедленно окрашивает объект в серый цвет. Таким образом, этот объект не будет пропущен в цикле сбора.

Проблема

Так много о трехцветных ГХ. Но есть одна вещь, которую я не понимаю в трехцветных ГХ. Давайте предположим, что у нас есть объект A, на который ссылается стек и который сам ссылается на объект B.

stack -> A.ref -> B

Теперь GC запускает цикл, останавливает поток, сканирует стек и видит A как непосредственно доступный, окрашивая в серый цвет. По завершении сканирования всего стека он снова останавливает поток и начинает обработку на шаге (3). Прежде чем начать что-либо делать, он прерывается (может произойти), и поток запускается снова и выполняет следующий код:

localRef = A.ref; // localRef points to B
A.ref = NULL;     // Now only the stack points to B
sleep(10000);     // Sleep for the whole GC cycle

Поскольку инвариант не был нарушен, B был белым, но не был назначен черному объекту, цвет B не изменился, он все еще белый. A больше не ссылается на B, поэтому при обработке «серого» A B не изменит свой цвет, а A станет черным. В конце цикла B все еще белый и выглядит как мусор. Однако localRef ссылается на B, поэтому это не мусор.

Вопрос

Прав ли я, что трехцветный ГХ должен сканировать стек каждой нити дважды? Один раз в самом начале, чтобы идентифицировать корневые объекты (получая серый цвет) и еще раз перед удалением белых объектов, поскольку на них может ссылаться стек, даже если ни один другой объект больше не ссылается на них. Ни одно описание алгоритма, который я видел до сих пор, не упоминало ничего о сканировании стека дважды. Все они только сказали, что при одновременном использовании важно, чтобы инвариант применялся постоянно, в противном случае достижимые объекты пропускаются. Но, насколько я понимаю, этого недостаточно. Стек должен рассматриваться как один большой объект, и после сканирования «стек является черным», и каждое обновление стека должно приводить к окрашиванию серого в объект.

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

Ответы [ 2 ]

20 голосов
/ 02 марта 2010

Немного терминологии:

Позвольте мне дать несколько имен, чтобы объяснения были понятнее.

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

В трехцветном инкрементном или параллельном GC существует три типа переменных:

  • истинные корни , которые всегда доступны (регистры ЦП, глобальные переменные);
  • быстрые переменные , которые сканируются способом остановки мира;
  • медленные переменные , которые обрабатываются цветами. Медленные переменные - это поля в цветных объектах.

«Истинные корни» и «быстрые переменные» далее будут вместе называться корнями .

Потоки приложения называются мутаторами , потому что они изменяют содержимое переменных.

При инкрементном или одновременном GC паузы GC происходят регулярно. Мир остановлен (мутаторы приостановлены), а корни отсканированы. Это сканирование показывает ряд ссылок на цветные объекты. Цвета объекта корректируются соответствующим образом (такие белые объекты сделаны серыми).

Когда ГХ имеет значение с шагом , происходит некоторое сканирование объекта: некоторые серые объекты сканируются (и окрашиваются в черный цвет), и серые объекты упоминаются как белые. Эта деятельность («маркировка») сохраняется в течение некоторого времени, но не обязательно, пока есть серые объекты. В какой-то момент маркировка останавливается, и мир пробуждается. GC называется «инкрементным», потому что цикл GC выполняется с небольшими приращениями, чередующимися с мутаторной активностью.

В одновременном GC сканирование серых объектов происходит одновременно с мутаторной активностью. Мир тогда пробуждается, как только корни были отсканированы. При параллельном GC барьеры доступа являются сложным для реализации, поскольку они должны обрабатывать параллельный доступ из потока GC; но на концептуальном уровне это не очень отличается от инкрементального GC. Параллельный GC можно рассматривать как оптимизацию по сравнению с инкрементным GC, в котором используется наличие нескольких ядер ЦП (параллельный GC имеет небольшое преимущество по сравнению с инкрементным GC, когда имеется только одно ядро).

Корни не должны быть защищены барьером доступа, так как они сканируются с остановленным миром. Фаза GC mark заканчивается, когда одновременно выполняются следующие условия:

  • корни только что отсканированы;
  • все объекты черного или белого цвета, но не серые.

поэтому такая ситуация может возникнуть только во время паузы. В этот момент начинается фаза развертки, во время которой белые объекты высвобождаются. Развертка может выполняться постепенно или одновременно; объекты, созданные во время развертки, немедленно окрашиваются в черный цвет. Когда развертка завершена, может наступить новая фаза метки GC: все объекты (которые в этот момент все черные) перекрашиваются в белый цвет (это делается атомарно, просто изменяя способ интерпретации цветовых битов).

Переменная классификация:

С учетом сказанного, теперь я могу ответить на ваш вопрос. С приведенным выше описанием возникает вопрос: каковы корни? Это на самом деле до реализации; Есть несколько возможностей и компромиссов.

Истинные корни всегда должны быть отсканированы; Истинные корни - это содержимое регистра ЦП и глобальные переменные. Обратите внимание, что стеки не истинные корни; только текущий указатель фрейма стека.

Поскольку к быстрым переменным обращаются без барьеров, принято делать фреймы стека быстрыми переменными (то есть также корнями). Это связано с тем, что, хотя доступ для записи является редким для всей системы, он довольно часто встречается в локальных переменных. Было измерено (в некоторых программах на Лиспе), что около 99% записей (от значения указателя) имеют локальную переменную в качестве цели.

Быстрые переменные часто расширяются еще больше, в случае GC поколения: «молодое поколение» состоит в специальной области выделения для новых объектов, ограниченной по длине и сканируемой как быстрые переменные.Яркой стороной быстрых переменных является быстрый доступ (отсюда и название);Недостатком является то, что все эти быстрые переменные можно сканировать только во время паузы (мир остановлен).Существует компромисс между размером быстрых переменных, который часто приводит к ограничению размера молодого поколения.Подрастающее молодое поколение повышает среднюю производительность (сокращая количество барьеров доступа) за счет более длительных пауз.

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

  • Могут быть сделаны жесткие гарантии на время паузы.Это сложно, если кадры стека являются корнями, потому что каждый новый поток имеет свой собственный стек, поэтому общий размер корней может возрасти до произвольной величины при запуске новых потоков.Если корнями являются только регистры ЦП и глобальные переменные (не более нескольких десятков в типичных случаях, и их число известно во время компиляции), то паузы могут быть очень короткими.
  • Это позволяет динамически распределятьскладывать кадры в кучу.Это необходимо, если вы играете с сопрограммами и продолжениями, как примитив call/cc Схемы.В таком случае кадры больше не обрабатываются как чистый «стек».Для правильной обработки продолжений в языке, поддерживающем GC, требуется, чтобы функциональные кадры выделялись динамически.

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

Другой концептуальный обзор:

Другой способ просмотра обработки корня заключается в следующем: корни - это переменные, для которых правило триколора (без черно-белого указателя) не поддерживается постоянно;эти переменные могут быть изменены без ограничений.Но их нужно регулярно приводить в порядок, останавливая мир и сканируя их.

На практике мутаторы участвуют в гонках с GC.Мутаторы создают новые объекты и указывают на них;каждая пауза создает новые серые объекты.В параллельном или инкрементальном GC, если вы позволите мутаторам слишком долго играть с корнями, то каждая пауза может создавать большую партию новых серых объектов.В худшем случае ГХ не может сканировать объекты достаточно быстро, чтобы не отставать от скорости создания серых объектов.Это проблема, потому что белые объекты могут быть освобождены только во время фазы развертки, которая достигается только в том случае, если в какой-то момент ГХ может завершить свою маркировку.Обычная стратегия реализации для инкрементального GC состоит в том, чтобы сканировать серые объекты, во время каждой паузы, на общий размер, который пропорционален общему размеру корней.Таким образом, время паузы остается ограниченным общим размером корней, и если коэффициент пропорциональности хорошо сбалансирован, то можно гарантировать, что GC в конечном итоге прекратит помечать фазу и войдет в фазу подметания.

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

Библиография:

Сборка мусора: алгоритмы для автоматического динамического управления памятью: обязательная к прочтению книга по сбору мусора.

4 голосов
/ 21 марта 2011

Томас, очевидно, имеет лучший ответ. Тем не менее, я просто добавлю небольшую дополнительную заметку.

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

Поэтому, чтобы сохранить инвариант, присвоение корню должно автоматически сереть объект.

...