Умный сборщик мусора для разделения поддиапазонов массивов? - PullRequest
12 голосов
/ 28 июля 2011

В этот популярный вопрос о том, почему подстрока принимает O (n) в C # , один из основных предоставленных ответов утверждал, что, если большой массив был выделен, а подстроки вычисляются с помощью новых строк, просто ссылаются на небольшую часть массива, сборщик мусора не сможет восстановить массив символов, содержащий большую строку, даже если на исходную строку больше не ссылались.

Это выглядит как вполне корректный ответ, но, похоже, теоретически можно построить сборщик мусора для массивов, который позволил бы собирать мусор для большей части массива, оставляя после себя небольшой подмассив, который все еще используется. Другими словами, если бы существовал массив из 50 000 элементов, из которых все еще использовался только небольшой срез из 100 элементов, сборщик мусора мог бы разбить массив на три части - элементы перед срезом из 100 элементов, 100-элементный Сам ломтик, а элементы после ломтика из 100 элементов - и затем мусор собирать первый и последний из этих кусочков.

Мой вопрос заключается в том, используют ли какие-либо языковые реализации такой сборщик мусора или он существует только в теории. Кто-нибудь знает пример языковой реализации, в которой есть сборщик мусора?

Ответы [ 5 ]

6 голосов
/ 12 августа 2011

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

Как работает GC?

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

Примечания. Я использовал информацию о типе и макете в одном значении.

Пример:

Imagine we have some object layouts:
====================================
A: { int, object, double, object }
B: { object, object }
C: { int }


Memory Data:
============
Now we need to describe what is in memory.
The following list is composed of memory positions,
and the data contained in each position.
There are 2 kinds of notation I will use:
  - Address: ROOT[ list-of-root-references ]
  - Address: type-identifier { object-data }
     Note that each object can span multiple memory
     positions from the first one.
     e.g. 90: B { 0, 0 } -- this reads as
       "stored at address 90: type-identifier of B followed by raw data { 0, 0 }"
  - A reference is represented by a number,
    that point to the memory address of the pointed object.

 1: ROOT[ 90, 20, 30 ]
20: A { 1236, 30, 0.5, 60 }
30: B { 60, 70 }
40: C { 1237 }
50: A { 1238, 20, 0.8, 50 }    There is a self-reference here!
60: C { 1234 }
70: A { 1234, 0, 0.7, 80 }
80: C { 1235 }
90: B { 0, 0 }                 Note that 0 is a null reference!

   The GC need to know the layout of each object.
   Otherwise it wouldn't be abled to tell
   what knid of information is stored in each memory position.

Running the GC:
===============
Garbage collecting steps, to clean the memory described above:
Step 1: Get ROOT references: 2, 3, 9 and mark as 'in-use'
Step 2: Get references from 2, 3 and 9: 3, 6, 7. Note that 0 is a null reference!
Step 3: Get references from 3, 6 and 7: 6, 7, 8, and mark them.
Step 4: Get references from 6, 7, 8, and mark them: only 8!
Step 5: Get references from 8... it has none! We are finished with marking objects.
Step 6: Delete unmarked objects.


      This shows what happened in each step with each object stored in the memory.

Step ->  1  2  3  4  5
Memory                    
20       x           
30       x  x        
40                   DELETE
50                   DELETE
60          x  x     
70          x  x     
80             x  x  
90       x           

То, что я описал, - это очень простой алгоритм GC.

Взгляните на трехцветную маркировку ... это действительно круто! Вот как сделаны настоящие современные GC.

Сборка мусора (информатика) - описывает некоторые современные методологии GC.

Но ... где хранится информация о макете?

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

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

Пример:

To represent the memory data I will introduce the following notation:
 - Address: { object-data } -- this time object type is not at the begining!
 - A reference is represented by a type-identifier and an address-number,
   that point to the memory address of the pointed object,
   in the following format: type/number...
   e.g. A/20 -- this reads as: "at address 20, there is an object of type A"
Note that: 0/0 is a null reference,
      but it still requires space to store the type.

The memory would look like this:
 1: ROOT[ B/90, A/20, B/30 ]
20: { 1236, B/30, 0.5, C/60 }
30: { C/60, A/70 }
40: { 1237 }
50: { 1238, A/20, 0.8, A/50 }
60: { 1234 }
70: { 1234, 0/0, 0.7, C/80 }
80: { 1235 }
90: { 0/0, 0/0 }

Если мы храним информацию о расположении рядом с выделенным блоком памяти, то это хорошо! Это быстро и позволяет избежать повторения информации о макете.

Пример:

The memory looks like the first sample:
 *This is the same notation used at the begining of this answer.
 1: ROOT[ 90, 20, 30 ]
20: A { 1236, 30, 0.5, 60 }
30: B { 60, 70 }
40: C { 1237 }
50: A { 1238, 20, 0.8, 50 }
60: C { 1234 }
70: A { 1234, 0, 0.7, 80 }
80: C { 1235 }
90: B { 0, 0 }

Пока все хорошо ... но теперь я хочу общую память.

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

Представьте массив с разделяемой памятью:

* тысяча сорок-девять * Пример: * ** 1052 тысяча пятьдесят-один * I'll introduce a new notation for arrays: type-identifier < array-length > 1: ROOT[ 20, 31 ] -- pointer 31 is invalid, destination has no type-identifier. 20: INT<5> -- info about layout of the next memory data (spans by 10 memory units) 30: 2 31: 3 -- should have type-identifier, because someone 32: 5 is pointing here!! Invalid memory!! 33: 7 34: 11 Мы все еще можем попытаться разместить информацию о макете рядом с указателем, вместо. Массив общей памяти теперь возможен: Пример: 1: ROOT[ INT<5>/30, INT<2>/31 ] 30: 2 31: 3 32: 5 33: 7 34: 11 Помните, что этот подход заставляет нас повторять информацию о макете везде ... но смысл в том, чтобы использовать меньше памяти, не так ли? Но чтобы поделиться памятью, нам нужно больше памяти для хранения данных макета / указателей. Для нас пока нет пончиков. = (* +1062 * Есть только одна надежда: давайте ухудшим функции времени выполнения! ЭТО МОЙ ОТВЕТ - Как мне кажется, это возможно => Как насчет использования каталога выделения памяти для хранения информации о типах? Это можно сделать, но тогда пострадает динамическое приведение, а также пострадает GC. Помните, я сказал, что GC необходимо получить доступ к каталогу памяти, просто чтобы удалить объекты ... ну, теперь ему нужно было бы обращаться к каталогу каждый раз, когда он находит ссылку, не просто удалить. О, МОЙ БОГ!! Мы собираемся убить производительность GC этим, а также производительность во время выполнения. Я думаю, слишком высокая стоимость! <= ЭТО МОЙ ОТВЕТ </em> Но ... а если среда выполнения не поддерживает динамическое приведение? если компилятор знает все о типе во время компиляции ... тогда GC даже не будет существовать ... ему НУЖНО информация о типе, потому что эта информация говорит ему, что такое макет память, используемая этим типом. Не простое, умное решение, в поле зрения. Может быть, я просто неправ. Но я не могу представить себе путь к этому лучше, чем он есть.Современные GC еще сложнее, чем это ... Я рассмотрел только основы, Я думаю, что современные GC оптимизируют другими способами, то есть другими, более надежными способами. Другие ссылки: http://en.wikipedia.org/wiki/Garbage_collection_(computer_science) http://www.memorymanagement.org/glossary/t.html http://www.cs.purdue.edu/homes/sbarakat/cs456/gc-2.pdf Трехцветное инкрементное обновление ГХ: нужно ли сканировать каждый стек дважды?
3 голосов
/ 14 августа 2011

D язык поддерживает срезы массивов именно с таким поведением GC. Посмотрите на http://www.dsource.org/projects/dcollections/wiki/ArrayArticle#WhosResponsible для получения дополнительной информации.

1 голос
/ 19 июня 2012

Кто-нибудь знает пример реализации языка, в которой есть сборщик мусора, подобный этому?

Нет. Я не думаю, что какие-либо языковые реализации в настоящее время делают это.

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

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

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

Другим решением будет использование виртуальной памяти. Вы можете «подделать» копирование моих логических страниц сопоставления на одну и ту же физическую страницу, и сборщик мусора может собрать недоступные страницы. Однако это очень крупнозернистая и, следовательно, еще более нишевая.

0 голосов
/ 08 августа 2011

Я думаю, что все реализации ассоциативных массивов (PERL, PHP, javascript ..) должны быть выполнены таким образом.Вы называете это «сбор мусора», но это означает, что определенные элементы должны быть сначала сброшены (удалены, удалены), чтобы сборщик мусора знал, что они не используются.Таким образом, это нормальное удаление / удаление / сброс, что наверняка отражается не только в ассоциативном массиве, но и в структуре данных, используемой конкретной реализацией языка.В противном случае память может быть исчерпана почти пустым массивом ...

0 голосов
/ 08 августа 2011

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

...