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

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

Код 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);   

}

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

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));
        }
    }
}

С приведенным выше кодом С# завершается в 0.328125 секунд (версия выпуска), а C занимает 11.14 секунды для запуска.

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

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

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

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

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

Ответ 1

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

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

Изменить: см. Jalf answer ниже

Ответ 2

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

Time elapsed: 0.000000

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

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

Кстати, вы сэкономите много головных болей в долгосрочной перспективе, если отказаться от любого понятия "быстрее", чем другие. С# больше не имеет скорости, чем делает английский.

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

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

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

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

Ответ 3

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

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

Ответ 4

Я разработчик С++ и С#. Я разработал приложения С# с первой бета-версии .NET Framework, и у меня было более 20 лет опыта разработки приложений на С++. Во-первых, код С# НИКОГДА не будет быстрее, чем приложение С++, но я не буду долго обсуждать управляемый код, как он работает, межоперационный уровень, внутренние элементы управления памятью, систему динамического типа и сборщик мусора. Тем не менее, позвольте мне продолжить, сказав, что приведенные здесь контрольные показатели производят результаты INCORRECT.

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

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

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

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

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

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

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

Ответ 5

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

Изменить: проклятый, удар на 9 секунд!

Ответ 6

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

root += Math.Sqrt(i);

аналогично в коде C, а затем напечатать значение корня вне цикла.

Ответ 7

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

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

EDIT: я не думаю, что компилятор может просто оптимизировать цикл for, потому что он должен знать, что Math.Sqrt() не имеет побочных эффектов.

Ответ 8

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

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

Ответ 9

Я собрал (на основе вашего кода) еще два сравнимых теста на 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);
}

В С#:

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), хотя массив значительно меньше (не хотел использовать избыточную память). Я скомпилировал их в конфигурации release и запускал их изнутри окна консоли (вместо запуска через VS).

На моем компьютере программа С# меняется от 6,2 до 6,9 секунд, а версия C варьируется между 6.9 и 7.1.

Ответ 10

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

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

Ответ 11

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

Ответ 12

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

Ответ 13

Собственно, ребята, цикл НЕ оптимизирован. Я скомпилировал код Джона и рассмотрел полученный .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

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

Изменить: Изменение С#:

 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 миллионов операторов сложения?