C # vs C - большая разница в производительности

94

Я нахожу огромные различия в производительности между аналогичным кодом на C и C #.

Код C:

#include <stdio.h>
#include <time.h>
#include <math.h>

main()
{
    int i;
    double root;

    clock_t start = clock();
    for (i = 0 ; i <= 100000000; i++){
        root = sqrt(i);
    }
    printf("Time elapsed: %f\n", ((double)clock() - start) / CLOCKS_PER_SEC);   

}

И C # (консольное приложение):

using System;
using System.Collections.Generic;
using System.Text;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            DateTime startTime = DateTime.Now;
            double root;
            for (int i = 0; i <= 100000000; i++)
            {
                root = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds/1000));
        }
    }
}

С приведенным выше кодом C # завершается за 0,328125 секунды (версия выпуска), а C - за 11,14 секунды.

C компилируется в исполняемый файл Windows с помощью mingw.

Я всегда считал, что C / C ++ быстрее или, по крайней мере, сопоставим с C # .net. Что именно заставляет C работать в 30 раз медленнее?

EDIT: похоже, что оптимизатор C # удалял корень, поскольку он не использовался. Я изменил корневое назначение на root + = и распечатал итог в конце. Я также скомпилировал C с помощью cl.exe с флагом / O2, установленным для максимальной скорости.

Теперь результаты: 3,75 секунды для C 2,61 секунды для C #

C все еще занимает больше времени, но это приемлемо

Джон
источник
18
Я бы посоветовал вам использовать секундомер, а не просто DateTime.
Alex Fort
2
Какие флаги компилятора? Оба скомпилированы с включенной оптимизацией?
jalf
2
А как насчет того, чтобы использовать -ffast-math с компилятором C ++?
Дэн МакКлейн,
10
Какой интересный вопрос!
Роберт С.
4
Может быть, функция C sqrt не так хороша, как в C #. Тогда это не будет проблемой с C, но с прикрепленной к нему библиотекой. Попробуйте произвести вычисления без математических функций.
klew

Ответы:

61

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

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

Изменить: см . Ответ Джальфа ниже

Бранн
источник
1
Небольшие эксперименты показывают, что это не так. Код для цикла создается, хотя, возможно, среда выполнения достаточно умна, чтобы пропустить его. Даже накапливая, C # все еще превосходит C.
Дана
3
Кажется, проблема на другом конце. C # ведет себя разумно во всех случаях. Его код на C, по-видимому, скомпилирован без оптимизации
jalf
2
Многие из вас упускают из виду здесь суть. Я читал много похожих случаев, когда c # превосходит c / c ++, и всегда опровергает использование некоторой оптимизации на экспертном уровне. 99% программистов не знают, как использовать такие методы оптимизации только для того, чтобы заставить их код работать немного быстрее, чем код C #. Варианты использования c / c ++ сужаются.
167

Вы, должно быть, сравниваете отладочные сборки. Я только что скомпилировал ваш код C и получил

Time elapsed: 0.000000

Если вы не включите оптимизацию, все ваши тесты будут бесполезны. (И если вы включите оптимизацию, цикл будет оптимизирован. Таким образом, ваш тестовый код тоже ошибочен. Вам нужно заставить его запустить цикл, обычно суммируя результат или аналогично и распечатывая его в конце)

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

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

В языке C есть определенные вещи, которые были бы эффективны даже в наивном неоптимизирующем компиляторе, а есть другие, которые сильно полагаются на компилятор для оптимизации всего. И, конечно же, то же самое касается C # или любого другого языка.

Скорость выполнения определяется:

  • платформа, на которой вы работаете (ОС, оборудование, другое программное обеспечение, работающее в системе)
  • компилятор
  • ваш исходный код

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

Jalf
источник
Еще много интересного здесь: blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx
Дэниел Эрвикер,
18
Хороший ответ, но я не согласен со скоростью языка, по крайней мере, по аналогии: было обнаружено, что валлийский язык медленнее большинства из-за высокой частоты долгих гласных. Кроме того, люди лучше запоминают слова (и списки слов), если они говорят быстрее. web.missouri.edu/~cowann/docs/articles/before%201993/… en.wikipedia.org/wiki/Vowel_length en.wikipedia.org/wiki/Welsh_language
exceptionerror
1
Разве это не зависит от того, что вы говорите на валлийском? Я считаю маловероятным, что все будет медленнее.
jalf
5
++ Эй, ребята, не отвлекайтесь здесь. Если одна и та же программа работает на одном языке быстрее, чем на другом, это потому, что создается другой ассемблерный код. В этом конкретном примере 99% или более времени будет плавать i, и sqrtэто то, что измеряется.
Mike Dunlavey
116

Буду краток, он уже отмечен как отвеченный. У C # есть большое преимущество, заключающееся в наличии четко определенной модели с плавающей запятой. Это просто соответствует собственному режиму работы набора инструкций FPU и SSE на процессорах x86 и x64. Это не случайно. JITter компилирует Math.Sqrt () в несколько встроенных инструкций.

Родной C / C ++ обременен годами обратной совместимости. Параметры компиляции / fp: precision, / fp: fast и / fp: strict являются наиболее заметными. Соответственно, он должен вызвать функцию CRT, которая реализует sqrt () и проверяет выбранные параметры с плавающей запятой, чтобы скорректировать результат. Это медленно.

Ганс Пассан
источник
66
Это странное убеждение среди программистов на C ++, они, кажется, думают, что машинный код, сгенерированный C #, чем-то отличается от машинного кода, сгенерированного собственным компилятором. Есть только один вид. Независимо от того, какой переключатель компилятора gcc вы используете или встроенную сборку вы пишете, по-прежнему существует только одна инструкция FSQRT. Это не всегда быстрее, потому что его генерирует родной язык, процессору все равно.
Hans Passant
16
Вот что решает pre-jitting с ngen.exe. Мы говорим о C #, а не о Java.
Hans Passant
20
@ user877329 - правда? Вот это да.
Андрас Золтан
7
Нет, джиттер x64 использует SSE. Math.Sqrt () переводится в инструкцию машинного кода sqrtsd.
Hans Passant
6
Хотя технически это не разница между языками, .NET JITter выполняет довольно ограниченную оптимизацию по сравнению с типичным компилятором C / C ++. Одним из самых больших ограничений является отсутствие поддержки SIMD, из-за чего код часто примерно в 4 раза медленнее. Не раскрытие многих внутренних функций тоже может быть большой проблемой, но это во многом зависит от того, что вы делаете.
CodesInChaos
57

Я разработчик C ++ и C #. Я разрабатывал приложения на C # с момента выхода первой бета-версии платформы .NET, и у меня более 20 лет опыта разработки приложений на C ++. Во-первых, код C # НИКОГДА не будет быстрее, чем приложение C ++, но я не буду вдаваться в подробное обсуждение управляемого кода, того, как он работает, межоперационного уровня, внутреннего устройства управления памятью, системы динамических типов и сборщика мусора. Тем не менее, позвольте мне продолжить, сказав, что все перечисленные здесь тесты дают НЕПРАВИЛЬНЫЕ результаты.

Позвольте мне объяснить: первое, что нам нужно рассмотреть, - это JIT-компилятор для C # (.NET Framework 4). Теперь JIT создает собственный код для ЦП с использованием различных алгоритмов оптимизации (которые, как правило, более агрессивны, чем оптимизатор C ++ по умолчанию, который поставляется с Visual Studio), а набор инструкций, используемый компилятором .NET JIT, более точно отражает фактический ЦП. на машине, чтобы можно было сделать определенные замены в машинном коде, чтобы сократить тактовые циклы и повысить частоту попаданий в кэш конвейера ЦП, а также произвести дополнительную оптимизацию гиперпоточности, такую ​​как переупорядочение инструкций и улучшения, относящиеся к предсказанию ветвлений.

Это означает, что если вы не скомпилируете свое приложение C ++ с использованием правильных параметров для сборки RELEASE (а не сборки DEBUG), ваше приложение C ++ может работать медленнее, чем соответствующее приложение на основе C # или .NET. При указании свойств проекта в приложении C ++ убедитесь, что вы включили «полную оптимизацию» и «предпочитайте быстрый код». Если у вас 64-битная машина, вы ДОЛЖНЫ указать генерацию x64 в качестве целевой платформы, иначе ваш код будет выполняться через подуровень преобразования (WOW64), что существенно снизит производительность.

После того, как вы выполните правильную оптимизацию в компиляторе, я получаю 0,72 секунды для приложения C ++ и 1,16 секунды для приложения C # (оба в сборке выпуска). Поскольку приложение C # очень простое и выделяет память, используемую в цикле, в стеке, а не в куче, на самом деле оно работает намного лучше, чем реальное приложение, связанное с объектами, тяжелыми вычислениями и с большими наборами данных. Таким образом, представленные цифры являются оптимистичными и ориентированы на C # и платформу .NET. Даже с учетом этого предубеждения приложение C ++ выполняется чуть более чем вдвое быстрее, чем эквивалентное приложение C #. Имейте в виду, что компилятор Microsoft C ++, который я использовал, не имел правильного конвейера и оптимизации гиперпоточности (с использованием WinDBG для просмотра инструкций по сборке).

Теперь, если мы используем компилятор Intel (который, кстати, является отраслевым секретом для создания высокопроизводительных приложений на процессорах AMD / Intel), тот же код выполняется за 0,54 секунды для исполняемого файла C ++ против 0,72 секунды при использовании Microsoft Visual Studio 2010 Итак, в итоге окончательные результаты составляют 0,54 секунды для C ++ и 1,16 секунды для C #. Таким образом, код, созданный компилятором .NET JIT, занимает в 214% раз больше времени, чем исполняемый файл C ++. Большая часть времени, потраченного на 0,54 секунды, была связана с получением времени от системы, а не внутри самого цикла!

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

При измерении производительности C ++ и .NET IL важно смотреть на ассемблерный код, чтобы убедиться, что ВСЕ вычисления выполняются. Я обнаружил, что без добавления дополнительного кода на C # большая часть кода в приведенных выше примерах была фактически удалена из двоичного файла. То же самое и с C ++, когда вы использовали более агрессивный оптимизатор, такой как тот, который поставляется с компилятором Intel C ++. Результаты, которые я предоставил выше, верны на 100% и проверены на уровне сборки.

Основная проблема в том, что множество форумов в Интернете состоит в том, что многие новички слушают маркетинговую пропаганду Microsoft, не понимая технологии, и делают ложные заявления о том, что C # быстрее, чем C ++. Утверждается, что теоретически C # быстрее, чем C ++, потому что JIT-компилятор может оптимизировать код для ЦП. Проблема с этой теорией состоит в том, что в .NET framework существует множество проблем, снижающих производительность; сантехника, которой нет в приложении C ++. Кроме того, опытный разработчик будет знать, какой компилятор использовать для данной платформы, и будет использовать соответствующие флаги при компиляции приложения. На платформах Linux или с открытым исходным кодом это не проблема, потому что вы можете распространять исходный код и создавать сценарии установки, которые компилируют код с использованием соответствующей оптимизации. На платформе Windows или с закрытым исходным кодом вам придется распространить несколько исполняемых файлов, каждый с определенной оптимизацией. Двоичные файлы Windows, которые будут развернуты, зависят от ЦП, обнаруженного установщиком msi (с использованием настраиваемых действий).

Ричард
источник
22
1. Microsoft никогда не заявляла о том, что C # быстрее, они утверждают, что это примерно 90% скорости, быстрее разрабатывается (и, следовательно, больше времени на настройку) и больше нет ошибок из-за безопасности памяти и типов. Все это правда (у меня 20 лет на C ++ и 10 на C #) 2. Производительность запуска в большинстве случаев бессмысленна. 3. Существуют также более быстрые компиляторы C #, такие как LLVM (так что Intel - это не от яблок к яблокам)
Бен
13
Производительность при запуске не бессмысленна. Это очень важно для большинства корпоративных веб-приложений, поэтому Microsoft представила веб-страницы для предварительной загрузки (автозапуска) в .NET 4.0. Когда пул приложений обновляется время от времени, при первой загрузке каждой страницы добавляется значительная задержка для сложных страниц и вызываются тайм-ауты в браузере.
Ричард
8
В более ранних маркетинговых материалах Microsoft заявляла, что .NET работает быстрее. Они также сделали различные заявления о том, что сборщик мусора практически не влияет на производительность. Некоторые из этих заявлений вошли в различные книги (по ASP.NET и .NET) в их более ранних изданиях. Хотя Microsoft конкретно не говорит, что ваше приложение C # будет быстрее, чем ваше приложение C ++, они могут широко использовать общие комментарии и маркетинговые слоганы, такие как «Just-In-Time Means Run-It-Fast» ( msdn.microsoft.com/ en-us / library / ms973894.aspx ).
Ричард
71
-1, эта напыщенная речь полна неверных и вводящих в заблуждение заявлений, таких как очевидная громкость: «Код C # НИКОГДА не будет быстрее, чем приложение C ++»
BCoates
32
-1. Вы должны прочитать битву производительности C # и C Рико Мариани против Раймонда Чена: blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx . Вкратце: одному из самых умных парней в Microsoft потребовалось немало оптимизировать, чтобы сделать версию C быстрее, чем простую версию C #.
Рольф Бьярне Квиндж
10

Моя первая догадка - оптимизация компилятора, потому что вы никогда не используете root. Вы просто назначаете его, а затем перезаписываете снова и снова.

Редактировать: блин, бей на 9 секунд!

Нил Н
источник
2
Я говорю, что вы правы. Фактическая переменная перезаписывается и больше никогда не используется. Скорее всего, csc просто откажется от всего цикла, в то время как компилятор c ++, вероятно, оставит его. Более точным тестом было бы накопление результатов и их последующая распечатка в конце. Также не следует жестко кодировать начальное значение, а оставить его на усмотрение пользователя. Это не дало бы компилятору C # места, чтобы ничего не забыть.
7

Чтобы увидеть, оптимизируется ли цикл, попробуйте изменить свой код на

root += Math.Sqrt(i);

Аналогично в коде C, а затем распечатайте значение root вне цикла.


источник
6

Возможно, компилятор C # замечает, что вы нигде не используете root, поэтому он просто пропускает весь цикл for. :)

Возможно, это не так, но я подозреваю, что независимо от причины, это зависит от реализации компилятора. Попробуйте скомпилировать вашу программу C с помощью компилятора Microsoft (cl.exe, доступного как часть win32 sdk) с оптимизацией и режимом выпуска. Бьюсь об заклад, вы увидите улучшение производительности по сравнению с другим компилятором.

РЕДАКТИРОВАТЬ: Я не думаю, что компилятор может просто оптимизировать цикл for, потому что он должен знать, что Math.Sqrt () не имеет побочных эффектов.

i_am_jorf
источник
2
Может, он это знает.
2
@Neil, @jeff: Согласен, он мог это узнать довольно легко. В зависимости от реализации статический анализ Math.Sqrt () может быть не таким сложным, хотя я не уверен, какие именно оптимизации выполняются.
Джон Феминелла,
5

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

Может тебе стоит попробовать выиграть. эквивалент $ / usr / bin / time my_cprog; / usr / bin / time my_csprog

Том
источник
1
Почему это отклонено? Кто-нибудь предполагает, что прерывания и переключение контекста не влияют на производительность? Может ли кто-нибудь делать предположения относительно промахов TLB, подкачки страниц и т. Д.?
Tom
5

Я собрал (на основе вашего кода) еще два сопоставимых теста на C и C #. Эти двое пишут меньший массив, используя оператор модуля для индексации (это добавляет небольшие накладные расходы, но эй, мы пытаемся сравнить производительность [на приблизительном уровне]).

Код C:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <math.h>

void main()
{
    int count = (int)1e8;
    int subcount = 1000;
    double* roots = (double*)malloc(sizeof(double) * subcount);
    clock_t start = clock();
    for (int i = 0 ; i < count; i++)
    {
        roots[i % subcount] = sqrt((double)i);
    }
    clock_t end = clock();
    double length = ((double)end - start) / CLOCKS_PER_SEC;
    printf("Time elapsed: %f\n", length);
}

В C #:

using System;

namespace CsPerfTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int count = (int)1e8;
            int subcount = 1000;
            double[] roots = new double[subcount];
            DateTime startTime = DateTime.Now;
            for (int i = 0; i < count; i++)
            {
                roots[i % subcount] = Math.Sqrt(i);
            }
            TimeSpan runTime = DateTime.Now - startTime;
            Console.WriteLine("Time elapsed: " + Convert.ToString(runTime.TotalMilliseconds / 1000));
        }
    }
}

Эти тесты записывают данные в массив (так что среде выполнения .NET не должно быть разрешено отбирать sqrt op), хотя массив значительно меньше (не хотелось использовать избыточную память). Я скомпилировал их в конфигурации выпуска и запускал из окна консоли (вместо запуска через VS).

На моем компьютере программа C # варьируется от 6,2 до 6,9 секунды, а версия C - от 6,9 до 7,1.

У Сесила есть имя
источник
5

Если вы выполните пошаговый код на уровне сборки, включая пошаговую процедуру извлечения квадратного корня, вы, вероятно, получите ответ на свой вопрос.

Нет необходимости в обоснованных предположениях.

Майк Данлэйви
источник
Я хотел бы знать, как это сделать
Джош Стодола
Зависит от вашей IDE или отладчика. Перерыв в начале PGM. Откройте окно разборки и начните пошаговое выполнение. При использовании GDB есть команды для пошагового выполнения одной инструкции за раз.
Майк Данлэйви,
Это хороший совет, он помогает лучше понять, что на самом деле происходит там внизу. Показывает ли это также оптимизацию JIT, такую ​​как встраивание и хвостовые вызовы?
gjvdkamp
FYI: для меня это показало, что VC ++ использует fadd и fsqrt, тогда как C # использует cvtsi2sd и sqrtsd, которые, как я понимаю, являются инструкциями SSE2 и поэтому значительно быстрее там, где они поддерживаются.
danio
2

Другой фактор, который может быть проблемой здесь, заключается в том, что компилятор C компилируется в общий машинный код для целевого семейства процессоров, тогда как MSIL, сгенерированный при компиляции кода C #, затем JIT-компиляция для нацеливания на точный процессор, который у вас есть в комплекте с любым возможные оптимизации. Таким образом, собственный код, созданный на C #, может быть значительно быстрее, чем на C.

Дэвид М
источник
Теоретически да. На практике это практически никогда не дает ощутимой разницы. Процента или два, если повезет.
jalf
или - если у вас есть код определенного типа, который использует расширения, которых нет в разрешенном списке для «универсального» процессора. Такие вещи, как ароматы SSE. Попробуйте установить более высокую цель процессора, чтобы увидеть, какие различия вы получите.
gbjbaanb
1

Мне кажется, что это не связано с самими языками, а скорее связано с различными реализациями функции извлечения квадратного корня.

Джек Райан
источник
Я очень сомневаюсь, что разные реализации sqrt могут вызвать такое несоответствие.
Alex Fort
Тем более, что даже в C # большинство математических функций по-прежнему считаются критическими для производительности и реализованы как таковые.
Мэтью Оленик,
fsqrt - это инструкция процессора IA-32, поэтому языковая реализация в наши дни не актуальна.
Not Sure,
Войдите в функцию sqrt MSVC с помощью отладчика. Он делает гораздо больше, чем просто выполняет инструкцию fsqrt.
bk1e,
1

На самом деле, ребята, цикл НЕ оптимизируется. Я скомпилировал код Джона и проверил полученный .exe. Внутренности петли следующие:

 IL_0005:  stloc.0
 IL_0006:  ldc.i4.0
 IL_0007:  stloc.1
 IL_0008:  br.s       IL_0016
 IL_000a:  ldloc.1
 IL_000b:  conv.r8
 IL_000c:  call       float64 [mscorlib]System.Math::Sqrt(float64)
 IL_0011:  pop
 IL_0012:  ldloc.1
 IL_0013:  ldc.i4.1
 IL_0014:  add
 IL_0015:  stloc.1
 IL_0016:  ldloc.1
 IL_0017:  ldc.i4     0x5f5e100
 IL_001c:  ble.s      IL_000a

Если среда выполнения не достаточно умна, чтобы понять, что цикл ничего не делает и пропускает его?

Изменить: изменение C # на:

 static void Main(string[] args)
 {
      DateTime startTime = DateTime.Now;
      double root = 0.0;
      for (int i = 0; i <= 100000000; i++)
      {
           root += Math.Sqrt(i);
      }
      System.Console.WriteLine(root);
      TimeSpan runTime = DateTime.Now - startTime;
      Console.WriteLine("Time elapsed: " +
          Convert.ToString(runTime.TotalMilliseconds / 1000));
 }

Результаты по затраченному времени (на моей машине) увеличились с 0,047 до 2,17. Но разве это просто накладные расходы на добавление 100 миллионов операторов сложения?

Дана
источник
3
Взгляд на IL мало что говорит об оптимизации, потому что, хотя компилятор C # делает некоторые вещи, такие как сворачивание констант и удаление мертвого кода, IL затем берет на себя и делает все остальное во время загрузки.
Дэниел Эрвикер,
Я думал, что это может быть так. Тем не менее, даже если заставить его работать, он все равно на 9 секунд быстрее, чем версия C. (Я бы этого совсем не ожидал)
Дана