C # против C - большая разница в производительности - PullRequest
92 голосов
/ 26 марта 2009

Я нахожу огромные различия в производительности между похожим кодом в C anc 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 все еще занимает больше времени, но это приемлемо

Ответы [ 13 ]

159 голосов
/ 26 марта 2009

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

Time elapsed: 0.000000

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

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

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

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

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

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

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

113 голосов
/ 27 марта 2009

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

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

60 голосов
/ 26 марта 2009

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

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

Редактировать: см. ответ Джальфа ниже

53 голосов
/ 11 июля 2010

Я разработчик на C ++ и C #. Я разрабатывал приложения на C # с момента первой беты .NET Framework, и у меня более 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 ++. Большая часть времени, проведенного за .54 секунды, была связана с получением времени от системы, а не внутри самого цикла!

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

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

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

10 голосов
/ 26 марта 2009

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

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

7 голосов
/ 26 марта 2009

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

root += Math.Sqrt(i);

аналогично в коде C и выводит значение root вне цикла.

6 голосов
/ 26 марта 2009

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

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

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

5 голосов
/ 26 марта 2009

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

Не нужно образованного гадания.

5 голосов
/ 26 марта 2009

Я собрал (основываясь на вашем коде) еще два сопоставимых теста в 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 голосов
/ 26 марта 2009

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...