Демонстрация сборки мусора быстрее, чем ручное управление памятью

23

Я читал во многих местах (черт, я даже написал так себе) , что сбор мусора может (теоретически) быстрее , чем ручное управление памятью.

Однако показать гораздо сложнее, чем рассказать.
Я никогда не видел ни одного куска кода, который демонстрирует этот эффект в действии.

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

Mehrdad
источник
5
проблема с GC заключается в том, что большинство реализаций не являются детерминированными, поэтому 2 прогона могут иметь совершенно разные результаты, не говоря уже о том, что трудно выделить правильные переменные для сравнения
ratchet freak
@ratchetfreak: Если вам известны примеры, которые бывают быстрее (скажем) в 70% случаев, это тоже хорошо для меня. Должен быть какой-то способ сравнить их, по крайней мере, с точки зрения пропускной способности (задержка, вероятно, не сработает).
Мердад
1
Ну, это немного сложно, потому что вы всегда можете сделать все вручную, что дает GC преимущество над тем, что вы делали вручную. Возможно, лучше ограничить это «стандартными» инструментами ручного управления памятью (malloc () / free (), собственными указателями, общими указателями с refcount, слабыми указателями, без пользовательских распределителей)? Или, если вы разрешаете использование пользовательских распределителей (которые могут быть более реалистичными или менее реалистичными, в зависимости от того, какого типа программист вы предполагаете), наложите ограничения на усилия, прилагаемые к этим распределителям. В противном случае, ручная стратегия «скопировать то, что делает GC в этом случае» всегда по крайней мере так же быстро, как GC.
1
Под «копировать то, что делает GC», я не имел в виду «построить свой собственный GC» (хотя учтите, что это теоретически возможно в C ++ 11 и более поздних версиях, которые вводят необязательную поддержку GC). Я имел в виду, как я сформулировал это ранее в том же комментарии: «делай то, что дает ГК преимущество над тем, что ты делал вручную». Например, если сжатие по Чейни очень помогает этому приложению, вы можете вручную реализовать аналогичную схему распределения + сжатия с настраиваемыми интеллектуальными указателями для обработки исправления указателя. Кроме того, с помощью таких методов, как стек теней, вы можете выполнять поиск корней в C или C ++ за счет дополнительной работы.
1
@ Айк: Все в порядке. Видишь, почему я задал вопрос? В этом весь смысл моего вопроса - люди придумывают всевозможные объяснения, которые должны иметь смысл, но все спотыкаются, когда вы просите их предоставить демонстрацию, которая доказывает, что они говорят, что это правильно на практике. Весь смысл этого вопроса заключался в том, чтобы раз и навсегда показать, что это действительно может произойти на практике.
Мердад

Ответы:

26

См. Http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx и перейдите по всем ссылкам, чтобы увидеть, как Рико Мариани против Раймонда Чена (оба очень компетентных программиста в Microsoft) сражаются друг с другом. , Раймонд улучшит неуправляемый, Рико ответит, оптимизировав то же самое в управляемых.

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

btilly
источник
+1 за цитирование реального примера с кодом :), хотя правильное использование конструкций C ++ (таких как swap) не так уж сложно, и, вероятно, приведет вас к этому довольно легко с точки зрения производительности ...
Mehrdad
5
Вы можете превзойти Раймона Чена по производительности. Я уверен, что не смогу, если он не выйдет из-за болезни, я много раз работаю и мне повезло. Я не знаю, почему он не выбрал решение, которое вы бы выбрали. Я уверен, что у него были причины для этого
до
Я скопировал код Раймонда здесь , и сравнить, я написал свою собственную версию здесь . ZIP-файл, содержащий текстовый файл, находится здесь . На моем компьютере мой работает за 14 мс, а Раймонд за 21 мс. Если я не сделал что-то не так (что возможно), его 215-строчный код будет на 50% медленнее, чем моя 48-строчная реализация, даже без использования отображенных в памяти файлов или пользовательских пулов памяти (которые он использовал). Мой в два раза меньше, чем версия C #. Я сделал это неправильно, или вы наблюдаете то же самое?
Мердад
1
@Mehrdad Вытащив старую копию gcc на этот ноутбук, я могу сообщить, что ни ваш код, ни его не будут компилироваться, не говоря уже о том, чтобы что-то с этим делать. Тот факт, что я не на Windows, вероятно, объясняет это. Но давайте предположим, что ваши номера и код верны. Они выполняют то же самое на десятилетнем компиляторе и компьютере? (Посмотрите, когда был написан блог.) Может быть, а может и нет. Давайте предположим, что это так, что он (будучи программистом на C) не знал, как правильно использовать C ++ и т. Д. С чем мы остались?
13
1
У нас есть разумная программа на C ++, которая может быть переведена в управляемую память и ускорена. Но где версия C ++ может быть оптимизирована и ускорена дальше. То, с чем мы все согласны, это общая схема, которая всегда происходит, когда управляемый код работает быстрее, чем неуправляемый. Однако у нас все еще есть конкретный пример разумного кода от хорошего программиста, который был быстрее в управляемой версии.
13
5

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

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

GC - это особый случай ручного управления памятью

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

Гай Сиртон
источник
1
Это не имеет смысла для меня. Чтобы дать вам несколько конкретных примеров: 1) распределители и барьеры записи в производственных GC являются рукописными ассемблерами, потому что C слишком неэффективен, так как вы справитесь с этим из C, и 2) устранение хвостовых вызовов является примером оптимизации сделано на высокоуровневых (функциональных) языках, которые не выполняются компилятором C и, следовательно, не могут быть выполнены на C. Ходьба в стеке - это еще один пример чего-то, что делается языком высокого уровня ниже уровня C.
Джон Харроп
2
1) Мне нужно увидеть конкретный код для комментирования, но если рукописные распределители / барьеры в ассемблере быстрее, чем использовать рукописный ассемблер. Не уверен, что это имеет отношение к GC. 2) Взгляните сюда: stackoverflow.com/a/9814654/441099 вопрос не в том, может ли какой-то не-GC язык сделать удаление хвостовой рекурсии за вас. Дело в том, что вы можете преобразовать свой код так быстро или быстро. Может ли компилятор какого-то определенного языка сделать это для вас автоматически - вопрос удобства. В достаточно низкой абстракции вы всегда можете сделать это самостоятельно, если хотите.
Гай Сиртон
1
Этот пример хвостового вызова в C работает только для особого случая вызова функции. C не может обработать общий случай функций, вызывающих друг друга. Перейдя к ассемблеру и предполагая бесконечное время для разработки, это брезент Тьюринга
Джон Харроп
3

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

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

Для практических программ, написанных на языках с сборкой мусора, преимуществом сборки мусора является не скорость, а правильность и простота.

ddyer
источник
Если создать искусственный пример легко, не могли бы вы показать простой?
Мердад
1
@ Mehrdad Он объяснил простой. Напишите программу, в которой версия GC не может выполнить мусор до выхода. Версия с ручным управлением памятью будет медленнее, потому что она явно отслеживала и освобождала вещи.
13
3
@btilly: «Напишите программу, в которой версия GC не может выполнить мусор перед выходом». ... неспособность выполнить сборку мусора в первую очередь является утечкой памяти из-за отсутствия работающего GC, а не улучшения производительности из-за присутствия GC! Это похоже на вызов abort()в C ++ до выхода из программы. Это бессмысленное сравнение; вы даже не собираете мусор, вы просто позволяете утечке памяти. Вы не можете сказать, что сборка мусора происходит быстрее (или медленнее), если вы не собираете мусор для начала ...
Mehrdad
Чтобы привести крайний пример, вам нужно определить полную систему с вашим собственным управлением кучей и кучей, что было бы отличным студенческим проектом, но слишком большим, чтобы уместиться в этом поле. Вы бы хорошо справились, написав программу, которая распределяет и освобождает массивы случайных размеров, таким образом, что это создает стресс для методов управления памятью не-gc.
ddyer
3
@ Mehrdad Не так. Сценарий состоит в том, что версия GC никогда не достигала порога, при котором она выполняла бы прогон, а не то, что она не могла бы работать правильно на другом наборе данных. Это тривиально будет очень хорошо для версии GC, хотя и не является хорошим предиктором конечной производительности.
Btilly
2

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

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

ddyer
источник
2

Быстрее сомнительна. Однако он может быть сверхбыстрым, незаметным или быстрым, если поддерживается аппаратное обеспечение. Подобные конструкции были для машин LISP давным-давно. Один встроил GC в подсистему памяти аппаратного обеспечения таким образом, чтобы основной процессор не знал, что он там находится. Как и во многих более поздних разработках, сборщик мусора работал одновременно с основным процессором, практически без необходимости делать паузы. Более современный дизайн - это машины Azul Systems Vega 3, которые выполняют Java-код намного быстрее, чем JVM, использующие специализированные процессоры и GC без пауз. Найдите их, если хотите знать, насколько быстрым может быть GC (или Java).

Ник П
источник
2

Я проделал довольно много работы над этим и описал некоторые из них здесь . Я тестировал Boehm GC на C ++, выделяя с помощью, mallocно не освобождая, выделяя и освобождая с помощью freeGC, созданного на заказ, написанного на C ++, и все по сравнению со стандартным GC OCaml, на котором работает решатель n-queens на основе списка. GC OCaml был быстрее во всех случаях. Программы на C ++ и OCaml были специально написаны для выполнения одинакового распределения в одном и том же порядке.

Конечно, вы можете переписать программы для решения проблемы, используя только 64-битные целые числа и без выделения. Хотя это было бы быстрее, это побило бы смысл упражнения (которое должно было предсказать производительность нового алгоритма GC, над которым я работал, используя прототип, построенный на C ++).

Я провел много лет в отрасли, портируя реальный код C ++ на управляемые языки. Почти в каждом отдельном случае я наблюдал существенные улучшения производительности, многие из которых, вероятно, были связаны с тем, что GC затормозил ручное управление памятью. Практическое ограничение заключается не в том, что может быть достигнуто в микробенчмарке, а в том, что может быть достигнуто до истечения крайнего срока, и языки на основе GC предлагают такие огромные улучшения производительности, что я никогда не оглядывался назад. Я все еще использую C и C ++ на встроенных устройствах (микроконтроллерах), но даже сейчас это меняется.

Джон Харроп
источник
+1 спасибо. Где мы можем увидеть и запустить код теста?
Mehrdad
Код разбросан по всему месту. Я разместил версию для региона маркеров
Джон Харроп
1
Там есть результаты как для потоковых, так и для небезопасных.
Джон Харроп
1
@Mehrdad: «Вы устранили такие потенциальные источники ошибок?». Да. OCaml имеет очень простую модель компиляции без оптимизаций, таких как escape-анализ. Представление замыкания в OCaml на самом деле значительно медленнее, чем в решении C ++, поэтому оно должно использовать кастом, List.filterкак в C ++. Но, да, вы, конечно, совершенно правы, что некоторые операции RC могут быть исключены. Тем не менее, самая большая проблема, которую я вижу в дикой природе, заключается в том, что у людей нет времени, чтобы вручную выполнить такую ​​оптимизацию на больших промышленных кодах.
Джон Харроп
2
Да, конечно. Никаких дополнительных усилий для написания, но написание кода не является узким местом в C ++. Ведение кода есть. Поддержание кода с такой непредвиденной сложностью - это кошмар. Большинство промышленных кодовых баз - это миллионы строк кода. Вы просто не хотите иметь дело с этим. Я видел, как люди конвертировали все, чтобы shared_ptrпросто исправить ошибки параллелизма. Код работает намного медленнее, но теперь он работает.
Джон Харроп
-1

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

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

Теперь рассмотрим ручной распределитель, который использует тот же механизм выделения GC, и free()вызов которого просто выделяет память для освобождения тем же методом, что и GC. У него нет фазы сканирования и других методов. Это обязательно займет меньше времени.

MSalters
источник
2
Сборщик мусора часто может освободить много объектов без необходимости переводить память в полезное состояние после каждого. Рассмотрим задачу удаления из массива-списка всех элементов, удовлетворяющих определенному критерию. Удаление одного элемента из списка N-элементов - это O (N); удаляя M элементов из списка N, по одному за раз O (M * N). Однако удаление всех элементов, соответствующих критерию, за один проход по списку - это O (1).
суперкат
@supercat: freeтакже может собирать партии. (И, конечно, удаление всех элементов, отвечающих критерию, по-прежнему равно O (N), хотя бы из-за самого обхода списка)
MSalters
Удаление всех элементов, отвечающих критерию, составляет не менее O (N). Вы правы, что freeмогли бы работать в режиме пакетного сбора, если с каждым элементом памяти был связан флаг, хотя в некоторых ситуациях GC все еще может выйти вперед. Если имеется M ссылок, которые идентифицируют L различных элементов из набора из N вещей, время для удаления каждой ссылки, на которую не существует никакой ссылки, и консолидации остатка, составляет O (M), а не O (N). Если у вас есть M доступного дополнительного пространства, константа масштабирования может быть довольно маленькой. Кроме того, для компактификации в не сканирующей системе ГХ требуется ...
суперкат
@supercat: Ну, это, конечно, не O (1), как говорится в последнем предложении первого комментария.
MSalters
1
@MSalters: «А что помешало бы детерминированной схеме иметь детский сад?». Ничего такого. Трассирующий сборщик мусора OCaml является детерминированным и использует детскую. Но это не «руководство», и я думаю, что вы неправильно используете слово «детерминированный».
Джон Харроп