Демонстрація сміття відбувається швидше, ніж ручне управління пам’яттю


23

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

Однак показати набагато складніше, ніж розповісти.
Я ніколи не бачив жодного фрагмента коду, який би демонстрував цей ефект у дії.

Хтось має (або знає, де я можу знайти) код, який демонструє цю перевагу у виконанні?


5
Проблема з GC полягає в тому, що більшість реалізацій не детерміновані, тому два запуски можуть мати дуже різні результати, не кажучи вже про важке виділення правильних змінних для порівняння
ratchet freak

@ratchetfreak: Якщо ви знаєте про будь-які приклади, які швидко проходять (скажімо) 70% часу, це теж добре зі мною. Має бути якийсь спосіб порівняння цих двох, щонайменше, щодо пропускної здатності (затримка, ймовірно, не спрацює).
Мехрдад

1
Ну, це трохи хитро, тому що ви завжди можете вручну робити все, що дає перевагу GC перед тим, що ви робили вручну. Можливо, краще обмежити це "стандартними" ручними засобами управління пам’яттю (malloc () / free (), що володіють покажчиками, загальними вказівниками зі знижкою, слабкі вказівники, відсутні спеціальні розподільники)? Або, якщо ви дозволяєте користувацькі алокатори (які можуть бути більш реалістичними або менш реалістичними, залежно від того, який програміст ви припускаєте), накладіть обмеження на зусилля, що докладаються до цих розподільників. В іншому випадку ручна стратегія "скопіювати те, що робить GC в цьому випадку" завжди є принаймні такою швидкою, як GC.

1
Під "копіюванням того, що робить GC" я не мав на увазі "побудувати свій власний GC" (хоча зауважте, що це теоретично можливо в C ++ 11 і далі, що вводить додаткову підтримку для GC). Я мав на увазі, як я вже говорив це в тому самому коментарі, "роби те, що дає перевагу GC перед тим, що ти робив вручну". Наприклад, якщо ущільнення, подібне до Чейні, дуже допомагає цій програмі, ви можете вручну реалізувати подібну схему розподілу + ущільнення, використовуючи власні розумні покажчики для обробки налаштування вказівника. Крім того, за допомогою таких методів, як тіньовий стек, ви можете виконувати пошук коренів у C або C ++ за рахунок додаткової роботи.

1
@Ike: Це добре. Подивіться, чому я задав питання? В цьому і полягала вся суть мого запитання - люди придумують всілякі пояснення, які повинні мати сенс, але всі спотикаються, коли ви попросите їх надати демонстрацію, яка підтверджує, що те, що вони говорять, є правильним на практиці. Вся суть цього питання полягала в тому, щоб раз і назавжди показати, що це насправді може статися на практиці.
Мехрдад

Відповіді:


26

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

Поклавши нульові зусилля на оптимізацію, керовані версії запускалися в багато разів швидше, ніж ручні. Врешті-решт посібник обіграв кероване, але лише оптимізувавшись до рівня, на який більшість програмістів не хотіли б йти. У всіх версіях використання пам'яті в посібнику було значно кращим, ніж кероване.


+1 для наведення фактичного прикладу з кодом :), хоча правильне використання C ++ конструкцій (таких як swap) не так вже й складно, і, ймовірно, потрапить до вас там досить легко з ефективністю ...
Mehrdad

5
Можливо, ви зможете перевершити Реймонда Чену по продуктивності. Я впевнений, що не можу, якщо він не з цього через хворобу, я працюю багато разів важче, і мені пощастило. Я не знаю, чому він не обрав рішення, яке ви обрали б. Я впевнений, що в нього були причини
btilly

Я скопіював тут код Реймонда , і для порівняння я написав тут свою версію . ZIP - файл , який містить текстовий файл знаходиться тут . На моєму комп’ютері міна працює в 14 мс, а Реймонд працює в 21 мс. Якщо я не зробив щось не так (що можливо), його 215-рядовий код на 50% повільніше, ніж моя 48-рядова реалізація, навіть не використовуючи файли, зіставлені з пам’яттю, або користувацькі пули пам’яті (якими він користувався). Моє наполовину довше, ніж версія C #. Я зробив це неправильно, чи ви спостерігаєте те саме?
Мехрдад

1
@Mehrdad Витягнувши на цьому ноутбуці стару копію gcc, я можу повідомити, що ні ваш код, ні його не будуть збиратись, не кажучи вже про те, щоб зробити що-небудь із цим. Те, що я не в Windows, ймовірно, це пояснює. Але припустимо, що ваші цифри та код правильні. Вони виконують те ж саме на десятирічному компіляторі та комп’ютері? (Подивіться, коли блог був написаний.) Можливо, може й ні. Припустимо, що вони є, що він (будучи програмістом на C) не знав, як правильно використовувати C ++ і т. Д. Що нам залишилося?
btilly

1
Нам залишається розумна програма C ++, яку можна перекласти в керовану пам'ять і прискорити. Але там, де версія C ++ може бути оптимізована та просунута далі. З чим ми всі згодні, це загальна модель, яка завжди відбувається, коли керований код швидше, ніж некерований. Однак у нас все ще є конкретний приклад розумного коду від хорошого програміста, який був швидшим в керованій версії.
btilly

5

Основне правило - безкоштовних обідів немає.

GC знімає головний біль ручного управління пам’яттю і зменшує ймовірність помилок. Є деякі ситуації, коли певна стратегія GC є оптимальним рішенням проблеми, і в цьому випадку ви не заплатите штраф за її використання. Але є й інші, де інші рішення будуть швидшими. Оскільки ви завжди можете імітувати більш високі абстракції з нижчого рівня, але не навпаки, ви можете ефективно довести, що немає можливості, щоб більш високі абстракції були швидшими, ніж нижчі в загальному випадку.

GC - це особливий випадок ручного управління пам'яттю

Для кращої роботи вручну може бути багато роботи або більше схильності до помилок, але це вже інша історія.


1
Це для мене немає сенсу. Щоб навести кілька конкретних прикладів: 1) розподільники та бар'єри запису у виробничих ГК - це написаний вручну асемблер, оскільки C занадто неефективний, як ви переможете це з C, і 2) усунення хвостових викликів є прикладом оптимізації зроблено на мовах високого рівня (функціональних), що не робиться компілятором C, і, отже, не може бути виконано в C. Ходіння по стеком - це ще один приклад того, що зроблено нижче рівня C мовами високого рівня.
Джон Харроп

2
1) Я повинен побачити конкретний код для коментарів, але якщо рукописні розподільники / бар'єри в асемблері швидші, то використовуйте рукописний асемблер. Не впевнений, що це стосується GC. 2) Подивіться тут: stackoverflow.com/a/9814654/441099 справа не в тому, чи може якась мова, яка не є GC, для вас усунути хвостові рекурсії. Справа в тому, що ви можете перетворити свій код настільки ж швидко або швидше. Чи може компілятор якоїсь конкретної мови зробити це для вас автоматично - це питання зручності. У досить низькій абстракції ви завжди можете зробити це самостійно, якщо хочете.
Гай Сіртон

1
Цей приклад виклику хвоста в C працює лише для особливого випадку функції, що викликає себе. C не може впоратися із загальним випадком функцій хвоста, що дзвонить один одному. Тріпінг Тюрінга припадає на монтажник і передбачає нескінченний час на розвиток.
Джон Харроп

3

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

Якщо ви задумаєтесь, це модель, яка використовується при збиранні сміття пам'яті, виділеної на процеси. Процес вмирає, все це пам'ять - сміття, ми закінчили. Навіть у практичному плані процес, який починається, запускається і вмирає, не залишаючи слідів, може бути більш ефективним, ніж той, який починається і працює вічно.

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


Якщо побудувати штучний приклад легко, чи не проти було б показати простий?
Мехрдад

1
@Mehrdad Він пояснив просте. Напишіть програму, де версія GC не справляється зі сміттям перед виходом. Версія, керована пам'яттю вручну, буде повільнішою, оскільки вона чітко відстежувала та звільняла речі.
btilly

3
@btilly: "Напишіть програму, де версія GC не спрацює зі сміттям перед виходом." ... в першу чергу відмову від сміття - це витік пам'яті через відсутність функціонуючого GC, а не покращення продуктивності через наявність GC! Це як зателефонувати abort()в C ++ перед виходом програми. Це безглузде порівняння; Ви навіть не збираєте сміття, ви просто даєте пам'яті просочитися. Ви не можете сказати, що збирання сміття відбувається швидше (або повільніше), якщо ви не збираєте сміття для початку ...
Mehrdad

Для того, щоб зробити крайній приклад, вам доведеться визначити повну систему із власним керуванням купою та купою, що було б чудовим студентським проектом, але занадто великим, щоб вмістити цей запас. Ви б зробили досить добре, написавши програму, яка виділяє і розміщує масиви випадкових розмірів таким чином, щоб бути напруженими для методів управління пам'яттю, що не стосується gc.
ddyer

3
@Mehrdad Не так. Сценарій полягає в тому, що версія GC ніколи не трапляла поріг, при якому вона зробила б пробіжку, не те, що вона не змогла б виконати правильно в іншому наборі даних. Це тривіально буде дуже добре для версії GC, хоча і не є хорошим прогнозувачем можливої ​​продуктивності.
btilly

2

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

Отже, якщо у вас є мова, яку збирають сміття, і порівнюйте швидкість із керованою пам’яттю вручну в одній системі, вам все одно доведеться заплатити накладні витрати, щоб підтримувати збирання сміття, навіть якщо ви не використовуєте його.


2

Швидше сумнівне. Однак це може бути надшвидким, непомітним або швидшим, якщо він підтримується апаратним забезпеченням. Такі конструкції для машин LISP давно існували. Один вбудував GC в підсистему пам'яті апаратного забезпечення як такий, що основний процесор не знав, що він є. Як і багато пізніших конструкцій, GC працював одночасно з основним процесором, не маючи необхідності в паузах або взагалі не потрібно. Більш сучасним дизайном є машини Azul Systems Vega 3, які керують Java кодом набагато швидше, ніж JVM, використовуючи цільові процесори та без паузи GC. Google, якщо ви хочете знати, наскільки швидкими можуть бути GC (або Java).


2

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

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

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


+1 спасибі Де ми можемо побачити та запустити контрольний код?
Мехрдад

Код розкиданий про місце. Тут я розмістив версію для регіону марки: groups.google.com/d/msg/…
Джон Харроп,

1
Є результати і для безпечних потоків, і для небезпечних.
Джон Харроп

1
@Mehrdad: "Чи ви усунули такі потенційні джерела помилок?". Так. OCaml має дуже просту модель компіляції без оптимізацій, таких як аналіз втечі. Представлення OCaml про закриття насправді значно повільніше, ніж рішення C ++, тому воно дійсно повинно використовувати звичай, List.filterяк це робить C ++. Але, так, ви, безумовно, абсолютно праві, що деякі операції RC можуть бути ухилені. Однак найбільша проблема, яку я бачу в дикій природі, полягає в тому, що люди не встигають проводити такі оптимізації вручну на великих базах промислових кодів.
Джон Харроп

2
Так, абсолютно. Ніяких додаткових зусиль для написання, але написання коду не є вузьким місцем із C ++. Підтримуючий код є. Збереження коду з таким видом випадкової складності - це кошмар. Більшість баз промислових кодів - це мільйони рядків коду. Ви просто не хочете мати з цим справи. Я бачив, як люди перетворюють все на те, shared_ptrщоб виправити помилки одночасності. Код набагато повільніше, але, ей, зараз він працює.
Джон Харроп

-1

Такий приклад обов'язково має погану схему розподілу пам'яті вручну.

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

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


2
Збирач сміття часто може звільнити багато об’єктів, не потребуючи переведення пам'яті у корисний стан після кожного. Розглянемо завдання видалити зі списку масивів усі елементи, що відповідають певному критерію. Видалення одного елемента зі списку N-елементів - це O (N); видалення M елементів із списку N, один за одним - O (M * N). Однак вилучити всі елементи, що відповідають критерію, за один прохід через список, це O (1).
supercat

@supercat: freeтакож можна збирати партії. (І, звичайно, видалення всіх предметів, що відповідають критерію, все-таки O (N), хоча б через сам перехід списку)
MSalters

Видалення всіх елементів, що відповідають критерію, принаймні O (N). Ви неправі, що freeможе працювати в режимі пакетного збору, якщо кожен елемент пам'яті мав пов'язаний з ним прапор, хоча GC у деяких ситуаціях все-таки може випереджати. Якщо у М є посилання, які ідентифікують L окремих елементів із набору N речей, час для видалення кожної посилання, на яку не існує посилань, та закріплення решти - це O (M), а не O (N). Якщо у вас є M додатковий простір, константа масштабування може бути зовсім невеликою. Крім того, для компактизації в системі, що не сканує ГК, потрібна ...
supercat

@supercat: Ну, звичайно, це не О (1), як зазначено в останньому реченні в першому коментарі.
MSalters

1
@MSalters: "А що заважатиме детермінованій схемі мати дитячий сад?". Нічого. Колекціонер сміття OCaml є детермінованим і використовує розплідник. Але це не "посібник", і я вважаю, що ви неправильно використовуєте слово "детермінований".
Джон Харроп
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.