Що підтримує твердження, що C ++ може бути швидшим, ніж JVM або CLR з JIT? [зачинено]


119

Повторювана тема щодо SE, яку я помічав у багатьох питаннях, є постійним аргументом, що C ++ є швидшим та / або ефективнішим, ніж мови вищого рівня, як Java. Контр-аргумент полягає в тому, що сучасний JVM або CLR може бути настільки ж ефективний завдяки JIT і так далі для зростаючої кількості завдань, і що C ++ стає завжди більш ефективним, якщо ви знаєте, чим займаєтесь і чому робите речі певним чином заслужить підвищення продуктивності. Це очевидно і має ідеальний сенс.

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

Коли я намагаюся дослідити цю тему, все, що я знаходжу, - це ті ж самі аргументи, які я виклав вище, без будь-якої детальної інформації щодо розуміння того, як саме C ++ можна використовувати для високоефективних обчислень.


Продуктивність залежить також від складності програми.
панду

23
Я додам, що "C ++ - це завжди ефективніше, якщо ви знаєте, що ви робите, і чому робити певний спосіб, це заслуговує підвищення продуктивності". кажучи, що це не лише питання знання, це питання часу розробника. Не завжди ефективно оптимізувати оптимізацію. Ось чому існують мови вищого рівня, такі як Java та Python (серед інших причин) - щоб зменшити кількість часу, який програміст повинен витратити на програмування, щоб виконати задану задачу за рахунок високо налаштованої оптимізації.
Джоел Корнетт

4
@ Джоел Корнетт: Я повністю згоден. Я, безумовно, більш продуктивний на Java, ніж у C ++, і вважаю C ++ лише тоді, коли мені потрібно написати дійсно швидкий код. З іншого боку, я бачив, що погано написаний код C ++ дуже повільний: C ++ менш корисний для рук некваліфікованих програмістів.
Джорджіо

3
Будь-який вихід компіляції, який може бути створений за допомогою JIT, може бути створений C ++, але код, який може створити C ++, не обов'язково повинен бути створений JIT. Таким чином, можливості та характеристики продуктивності C ++ є сукупністю можливостей будь-якої мови вищого рівня. QED
tylerl

1
@Doval Технічно вірно, але, як правило, ви можете порахувати можливі фактори виконання, що впливають на продуктивність програми з одного боку. Зазвичай без використання більше двох пальців. Тож найгірший випадок доставляє кілька бінарних файлів ... крім того, виявляється, що вам навіть не потрібно цього робити, оскільки потенційна швидкість є незначною, тому ніхто навіть не турбує.
tylerl

Відповіді:


200

Вся справа в пам’яті (не в JIT). «Перевага JIT перед C» здебільшого обмежується оптимізацією віртуальних або невіртуальних викликів за допомогою вбудованого інтерфейсу - те, що CPT BTB вже наполегливо працює.

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

  • Для кожного об'єкта є накладні витрати на принаймні 8 байт, і використання об'єктів замість примітивів потрібно або бажано в багатьох місцях (а саме стандартних колекціях).
  • Рядки складаються з двох об'єктів і мають накладні витрати в 38 байт
  • UTF-16 використовується внутрішньо, що означає, що кожному символу ASCII потрібно два байти замість одного (Oracle JVM нещодавно ввів оптимізацію, щоб уникнути цього для чистих рядків ASCII).
  • Не існує сукупного опорного типу (тобто структури), і, в свою чергу, немає масивів сукупних посилальних типів. Об'єкт Java або масив об'єктів Java має дуже погану локальність кешу L1 / L2 порівняно з C-структурами та масивами.
  • Java-дженерики використовують стирання типу, яке має погану локалізацію кешу в порівнянні з типом даних.
  • Розподіл об’єктів непрозорий і повинен здійснюватися окремо для кожного об’єкта, тому програма не може навмисно викладати свої дані кешованим способом і все ще трактувати їх як структуровані дані.

Деякі інші фактори, пов'язані з пам'яттю, але не є кешем:

  • Розподілу стеків немає, тому всі непримітивні дані, з якими ви працюєте, повинні знаходитись у купі та проходити збір сміття (деякі останні JIT в деяких випадках розподіляють стеки за кадром).
  • Оскільки немає сукупних еталонних типів, не існує проходження стека сукупних типів посилань. (Подумайте про ефективне передавання векторних аргументів)
  • Збір сміття може завдати шкоди вмісту кешу L1 / L2, а паузи GC, що зупиняються у світі, шкодять інтерактивності.
  • Перетворення між типами даних завжди вимагає копіювання; ви не можете взяти вказівник на купу байтів, отриманих з сокета, і інтерпретувати їх як плавці.

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

Варто зазначити, що багато з цих компромісів для Java / JVM дуже відрізняються, ніж для C # / CIL. .NET CIL має структури еталонного типу, розподіл / передачу стека, запаковані масиви структур та типові інстанційні дженерики.


37
+1 - загалом, це хороша відповідь. Однак я не впевнений, що точка кулі "немає розподілу стека" є абсолютно точною. Java JIT часто роблять аналіз евакуації, щоб дозволити розподіл стеків там, де це можливо - можливо, ви повинні сказати, що мова Java не дозволяє програмісту вирішувати, коли об'єкт розподіляється стеком порівняно з купою. Крім того, якщо використовується генераційний сміттєзбірник (який використовують усі сучасні JVM), "розподіл купи" означає зовсім іншу річ (з абсолютно іншими характеристиками продуктивності), ніж це в середовищі C ++.
Даніель Приден

5
Я думаю, що є ще дві речі, але я здебільшого працюю з речами на набагато вищому рівні, тому скажіть, чи я помиляюся. Насправді не можна писати на C ++, не розвиваючи більш загальної обізнаності про те, що насправді відбувається в пам'яті та як машинний код насправді працює, тоді як сценарії або мови віртуальної машини абстрагують все, що не стосується вашої уваги. Ви також маєте набагато більш тонкий контроль над тим, як все працює, тоді як у віртуальній машині чи інтерпретованій мові ви покладаєтесь на те, що основні автори бібліотеки, можливо, оптимізували для надто конкретного сценарію.
Ерік Реппен

18
+1. Ще одне, що я додам (але я не бажаю надсилати нову відповідь за): індексація масиву на Java завжди включає перевірку меж. З C і C ++ це не так.
riwalk

7
Варто зазначити, що розподіл купи Java значно швидше, ніж наївна версія з C ++ (за рахунок внутрішнього об'єднання та інших речей), але розподіл пам'яті в C ++ може бути значно кращим, якщо ви знаєте, що ви робите.
Брендан Лонг

10
@BrendanLong, правда .. але тільки якщо пам'ять чиста - як тільки програма працює на деякий час, розподіл пам’яті буде повільнішим через необхідність GC, який різко сповільнює роботу, як це має звільнити пам'ять, запустити фіналізатори, а потім компактний. Його торгівля, яка приносить користь орієнтирам, але (IMHO) загалом сповільнює програми.
gbjbaanb

67

Це просто тому, що C ++ компілюється в код складання / машини, тоді як Java / C # все ще мають накладні витрати на компіляцію JIT під час виконання?

Частково, але загалом, якщо припустити абсолютно фантастичний сучасний компілятор JIT, належний код C ++ все ще має кращі результати, ніж код Java з ДВОХ основних причин:

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

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

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

Однією з областей, де C ++ може відставати від Java, є будь-яка ситуація, коли на купі потрібно виділити багато невеликих об’єктів. У цьому випадку система збору сміття Java, ймовірно, призведе до кращої продуктивності, ніж стандартна newта deleteв C ++, тому що Java GC дозволяє проводити масове розселення. Але знову ж таки, програміст на C ++ може компенсувати це за допомогою пулу пам’яті або розподільника плат, тоді як програміст Java не звертається, коли стикається з шаблоном розподілу пам’яті, для якого час роботи Java не оптимізовано.

Також дивіться цю чудову відповідь для отримання додаткової інформації про цю тему.


6
Хороша відповідь, але один незначний момент: "C ++ шаблони дають вам обоє (ціною більш тривалих разів компіляції.)" Я також додам ціною більшого розміру програми. Це не завжди може бути проблемою, але якщо розробляється для мобільних пристроїв, це безумовно може бути.
Лев

9
@luiscubal: ні, в цьому відношенні C # generics дуже схожі на Java (оскільки той самий "загальний" шлях коду береться незалежно від того, через які типи проходити.) Примітка до шаблонів C ++ полягає в тому, що код інстанцірується один раз для кожен тип, до якого він застосовується. Таким чином std::vector<int>, це динамічний масив, призначений саме для ints, і компілятор може його оптимізувати відповідно. AC # List<int>все ще просто a List.
jalf

12
@jalf C # List<int>використовує int[]не так, Object[]як робить Java. Див stackoverflow.com/questions/116988 / ...
luiscubal

5
@luiscubal: ваша термінологія незрозуміла. JIT не діє, якщо я вважаю "час збирання". Ви маєте рацію, звичайно, враховуючи достатньо розумний та агресивний компілятор JIT, фактично немає меж того, що він може зробити. Але C ++ вимагає такої поведінки. Крім того, шаблони C ++ дозволяють програмісту задавати явні спеціалізації, дозволяючи додаткові явні оптимізації, де це можливо. C # не має для цього еквівалента. Наприклад, в C ++ я міг визначити, vector<N>де, для конкретного випадку vector<4>, слід використовувати мою кодовану вручну реалізацію SIMD
jalf

5
@Leo: Розповсюдження коду через шаблони було проблемою 15 років тому. Завдяки великій шаблонізації та вбудовування, а також компіляторам здібностей, набраних з тих пір (як складання однакових екземплярів), в даний час багато кодів зменшується за допомогою шаблонів.
sbi

46

Інші відповіді (6 поки що), здається, забули згадати, але те, що я вважаю дуже важливим для відповіді на це, - це одна з найосновніших філософій дизайну C ++, яку сформулював і використовував Stroustrup з першого дня:

Ви не платите за те, що не використовуєте.

Існують ще деякі важливі основні принципи дизайну, які сильно формують C ++ (як, наприклад, вас не варто примушувати до конкретної парадигми), але ви не платите за те, що не використовуєте , саме там є найважливішим.


У своїй книзі «Дизайн та еволюція C ++» (зазвичай її називають [D&E]), Stroustrup описує те, що йому потрібно, що спонукало його спершу придумати C ++. З моїх власних слів: Для своєї докторської дисертації (щось стосується мережевого моделювання, IIRC) він впровадив систему в SIMULA, яка йому дуже сподобалась, оскільки мова була дуже хорошою, дозволяючи йому висловлювати свої думки безпосередньо в коді. Однак отримана програма пройшла занадто повільно, і щоб отримати ступінь, він переписав річ у BCPL, попереднику C. Написання коду в BCPL він характеризує як біль, але отримана програма була достатньо швидкою для доставки результати, які дозволили йому закінчити докторську ступінь.

Після цього він захотів мову, яка дозволить якомога прямо перекладати проблеми реального світу в код, але також дозволить коду бути дуже ефективним.
Займаючись цим, він створив те, що згодом стане C ++.


Таким чином, мета цитованій вище не тільки один з декількох фундаментальних основоположного принципу проектування, це дуже близько до Raison d'être для C ++. І його можна знайти майже всюди на мові: Функції є лише virtualтоді, коли ви їх хочете (оскільки виклик віртуальних функцій має невеликі накладні витрати) POD-адреси ініціалізуються автоматично автоматично, коли ви явно вимагаєте цього, винятки лише коштують вашої продуктивності, коли ви насправді кидайте їх (тоді як це було чіткою метою дизайну, щоб дозволити налаштування / очищення стекфреймів бути дуже дешевими), без роботи GC, коли це відчувається, і т.д.

C ++ явно вирішив не надавати вам певних зручностей ("чи повинен я зробити цей метод віртуальним тут?") В обмін на продуктивність ("ні, я цього не роблю, і тепер компілятор може inlineце зробити і оптимізувати хек з вся справа! "), і, що не дивно, це справді призвело до підвищення продуктивності порівняно з зручнішими мовами.


4
Ви не платите за те, що не використовуєте. =>, а потім додали RTTI :(
Матьє М.

11
@Matthieu: Хоча я розумію ваші настрої, я не можу не помітити, що навіть це було додано з обережністю щодо продуктивності. RTTI визначено так, що він може бути реалізований за допомогою віртуальних таблиць, і, таким чином, додається дуже мало накладних витрат, якщо ви не використовуєте його. Якщо ви не використовуєте поліморфізм, це взагалі не коштує. Я щось пропускаю?
sbi

9
@Matthieu: Звичайно, є причина. Але чи раціональна ця причина? Як я бачу, "вартість RTTI", якщо вона не використовується, є додатковим покажчиком у віртуальній таблиці кожного поліморфного класу, що вказує на якийсь об'єкт RTTI, статично розподілений десь. Якщо ви не хочете запрограмувати чіп у моєму тостері, як це могло бути актуальним?
sbi

4
@Aaronaught: Я в збитку щодо того, що на це відповісти. Ви справді просто відхилили мою відповідь, оскільки вона вказує на основоположну філософію, яка змусила Stroustrup et al додати функції таким чином, що дозволяє зробити продуктивність, а не перераховувати ці способи та особливості окремо?
sbi

9
@Aaronaught: У вас є моя симпатія.
sbi

29

Чи знаєте ви науково-дослідний документ Google на цю тему?

З висновку:

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

Це хоча б частково пояснення, в сенсі "тому, що компілятори реального світу C ++ виробляють швидший код, ніж компілятори Java шляхом емпіричних заходів".


4
Окрім відмінностей у використанні пам’яті та кешу, однією з найважливіших є кількість проведеної оптимізації. Порівняйте, скільки оптимізацій GCC / LLVM (і, мабуть, Visual C ++ / ICC) роблять відносно компілятора Java HotSpot: набагато більше, особливо щодо циклів, усунення зайвих гілок та розподілу регістрів. У компіляторів JIT зазвичай немає часу для цих агресивних оптимізацій, навіть думали, що вони можуть їх краще реалізувати, використовуючи доступну інформацію про час виконання.
Граціан Луп

2
@GratianLup: Цікаво, чи це (все-таки) правда з LTO.
Дедуплікатор

2
@GratianLup: Давайте не будемо забувати керовану профілем оптимізацію для C ++ ...
Deduplicator

23

Це не дублікат ваших запитань, але прийнята відповідь відповідає на більшість ваших запитань: Сучасний огляд Java

Підсумовуючи:

Принципово семантика Java диктує, що мова йде повільніше, ніж C ++.

Отже, залежно від того, з якою іншою мовою ви порівнюєте C ++, ви можете отримати чи не таку ж відповідь.

У C ++ у вас є:

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

Це особливості або побічні ефекти визначення мови, які роблять його теоретично більш ефективним у пам’яті та швидкості, ніж будь-яка мова, що:

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

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

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


7

Йдеться головним чином про пам’ять (як сказав Майкл Боргвардт) з додаванням трохи неефективності JIT.

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

Це можна побачити в роботі, просто замінивши будь-які об’єкти StringBuilder на String ... Stringbuilders працюють, попередньо розподіливши пам'ять і заповнивши її, і є відомим трюком продуктивності для систем java / .NET.

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

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

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

Існує більше ... як завантаження збірок із GAC, що вимагає перевірки безпеки, шляхи зондування (увімкніть sxstrace і просто подивіться, що це відбувається!) Та загальне інше переобладнання, яке, схоже, набагато популярніше у java / .net ніж C / C ++.


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

3
@MichaelBorgwardt типу? Я кажу "GC працює регулярно" і "це ущільнює купу". Решта моєї відповіді стосується того, як структури даних програм використовують пам'ять.
gbjbaanb

6

"Це просто тому, що C ++ компілюється в код складання / машини, тоді як Java / C # все ще мають накладні витрати на компіляцію JIT під час виконання?" В основному, так!

Хоча швидка примітка, у Java є більше накладних витрат, ніж просто компіляція JIT. Наприклад, він робить для вас набагато більше перевірки (як це робить такі речі, як ArrayIndexOutOfBoundsExceptionsі NullPointerExceptions). Сміттєзбірник - ще одна значна витрата.

Там досить докладний порівняння тут .


2

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

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

Припустимо, що у нас є програма, написана якоюсь мовою X, і ми можемо її скласти за допомогою власного компілятора та знову за допомогою компілятора JIT. Кожен робочий потік має однакові етапи, які можна узагальнити (Код -> Проміжне представництво -> Машинний код -> Виконання). Велика різниця між двома полягає в тому, які етапи бачить користувач і які бачить програміст. При нативної компіляції програміст бачить усі, крім етапу виконання, але за допомогою рішення JIT, компіляцію до машинного коду бачить користувач, крім виконання.

Твердження, що A швидше B , посилається на час, необхідний для запуску програми, як це бачить користувач . Якщо припустити, що обидва фрагменти коду виконують однаково на етапі виконання, ми повинні припустити, що робочий потік JIT уповільнюється для користувача, оскільки він також повинен бачити час T компіляції до машинного коду, де T> 0. Отже , для будь-якої можливості робочого потоку JIT виконувати так само, як і власний робочий потік, користувачеві, ми повинні скоротити час виконання коду, таким чином, щоб Виконання + Компіляція до машинного коду були нижчими лише на етапі виконання рідного робочого потоку. Це означає, що ми повинні оптимізувати код краще у компіляції JIT, ніж у власній компіляції.

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

Я буду використовувати приклад: Реєструвати розподіл. Оскільки доступ до пам’яті в тисячі разів повільніше, ніж доступ до реєстрації, ми в ідеалі хочемо використовувати регістри, де це можливо, і маємо якомога менше доступу до пам'яті, але у нас є обмежена кількість регістрів, і ми повинні перелити стан у пам'ять, коли нам це потрібно реєстр. Якщо ми використовуємо алгоритм розподілу реєстру, який займає 200 мс для обчислення, і в результаті ми економимо 2 мс часу виконання - ми не використовуємо найкраще час для компілятора JIT. Такі рішення, як алгоритм Chaitin, який може створювати високооптимізований код, є непридатними.

Роль компілятора JIT полягає в тому, щоб досягти найкращого балансу між часом компіляції та якістю виробленого коду, однак із великим ухилом щодо швидкого часу компіляції, оскільки ви не хочете залишати користувача в очікуванні. Продуктивність виконуваного коду у випадку JIT повільніше, оскільки власний компілятор не обмежений (значно) часом оптимізації коду, тому вільний у використанні найкращі алгоритми. Можливість того, що загальна компіляція + виконання для компілятора JIT може бити лише час виконання для власне складеного коду, фактично 0.

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

Однак у цьому аргументі є проблема - він передбачає, що оптимізація, орієнтована на профіль, тощо - це щось унікальне для віртуальних машин, що не відповідає дійсності. Ми також можемо застосувати його до нашої компіляції - склавши нашу програму з увімкненим профілюванням, записуючи інформацію, а потім перекомпілюйте програму з цим профілем. Напевно, також варто зазначити, що гаряча заміна коду - це не те, що може робити лише компілятор JIT, ми можемо це зробити за власним кодом - хоча рішення на базі JIT для цього є більш доступними та набагато простішими для розробника. Отже, велике питання: Чи може VM запропонувати нам певну інформацію, яку не може зробити нативна компіляція, що може підвищити ефективність нашого коду?

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

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


0

Ця стаття - це підсумок набору публікацій в блозі, які намагаються порівняти швидкість c ++ проти c # та проблеми, які вам доведеться подолати в обох мовах, щоб отримати високу ефективність коду. Підсумок - "Бібліотека має значення більше за все, але якщо ви перебуваєте в c ++, ви можете це подолати". або "сучасні мови мають кращі бібліотеки, і тому ви отримуєте швидші результати з меншими зусиллями" залежно від вашого філософського нахилу.


0

Я думаю, що справжнє питання тут не в тому, "що швидше?" але "який має найкращий потенціал для підвищення продуктивності?". Якщо дивитися на ці умови, C ++ явно виграє - він компілюється в початковий код, немає JITting, це нижчий рівень абстракції тощо.

Це далеко не повна історія.

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

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

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


3
"оптимізація компілятора, яка підходить для однієї машини, може бути зовсім неправильною для іншої" Ну, це не дуже винно в мові. Справді критичний для продуктивності код може бути складений окремо для кожної машини, на якій він буде працювати, що не вимагає, якщо ви компілюєте локально з source ( -march=native). - "це нижчий рівень абстракції" насправді не відповідає дійсності. C ++ використовує такі ж абстракції високого рівня, як і Java (або, власне, вищі: функціональне програмування - метапрограмування шаблонів?), Він просто реалізує абстракції менш "чисто", ніж робить Java.
близько

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

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

1
Хоча теоретично так, JIT може натягнути більше хитрощів, ніж статичний компілятор (для .NET принаймні, ява також не знаю), він насправді нічого з цього не робить. Нещодавно я зробив купу розбирання коду .NET JIT, і існують всілякі оптимізації, такі як піднімання коду з циклів, усунення мертвого коду тощо, чого .NET JIT просто не робить. Я б хотів, але так, але ей, команда Windows у мікрософті протягом багатьох років намагається вбити .NET, тому я не затримую дихання
Orion Edwards

-1

Складання JIT фактично негативно впливає на продуктивність. Якщо ви розробляєте "досконалий" компілятор і "досконалий" компілятор JIT, перший варіант завжди виграє в продуктивності.

І Java, і C # інтерпретуються на проміжні мови, а потім компілюються в початковий код під час виконання, що знижує продуктивність.

Але тепер різниця не настільки очевидна для C #: Microsoft CLR виробляє різний нативний код для різних процесорів, тим самим робить код більш ефективним для роботи машини, що не завжди робиться компіляторами C ++.

PS C # написано дуже легко і не має багатьох абстракційних шарів. Це не вірно для Java, яка не настільки ефективна. Так, у цьому випадку програми C # зі своїм найсильнішим CLR часто демонструють кращу продуктивність, ніж програми C ++. Докладніше про .Net та CLR подивіться на "CLR через C #" Джефрі Ріхтера .


8
Якщо JIT насправді негативно вплинув на продуктивність, то, звичайно, він не використовувався б?
Захід

2
@Zavior - Я не можу придумати гарну відповідь на ваше запитання, але я не бачу, як JIT не може додати додаткових накладних витрат - JIT - це додатковий процес, який потрібно завершити під час виконання, який вимагає ресурсів, які не є ' t витрачається на виконання самої програми, тоді як повністю складена мова "готова до запуску".
Анонім

3
JIT позитивно впливає на продуктивність, а не негативно, якщо ви ставите його в контекст - Це компіляція байтового коду в машинний код перед його запуском. Результати також можуть бути кешовані, що дозволяє йому працювати швидше, ніж еквівалентний байт-код, який інтерпретується.
Кейсі Кубалл

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

4
@Tdammmers, насправді є і компонент продуктивності. Дивіться java.sun.com/products/hotspot/whitepaper.html . Оптимізація може включати в себе такі елементи, як динамічні корективи для поліпшення прогнозування гілок та звернень до кешу, динамічне вбудовування, девіртуалізація, відключення перевірки меж та розкручування циклу. Твердження полягає в тому, що у багатьох випадках вони можуть більше, ніж платити за вартість JIT.
Чарльз Е. Грант
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.