Виділення Java Heap швидше, ніж C ++


13

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


Я читав коментарі до цієї відповіді і побачив цю цитату.

Об'єктні об'єктивні та об'єктно-орієнтовані функції швидко використовуються (швидше, ніж C ++ у багатьох випадках), оскільки вони розроблені з самого початку. і колекції швидко. Стандартна Java перемагає стандартну C / C ++ у цій галузі, навіть для найбільш оптимізованого коду С.

Один користувач (із дійсно високим представником, який я можу додати) сміливо захищав цю заяву, заявляючи, що

  1. розподіл купи в Java краще, ніж у C ++

  2. і додав це твердження, захищаючи колекції в java

    І колекції Java швидкі порівняно з колекціями C ++, в основному завдяки різній підсистемі пам'яті.

Тож моє запитання - чи може щось із цього насправді бути правдивим, і якщо так, то чому виділення купи Java відбувається набагато швидше.


Ви можете знайти мою відповідь на подібне запитання на ТАК корисною / актуальною.
Даніель Приден

1
Це банально: за допомогою Java (або будь-якого іншого керованого, обмеженого середовища) ви можете переміщати об'єкти та оновлювати покажчики на них, тобто оптимізувати для кращого місця розташування кешу динамічно. За допомогою C ++ та його арифметики вказівників з неконтрольованими біткоістами всі об'єкти закріплюються на своє місце назавжди.
SK-логіка

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

1
@gbjbaanb, ви коли-небудь чули про ієрархію пам’яті? Кеш пропускає штраф? Чи розумієте ви, що розподільник загального призначення дорогий, тоді як розподіл першого покоління - це лише одна операція додавання?
SK-логіка

1
Незважаючи на те, що це може бути дещо правдою, у деяких випадках пропускається те, що в java ви виділяєте все на купі, а в c ++ ви виділяєте велику кількість об'єктів на стеку, що може бути дуже швидким.
JohnB

Відповіді:


23

Це цікаве питання, а відповідь складний.

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

C ++ може перемогти JVM GC спеціалізованими розподільниками пам'яті , розробленими для конкретних цілей. Прикладами можуть бути:

  • Перкадрові розподільники пам’яті, які періодично протирають всю область пам’яті. Вони часто використовуються в іграх на C ++, наприклад, коли тимчасова область пам'яті використовується один раз на кадр і негайно викидається.
  • Спеціальні розподільники, що керують пулом об'єктів фіксованого розміру
  • Розподіл на основі стека (хоча зауважте, що JVM також робить це за різних обставин, наприклад, за допомогою аналізу втечі )

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

Збір сміття також дає вагомі переваги з точки зору ефективності:

  • Облік об'єктів дійсно надзвичайно швидкий. Через те, що нові об'єкти послідовно виділяються в пам'яті, для цього часто потрібно трохи більше, ніж одне додавання покажчиків, що, безумовно, швидше, ніж типові алгоритми розподілу купи C ++.
  • Ви уникаєте потреби у витратах на управління життєвим циклом - наприклад, підрахунок посилань (іноді використовується як альтернатива GC) вкрай поганий з точки зору продуктивності, оскільки часте збільшення та зменшення еталонних підрахунків додає великі накладні показники (зазвичай набагато більше, ніж GC) .
  • Якщо ви використовуєте незмінні об'єкти, ви можете скористатися структурним спільним доступом для збереження пам'яті та підвищення ефективності кешу. Це широко використовується функціональними мовами на JVM, як Scala та Clojure. Це дуже важко зробити без ГК, оскільки надзвичайно важко керувати життями спільних об'єктів. Якщо ви вважаєте (як і я), що незмінність та структурний обмін є ключовими для створення великих одночасних застосувань, то це, мабуть, найбільша перевага GC в продуктивності.
  • Ви можете уникнути копіювання, якщо всіма об'єктами та відповідними життєвими циклами керує одна і та ж система збору сміття. На відміну від C ++, де вам часто доводиться отримувати повні копії даних, оскільки призначення вимагає іншого підходу до управління пам'яттю або має інший життєвий цикл об'єкта.

У Java GC є один головний недолік: оскільки робота зі збирання сміття відкладається і виконується в шматки роботи з періодичними інтервалами, це викликає періодичні паузи GC для збору сміття, що може вплинути на затримку. Зазвичай це не є проблемою для типових програм, але може виключати Java у ситуаціях, коли важкий реальний час є вимогою (наприклад, роботизований контроль). Типовий режим реального часу (наприклад, ігри, мультимедіа), як правило, добре.


Є спеціалізовані бібліотеки в області c ++, які вирішують цю проблему. Напевно, найвідоміший приклад для цього - SmartHeap.
Тобіас Лангнер

5
Soft- в режимі реального часу , не означає , що ти в порядку , щоб зупинити зазвичай . Це просто означає, що ви можете зробити паузу / повторити спробу в реальній поганій ситуації - зазвичай несподіваній - замість зупинки / аварії / відмови. Ніхто не хотів би використовувати звичайно призупинення музичного плеєра. Проблема паузи в ГК - це трапляється зазвичай і непередбачувано . Таким чином, пауза GC неприйнятна навіть для програмного забезпечення в режимі реального часу. Пауза в GC прийнятна лише тоді, коли користувачам не важлива якість додатків. І в наш час люди вже не такі наївні.
Eonil

1
Будь ласка, опублікуйте деякі вимірювання ефективності, щоб підтвердити ваші претензії, інакше ми порівнюємо яблука та апельсини.
JBRWilkinson

1
@Demetri Але насправді це лише в тому випадку, якщо випадків трапляється занадто багато (і знову ж таки, навіть непередбачувано!), Якщо ви не зможете задовольнити деякі непрактичні обмеження. Іншими словами, C ++ набагато простіше для будь-якої ситуації в реальному часі.
Еоніл

1
Для повноти: є ще один недолік продуктивності GC: оскільки в більшості існуючих GC пам'ять звільнення відбувається в іншій потоці, яка, ймовірно, працює на іншому ядрі, це означає, що GC несуть великі витрати на відключення кешу для синхронізації L1 / L2 кеші між різними ядрами; крім того, на серверах, на яких переважно є NUMA, кеші L3 повинні також синхронізуватися (і над Hypertransport / QPI, ouch (!)).
Заєць із

3

Це не наукове твердження. Я просто даю трохи їжі для роздумів з цього питання.

Одна з візуальних аналогій така: вам надають квартиру (житловий квартал), котрий обстелений килимами. Килим брудний. Який найшвидший спосіб (в години) зробити підлогу квартири чистою?

Відповідь: просто загорніть старий килим; викинути; і прокатати новий килим.

Чого ми тут нехтуємо?

  • Вартість виїзду існуючих особистих речей та їх переїзду.
    • Це відомо як "стоп-світ" вартість вивезення сміття.
  • Вартість нового килима.
    • Що, випадково для оперативної пам’яті, безкоштовно.

Збір сміття - це величезна тема, і в програмістах.SE та StackOverflow виникає багато питань.

Що стосується побічної проблеми, диспетчер розподілу C / C ++, відомий як TCMalloc, разом з підрахунком посилання на об'єкт теоретично здатний задовольнити вимоги щодо найкращих показників роботи будь-якої системи GC.


насправді c ++ 11 навіть має сміття ABI , це досить схоже на деякі відповіді, які я отримав на SO
aaronman

Страх зламати існуючі програми C / C ++ (бази кодів, такі як ядра Linux та archaic_but_still_economically_важливі бібліотеки, такі як libtiff) перешкоджав прогресу мовних інновацій у C ++.
rwong

Має сенс, я б здогадався, що через c ++ 17 він буде більш повним, але правда, як тільки ти справді навчишся програмувати на c ++, ти навіть цього більше не хочеш, можливо, вони зможуть знайти спосіб поєднати дві ідіоми красиво
aaronman

Ви усвідомлюєте, що є сміттєзбірники, які не зупиняють світ? Чи розглядали Ви наслідки для компактизації результатів (на стороні GC) та фрагментації купи (для загальних розподільників C ++)?
SK-логіка

2
Я думаю, що головний недолік цієї аналогії полягає в тому, що GC насправді робить це знайти брудні шматочки, вирізати їх, а потім побачити решту бітів разом, щоб створити новий килим.
svick

3

Основна причина полягає в тому, що, коли ви запитаєте у Java новий грудочок пам’яті, він іде прямо до кінця купи і дає вам блок. Таким чином, розподіл пам’яті настільки ж швидко, як і розподіл на стеці (саме так ви робите це більшу частину часу в C / C ++, але крім цього ..)

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

Це одна з причин, через які показники Java / .NET демонструють таку хорошу продуктивність, але в реальному застосуванні такі погані показники. Мені залишається лише дивитись програми на своєму телефоні - справді швидкі, чуйні всі написані за допомогою NDK, настільки навіть я здивований.

Зараз колекції можуть бути швидкими, якщо всі об'єкти локально розподілені, наприклад, в одному суміжному блоці. Тепер у Java ви просто не отримуєте суміжних блоків, оскільки об'єкти виділяються один за одним із вільного кінця купи. Ви можете закінчити з ними щасливо суміжні, але лише пощастило (тобто, до примхи процедур ущільнення GC та того, як він копіює об'єкти). З іншого боку, C / C ++ явно підтримує суміжні розподіли (очевидно, через стек). Як правило, купи об'єктів у C / C ++ нічим не відрізняються від BTW Java.

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


2
Схоже, ви взагалі не розумієте GC. Розглянемо найбільш типовий сценарій - сотні невеликих об’єктів постійно виділяються, але лише десяток з них виживе більше секунди. Таким чином, звільнення пам’яті абсолютно не витрачається - цей десяток копіюється з молодого покоління (і ущільнюється, як додаткова вигода), а решта викидається безкоштовно. І, до речі, жалюгідний Dalvik GC не має нічого спільного з сучасними, найсучаснішими ГК, які ви знайдете у належних реалізаціях JVM.
SK-логіка

1
Якщо один із звільнених предметів знаходиться посередині купи, решта купи буде ущільнена, щоб повернути простір. Або ви кажете, що ущільнення GC не відбувається, якщо це не найкращий ви описаний? Я знаю, що покоління ГК тут набагато краще, якщо ви не випустите об’єкт у середині наступних поколінь, і в цьому випадку вплив може бути порівняно великим. Щось написав Microsoftie, який працював над їх GC, що я прочитав, що описав компроміси GC під час генерації GC поколінь.
gbjbaanb

1
Про яку "купу" ви говорите? Більшість сміття переробляється на етапі молодого покоління, і більшість корисних результатів виходить саме від цієї компактності. Звичайно, це здебільшого видно на профілі розподілу пам’яті, типовому для функціонального програмування (багато короткоживучих невеликих об’єктів). І, звичайно, є численні можливості оптимізації, які ще не досить вивчені - наприклад, динамічний аналіз регіону, який може перетворити купі призначення на певному шляху в автоматичне виділення стека або пулу.
SK-логіка

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

1
Я думаю, що так, але з Java та .net ви бачите мою думку - вам не доведеться ходити по купі, щоб знайти наступний безкоштовний блок, щоб його було значно швидше в цьому плані, але так - ви маєте рацію, це повинно бути заблоковано, що зашкодить потоковим програмам.
gbjbaanb

2

Едемський простір

Тож моє запитання - чи може щось із цього насправді бути правдивим, і якщо так, то чому виділення купи Java відбувається набагато швидше.

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

Спосіб розподілу Java GC полягає у використанні надзвичайно дешевої стратегії розподілу для початкового розподілу об'єктів у просторі "Еден". Як я можу сказати, це використання послідовного розподільника пулу.

Це набагато швидше саме з точки зору алгоритму та зменшення обов'язкових помилок сторінки, ніж загального призначення mallocна C або за замовчуванням, викидання operator newC ++.

Але послідовні розподільники мають очевидну слабкість: вони можуть виділяти шматки змінного розміру, але вони не можуть звільнити жодних окремих шматочків. Вони просто виділяють прямим послідовним способом з накладкою для вирівнювання і можуть лише очистити всю пам'ять, яку вони виділили одразу. Вони зазвичай корисні для C та C ++ для побудови структур даних, для яких потрібні лише вставки та не видалення елементів, як дерево пошуку, яке потрібно створити лише один раз, коли програма запускається, а потім повторно шукається або додаються лише нові ключі ( клавіші не видалено)

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

Колекція

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

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

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

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

Швидкість

Отже, грубо кажучи, причина Java GC може дуже добре перевершити C або C ++ при прямому розподілі купи, тому що вона використовує найдешевшу, повністю вироджену стратегію розподілу в потоці з проханням виділити пам'ять. Тоді це економить більш дорогу роботу, яку нам зазвичай потрібно робити, використовуючи більш загальний розподільник, як прямий вгору mallocдля іншого потоку.

Таким чином, концептуально GC насправді має робити більше загальної роботи, але він розподіляє це по потоках, щоб повна вартість не була сплачена заздалегідь одним потоком. Це дозволяє потоку, що виділяє пам'ять, робити це дуже дешево, а потім відкладає справжні витрати, необхідні для того, щоб робити все належним чином, щоб окремі об'єкти могли бути фактично звільнені в інший потік. У C або C ++, коли ми mallocабо дзвонимо operator new, ми повинні сплатити повну вартість заздалегідь у межах однієї нитки.

Це головна відмінність, і чому Java може дуже добре перевершити C або C ++, використовуючи лише наївні дзвінки mallocабо operator newвиділяти купу маленьких шматок окремо. Звичайно, як правило, будуть якісь атомні операції та потенційна блокування, коли починається цикл GC, але це, мабуть, оптимізовано зовсім небагато.

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

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


0

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

Якщо ви просто дивитесь на розподіл / угоду, у C ++ у вас може бути 1 000 000 дзвінків на malloc та 1 000 000 дзвінків у безкоштовний (). У Java у вас буде 1 000 000 викликів до new () та сміттєзбірника, що працює в циклі, знаходячи 1 000 000 об'єктів, які він може звільнити. Цикл може бути швидшим, ніж виклик free ().

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

З третьої сторони, є такі речі, як підрахунок довідок, який вам може знадобитися без збору сміття, і це не приходить безкоштовно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.