Что подтверждает утверждение, что 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» в основном ограничивается оптимизацией виртуальных или не виртуальных вызовов с помощью встраивания, то, что BTB CPU уже усердно работает.

В современных машинах доступ к ОЗУ очень медленный (по сравнению с тем, что делает ЦП), что означает, что приложения, которые используют кеши в максимально возможной степени (что легче, когда используется меньше памяти), могут быть в сто раз быстрее, чем те, нет. И есть много способов, которыми Java использует больше памяти, чем C ++, и затрудняет написание приложений, которые полностью используют кеш:

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

Некоторые другие факторы, связанные с памятью, но не связанные с кэшем:

  • Выделение стека отсутствует, поэтому все не примитивные данные, с которыми вы работаете, должны находиться в куче и проходить сборку мусора (в некоторых случаях в некоторых недавних JIT распределение стеков происходит за кулисами).
  • Поскольку нет агрегатных ссылочных типов, нет передачи в стеке агрегатных ссылочных типов. (Подумайте, как эффективно передавать аргументы Vector)
  • Сборка мусора может повредить содержимое кэша 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, который сильно замедляет работу, поскольку он должен освободить память, запустить финализаторы и затем компактный. Это компромисс, который приносит пользу тестам, но (ИМХО) в целом замедляет работу приложений.
gbjbaanb
67

Это просто потому, что C ++ скомпилирован в ассемблер / машинный код, в то время как Java / C # все еще имеют накладные расходы на обработку JIT-компиляции во время выполнения?

Частично, но в целом, при условии совершенно фантастического современного JIT-компилятора, надлежащий код C ++ по- прежнему имеет тенденцию работать лучше, чем код Java, по двум основным причинам:

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

В целом, большинство языков, включая 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 # очень похожи на Java (в том смысле, что один и тот же «универсальный» путь к коду берется независимо от того, через какие типы передаются). Хитрость в шаблонах C ++ заключается в том, что код создается один раз для каждый тип, к которому он применяется. Таким образом , std::vector<int>динамический массив предназначен только для целых чисел, и компилятор может оптимизировать его соответствующим образом . AC # List<int>по-прежнему просто List.
Джалф
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>, должна использоваться
моя
5
@Leo: распространение кода через шаблоны было проблемой 15 лет назад. С тяжелыми шаблонизацией и встраиванием, а также с тех пор, как компиляторы возможностей подобраны (например, сворачивание идентичных экземпляров), в наши дни много кода становится меньше с помощью шаблонов.
Sbi
46

То, что другие ответы (до сих пор 6), кажется, забыли упомянуть, но то, что я считаю очень важным для ответа на этот вопрос, является одной из самых базовых философий проектирования C ++, которая была сформулирована и использована Страуструпом со дня №1:

Вы не платите за то, что не используете.

Существуют и другие важные принципы проектирования, которые в значительной степени сформировали C ++ (например, вам не следует навязывать какую-то конкретную парадигму), но вы не платите за то, что вы не используете , прямо среди самых важных.


В своей книге «Дизайн и развитие C ++» (обычно называемой [D & E]), Страуструп описывает, в чем он нуждался, что заставило его в первую очередь придумать C ++. Мои собственные слова: для своей кандидатской диссертации (что-то связанное с сетевым моделированием, IIRC) он внедрил систему в SIMULA, которая ему очень понравилась, потому что язык очень хорошо позволял ему выражать свои мысли непосредственно в коде. Тем не менее, результирующая программа работала слишком медленно, и чтобы получить степень, он переписал эту вещь на BCPL, предшественнике языка C. Написание кода на BCPL, который он описывает как боль, но получающаяся программа была достаточно быстрой, чтобы доставить ее. результаты, которые позволили ему закончить докторскую диссертацию.

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


Таким образом, указанная выше цель - не просто один из нескольких фундаментальных принципов проектирования, она очень близка смыслу C ++. И это можно найти практически повсюду в языке: функции доступны только virtualтогда, когда вы хотите, чтобы они (потому что вызов виртуальных функций сопряжен с небольшими издержками), POD инициализируются автоматически только при явном запросе, исключения только снижают производительность, когда вы на самом деле выбросить их (тогда как это было явной целью разработки, чтобы позволить установку / очистку стековых фреймов быть очень дешевой), не запускать сборщик мусора, когда захочется, и т. д.

C ++ явно решил не дать вам некоторые удобства ( «я должен сделать этот виртуальный метод здесь?») В обмен на исполнение ( "нет, у меня нет, и теперь компилятор может inlineэто и оптимизировать черт из все дело! "), и, что неудивительно, это действительно привело к повышению производительности по сравнению с более удобными языками.

SBI
источник
4
Вы не платите за то, что не используете. => а потом они добавили RTTI :(
Матье М.
11
@Matthieu: Хотя я понимаю ваши чувства, я не могу не заметить, что даже это было добавлено с осторожностью в отношении производительности. RTTI указывается таким образом, чтобы его можно было реализовать с использованием виртуальных таблиц, и, следовательно, он добавляет очень мало накладных расходов, если вы его не используете. Если вы не используете полиморфизм, это совершенно бесплатно. Я что-то пропустил?
2012 г.,
9
@Matthieu: Конечно, есть причина. Но разумна ли эта причина? Из того, что я вижу, «стоимость RTTI», если она не используется, является дополнительным указателем в виртуальной таблице каждого полиморфного класса, указывающим на некоторый объект RTTI, статически размещенный где-то. Если вы не хотите запрограммировать чип в моем тостере, как это может быть актуально?
2012 г.
4
@Aaronaught: Я не знаю, что ответить на это. Вы действительно просто отклонили мой ответ, потому что он указывает на основополагающую философию, которая заставила Страуструпа и его коллег добавить функции таким образом, чтобы обеспечить производительность, а не перечислять эти способы и функции по отдельности?
СБИ
9
@Aaronaught: Вы имеете мое сочувствие.
12:30
29

Знаете ли вы исследовательский документ Google по этой теме?

Из заключения:

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

Это, по крайней мере, частичное объяснение, в смысле «потому что компиляторы реального мира C ++ производят более быстрый код, чем компиляторы Java с помощью эмпирических мер».

Док Браун
источник
4
Помимо различий в использовании памяти и кэша, одним из наиболее важных является количество выполненных оптимизаций. Сравните, сколько оптимизаций GCC / LLVM (и, вероятно, Visual C ++ / ICC) выполняют относительно компилятора Java HotSpot: гораздо больше, особенно в отношении циклов, устранения избыточных ветвей и распределения регистров. JIT-компиляторам обычно не хватает времени для этих агрессивных оптимизаций, даже если они думают, что могли бы реализовать их лучше, используя доступную информацию времени выполнения.
Грациан Луп
2
@GratianLup: Интересно, правда ли это (все еще) с LTO?
Дедупликатор
2
@GratianLup: давайте не будем забывать оптимизацию по профилю для C ++ ...
Дедупликатор
23

Это не дубликат ваших вопросов, но принятый ответ отвечает на большинство ваших вопросов: современный обзор Java

Подводить итоги:

По сути, семантика Java диктует, что это более медленный язык, чем C ++.

Таким образом, в зависимости от того, с каким другим языком вы сравниваете C ++, вы можете получить или не получить тот же ответ.

В C ++ у вас есть:

  • Возможность делать умные врезки,
  • генерация общего кода с сильной локальностью (шаблоны)
  • как можно меньше и компактнее данных
  • возможности избежать косвенных
  • предсказуемое поведение памяти
  • Оптимизация компилятора возможна только из-за использования абстракций высокого уровня (шаблонов)

Это особенности или побочные эффекты определения языка, которые теоретически делают его более эффективным в отношении памяти и скорости, чем любой язык, который:

  • массово использовать косвенное обращение (языки «все - управляемая ссылка / указатель»): косвенное обращение означает, что ЦПУ приходится перепрыгивать в память, чтобы получить необходимые данные, увеличивая сбои в кеше ЦП, что означает замедление обработки - C использует также косвенные выражения a много, даже если он может иметь небольшие данные как C ++;
  • генерировать объекты большого размера, к элементам которых обращаются косвенно: это является следствием наличия ссылок по умолчанию, члены являются указателями, поэтому при получении элемента вы можете не получить данные, близкие к ядру родительского объекта, что снова вызывает пропуски кэша.
  • используйте сборщик мусора: он просто делает предсказуемость производительности невозможной (по замыслу).

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

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

Klaim
источник
7

В основном речь идет о памяти (как сказал Майкл Боргвардт) с добавлением немного неэффективности JIT.

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

Вы можете увидеть это в действии, просто заменив любые объекты StringBuilder на String ... Stringbuilders работают, предварительно выделяя память и заполняя ее, и это известный прием производительности для систем java / .NET.

Не забывайте, что парадигма «удалять старые и выделять новые копии» очень интенсивно используется в Java / C #, просто потому, что людям говорят, что благодаря GC выделение памяти происходит очень быстро, и поэтому модель рассеянной памяти используется повсеместно ( кроме строителей строк, конечно), поэтому все ваши библиотеки, как правило, тратят впустую память и используют ее много, ни одна из которых не получает преимущества от смежности. Обвините в этом шумиху вокруг GC - они сказали, что память свободна, смеется.

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

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

Это еще не все, например загрузка сборок из GAC, требующая проверки безопасности, проверки путей (включите sxstrace и просто посмотрите, к чему это приводит!), А также другие общие разработки, которые, по-видимому, гораздо более популярны в java / .net. чем C / C ++.

gbjbaanb
источник
2
Многие вещи, которые вы пишете, не соответствуют действительности для современных сборщиков мусора.
Майкл Боргвардт
3
@MichaelBorgwardt, например? Я говорю «GC работает регулярно» и «он сжимает кучу». Остальная часть моего ответа касается того, как структуры данных приложения используют память.
gbjbaanb
6

«Это просто потому, что C ++ скомпилирован в ассемблерный / машинный код, в то время как Java / C # все еще имеют накладные расходы на компиляцию JIT во время выполнения? В основном да!

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

Там довольно подробное сравнение здесь .

vaughandroid
источник
2

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

Когда мы утверждаем, что нативный код работает быстрее, мы говорим о типичном случае использования нативно скомпилированного кода в сравнении с JIT-скомпилированным кодом, когда типичное использование JIT-скомпилированного приложения должно выполняться пользователем с немедленными результатами (например, нет сначала жду компилятора). В этом случае, я не думаю, что кто-то может с уверенностью заявить, что JIT-скомпилированный код может соответствовать или превосходить нативный код.

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

Заявление о том, что A быстрее, чем B , относится к времени, затраченному на выполнение программы, как это видит пользователь . Если мы предположим, что обе части кода работают одинаково на этапе выполнения, мы должны предположить, что рабочий процесс JIT медленнее для пользователя, поскольку он также должен видеть время T компиляции в машинный код, где T> 0. Так что чтобы любая возможность рабочего процесса JIT выполнять то же, что и собственный рабочий процесс, для пользователя, мы должны уменьшить время выполнения кода, чтобы выполнение + компиляция в машинный код было меньше, чем только этап выполнения родного рабочего потока. Это означает, что мы должны оптимизировать код лучше в JIT-компиляции, чем в нативной компиляции.

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

Я буду использовать пример: распределение регистра. Поскольку доступ к памяти в несколько тысяч раз медленнее, чем доступ к регистрам, в идеале мы хотим использовать регистры везде, где это возможно, и иметь как можно меньше обращений к памяти, но у нас ограниченное количество регистров, и мы должны выливать состояние в память, когда нам нужно регистр. Если мы используем алгоритм распределения регистров, который требует 200 мс для вычисления, и в результате мы экономим 2 мс времени выполнения - мы не будем оптимально использовать время для JIT-компилятора. Такие решения, как алгоритм Чейтина, который может генерировать высоко оптимизированный код, не подходят.

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

Но наши виртуальные машины не ограничиваются компиляцией JIT. Они используют опережающие методы компиляции, кэширование, горячую замену и адаптивную оптимизацию. Итак, давайте изменим наше утверждение, что производительность - это то, что видит пользователь, и ограничим его временем, затрачиваемым на выполнение программы (предположим, что мы скомпилировали AOT). Мы можем эффективно сделать исполняемый код эквивалентным нативному компилятору (или, может быть, лучше?). Большим преимуществом для виртуальных машин является то, что они могут создавать код более высокого качества, чем собственный компилятор, поскольку он имеет доступ к большему количеству информации - информации о выполняющемся процессе, например о том, как часто может выполняться определенная функция. Затем виртуальная машина может применить адаптивную оптимизацию к наиболее важному коду с помощью горячей замены.

Однако есть проблема с этим аргументом - он предполагает, что оптимизация на основе профилей и тому подобное является чем-то уникальным для виртуальных машин, что не соответствует действительности. Мы также можем применить его к собственной компиляции - скомпилировав наше приложение с включенным профилированием, записав информацию, а затем перекомпилировав приложение с этим профилем. Вероятно, также стоит отметить, что горячая замена кода - это не то, что может сделать только JIT-компилятор, мы можем сделать это для собственного кода - хотя решения на основе JIT для этого более доступны и намного проще для разработчика. Итак, главный вопрос: может ли виртуальная машина предложить нам некоторую информацию, которую не может дать нативная компиляция, которая может повысить производительность нашего кода?

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

Программист C ++ может утверждать, что ему нужны оптимизации с самого начала, и он не должен ждать, пока виртуальная машина решит, как это сделать, если вообще будет. Это, вероятно, справедливо для нашей современной технологии, поскольку текущий уровень оптимизации в наших виртуальных машинах уступает тому, что могут предложить нативные компиляторы, но это может не всегда иметь место в случае улучшения решений AOT в наших виртуальных машинах и т. Д.

Марк Н
источник
0

Эта статья представляет собой краткое изложение постов в блоге, в которых сравнивается скорость c ++ и c #, а также проблемы, которые необходимо решить на обоих языках, чтобы получить высокопроизводительный код. Суть в том, что «ваша библиотека важнее всего, но если вы находитесь на c ++, вы можете преодолеть это». или «современные языки имеют лучшие библиотеки и, следовательно, получают более быстрые результаты с меньшими усилиями» в зависимости от вашего философского уклона.

Джефф Гейтс
источник
0

Я думаю, что реальный вопрос здесь не в том, "что быстрее?" но "у которого есть лучший потенциал для более высокой производительности?" С учетом этих терминов C ++ явно выигрывает - он скомпилирован в нативный код, нет JITting, это более низкий уровень абстракции и т. Д.

Это далеко от полной истории.

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

С другой стороны, программа JITted будет оптимизировать во время JIT, поэтому она может использовать некоторые приемы, которые не может скомпилированная программа, и может выполнять очень специфические оптимизации для машины, на которой она работает, и кода, на котором она работает. Как только вы преодолеете начальные издержки JIT, в некоторых случаях он может быть быстрее.

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

Максимус Минимус
источник
3
«Оптимизация компилятора, подходящая для одной машины, может совершенно не подходить для другой». Ну, это не совсем вина в языке. Действительно, критичный к производительности код может быть скомпилирован отдельно для каждой машины, на которой он будет работать, что не составляет никакого труда, если вы компилируете локально из source ( -march=native). - «это более низкий уровень абстракции» не совсем верно. C ++ использует такие же высокоуровневые абстракции, как и Java (или, фактически, более высокие: функциональное программирование, «метапрограммирование шаблонов»), он просто реализует абстракции менее «чисто», чем Java.
оставлено около
«По-настоящему критичный к производительности код может быть скомпилирован отдельно для каждой машины, на которой он будет работать, что не составляет никакого труда, если вы компилируете локально из исходного кода» - это не удается из-за базового предположения, что конечный пользователь также является программистом.
Максимус Минимус
Не обязательно конечный пользователь, просто человек, ответственный за установку программы. На рабочем столе и мобильных устройства, которые , как правило , является конечным пользователем, но это не единственными приложений есть, конечно , не наиболее критичные из них. И вам не нужно быть программистом, чтобы создавать программу из исходного кода, если она правильно написала сценарии сборки, как это делают все хорошие бесплатные / открытые программные проекты.
оставлено около
1
Хотя в теории да, JIT может использовать больше трюков, чем статический компилятор, на практике (по крайней мере, для .NET я также не знаю java), на самом деле он этого не делает. Недавно я выполнил кучу дизассемблирования JIT-кода .NET, и есть все виды оптимизаций, такие как удаление кода из циклов, удаление мертвого кода и т. Д., Которые просто не делает .NET JIT. Я бы хотел, но эй, команда Windows из Microsoft пытается убить .NET уже много лет, поэтому я не затаив дыхание
Орион Эдвардс
-1

JIT-компиляция фактически отрицательно влияет на производительность. Если вы разрабатываете «идеальный» компилятор и «идеальный» JIT-компилятор, первый вариант всегда выигрывает в производительности.

И Java, и C # интерпретируются на промежуточные языки, а затем компилируются в собственный код во время выполнения, что снижает производительность.

Но теперь разница не столь очевидна для C #: Microsoft CLR создает различный собственный код для разных процессоров, что делает код более эффективным для машины, на которой он работает, что не всегда выполняется компиляторами C ++.

PS C # написан очень эффективно и не имеет много уровней абстракции. Это не относится к Java, который не так эффективен. Таким образом, в этом случае, с его отличным CLR, программы на C # часто показывают лучшую производительность, чем программы на C ++. Чтобы узнать больше о .Net и CLR, взгляните на «CLR via C #» Джеффри Рихтера .

superM
источник
8
Если JIT действительно оказал негативное влияние на производительность, не будет ли он использоваться?
Завиор
2
@Zavior - я не могу придумать хорошего ответа на ваш вопрос, но я не понимаю, как JIT не может добавить дополнительных накладных расходов на производительность - JIT - это дополнительный процесс, который нужно завершить во время выполнения и требующий ресурсов, которые не требуются. тратится на выполнение самой программы, в то время как полностью скомпилированный язык «готов к работе».
Аноним
3
JIT положительно влияет на производительность, а не отрицательно, если поместить его в контекст - он компилирует байт- код в машинный код перед его запуском. Результаты также могут быть кэшированы, что позволяет ему работать быстрее, чем эквивалентный байт-код, который интерпретируется.
Кейси Кубалл
3
JIT (точнее, подход с использованием байт-кода) используется не для производительности, а для удобства. Вместо предварительной сборки двоичных файлов для каждой платформы (или общего подмножества, которое является субоптимальным для каждой из них), вы компилируете только наполовину и позволяете компилятору JIT делать все остальное. «Пиши один раз, развернись где угодно» - вот почему это делается Удобства можно было только с интерпретатором байт - код, но JIT делает это быстрее , чем сырой интерпретатором (хотя и не обязательно достаточно быстро , чтобы бить предварительно скомпилированного решение, JIT компиляция делает занять некоторое время, и результат не всегда составляют для этого).
tdammers
4
@ Tdammmers, на самом деле есть компонент производительности тоже. См. Java.sun.com/products/hotspot/whitepaper.html . Оптимизация может включать в себя такие вещи, как динамические корректировки для улучшения предсказания ветвлений и попаданий в кэш, динамическое встраивание, де-виртуализация, отключение проверки границ и развертывание цикла. Утверждается, что во многих случаях они могут более чем оплатить стоимость JIT.
Чарльз И. Грант