Подтвердить что ты не робот

Почему .NET быстрее, чем С++ в этом случае?

Убедитесь, что вы работаете за пределами среды IDE. Это ключ.

-edit- Я ЛЮБЛЮ комментарий SLaks. "Количество дезинформации в этих ответах колеблется".: D

Успокойтесь, ребята. Почти все вы ошибались. Я сделал оптимизацию. Оказывается, какие-то оптимизации, которые я сделал, были недостаточно хороши. Я запустил код в GCC, используя gettimeofday (я вставлю код ниже) и использовал g++ -O2 file.cpp и получил немного более быстрый результат, чем С#. Возможно, MS не создала оптимизаций, необходимых в этом конкретном случае, но после загрузки и установки mingw я был протестирован и нашел, что скорость близка к идентичной. Юстик Кажется правильным. Я мог бы поклясться, что использую часы на своем ПК и использовал это для подсчета и нашел, что он работает медленнее, но проблема решена. Скорость С++ не почти в два раза медленнее в компиляторе MS.

Когда мой друг сообщил мне об этом, я не мог в это поверить. Поэтому я взял его код и надел на него некоторые таймеры.

Вместо Boo Я использовал С#. Я постоянно получаю более быстрые результаты на С#. Зачем? Версия .NET была почти в половину времени независимо от того, какой номер я использовал.

Версия на С++ (плохая версия):

#include <iostream>
#include <stdio.h>
#include <intrin.h>
#include <windows.h>
using namespace std;

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    __int64 time = 0xFFFFFFFF;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
__int64 start = __rdtsc();
        int res = fib(n);
__int64 end = __rdtsc();
        cout << res << endl;
        cout << (float)(end-start)/1000000<<endl;
        break;
    }

    return 0;
}

Версия на С++ (лучшая версия):

#include <iostream>
#include <stdio.h>
#include <intrin.h>
#include <windows.h>
using namespace std;

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    __int64 time = 0xFFFFFFFF;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
        LARGE_INTEGER start, end, delta, freq;
        ::QueryPerformanceFrequency( &freq );
        ::QueryPerformanceCounter( &start );
        int res = fib(n);
        ::QueryPerformanceCounter( &end );
        delta.QuadPart = end.QuadPart - start.QuadPart;
        cout << res << endl;
        cout << ( delta.QuadPart * 1000 ) / freq.QuadPart <<endl;
break;
    }

    return 0;
}

Версия С#:

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

using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Threading;
using System.IO;

using System.Diagnostics;

namespace fibCSTest
{
    class Program
    {
         static int fib(int n)
         {
            if (n < 2)return n;
            return fib(n - 1) + fib(n - 2);
         }

         static void Main(string[] args)
         {
             //var sw = new Stopwatch();
             //var timer = new PAB.HiPerfTimer();
             var timer = new Stopwatch();
             while (true)
             {
                 int n;
                 //cin >> n;
                 n = 41;
                 if (n < 0) break;
                 timer.Start();
                 int res = fib(n);
                 timer.Stop();
                 Console.WriteLine(res);
                 Console.WriteLine(timer.ElapsedMilliseconds);
                 break;
             }
         }
    }
}

Версия GCC:

#include <iostream>
#include <stdio.h>
#include <sys/time.h>
using namespace std;

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    timeval start, end;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
        gettimeofday(&start, 0);
        int res = fib(n);
        gettimeofday(&end, 0);
        int sec = end.tv_sec - start.tv_sec;
        int usec = end.tv_usec - start.tv_usec;
        cout << res << endl;
        cout << sec << " " << usec <<endl;
        break;
    }

    return 0;
}
4b9b3361

Ответ 1

EDIT: версия TL/DR: CLR JIT будет поддерживать один уровень рекурсии, MSVC 8 SP1 не будет без #pragma inline_recursion(on). И вы должны запустить версию С# за пределами отладчика, чтобы получить полностью оптимизированную JIT.

Я получил аналогичные результаты с acidzombie24 с С# по сравнению с С++, используя VS 2008 SP1 на ноутбуке Core2 Duo под управлением Vista, подключенном к высокопроизводительным настройкам питания (~ 1600 мс против ~ 3800 мс). Трудно видеть оптимизированный код JIT'd С#, но для x86 он сводится к следующему:

00000000 55               push        ebp  
00000001 8B EC            mov         ebp,esp 
00000003 57               push        edi  
00000004 56               push        esi  
00000005 53               push        ebx  
00000006 8B F1            mov         esi,ecx 
00000008 83 FE 02         cmp         esi,2 
0000000b 7D 07            jge         00000014 
0000000d 8B C6            mov         eax,esi 
0000000f 5B               pop         ebx  
00000010 5E               pop         esi  
00000011 5F               pop         edi  
00000012 5D               pop         ebp  
00000013 C3               ret              
            return fib(n - 1) + fib(n - 2);
00000014 8D 7E FF         lea         edi,[esi-1] 
00000017 83 FF 02         cmp         edi,2 
0000001a 7D 04            jge         00000020 
0000001c 8B DF            mov         ebx,edi 
0000001e EB 19            jmp         00000039 
00000020 8D 4F FF         lea         ecx,[edi-1] 
00000023 FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
00000029 8B D8            mov         ebx,eax 
0000002b 4F               dec         edi  
0000002c 4F               dec         edi  
0000002d 8B CF            mov         ecx,edi 
0000002f FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
00000035 03 C3            add         eax,ebx 
00000037 8B D8            mov         ebx,eax 
00000039 4E               dec         esi  
0000003a 4E               dec         esi  
0000003b 83 FE 02         cmp         esi,2 
0000003e 7D 04            jge         00000044 
00000040 8B D6            mov         edx,esi 
00000042 EB 19            jmp         0000005D 
00000044 8D 4E FF         lea         ecx,[esi-1] 
00000047 FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
0000004d 8B F8            mov         edi,eax 
0000004f 4E               dec         esi  
00000050 4E               dec         esi  
00000051 8B CE            mov         ecx,esi 
00000053 FF 15 F8 2F 12 00 call        dword ptr ds:[00122FF8h] 
00000059 03 C7            add         eax,edi 
0000005b 8B D0            mov         edx,eax 
0000005d 03 DA            add         ebx,edx 
0000005f 8B C3            mov         eax,ebx 
00000061 5B               pop         ebx  
00000062 5E               pop         esi  
00000063 5F               pop         edi  
00000064 5D               pop         ebp  
00000065 C3               ret  

В отличие от сгенерированного C++ кода (/Ox/Ob2/Oi/Ot/Oy/GL/Gr):

int fib(int n)
{ 
00B31000 56               push        esi  
00B31001 8B F1            mov         esi,ecx 
    if (n < 2) return n; 
00B31003 83 FE 02         cmp         esi,2 
00B31006 7D 04            jge         fib+0Ch (0B3100Ch) 
00B31008 8B C6            mov         eax,esi 
00B3100A 5E               pop         esi  
00B3100B C3               ret              
00B3100C 57               push        edi  
    return fib(n - 1) + fib(n - 2); 
00B3100D 8D 4E FE         lea         ecx,[esi-2] 
00B31010 E8 EB FF FF FF   call        fib (0B31000h) 
00B31015 8D 4E FF         lea         ecx,[esi-1] 
00B31018 8B F8            mov         edi,eax 
00B3101A E8 E1 FF FF FF   call        fib (0B31000h) 
00B3101F 03 C7            add         eax,edi 
00B31021 5F               pop         edi  
00B31022 5E               pop         esi  
} 
00B31023 C3               ret              

Версия С# в основном встроена fib(n-1) и fib(n-2). Для функции, которая так тяжела, call, сокращение числа вызовов функций является ключом к скорости. Заменив fib на следующее:

int fib(int n);

int fib2(int n) 
{ 
    if (n < 2) return n; 
    return fib(n - 1) + fib(n - 2); 
} 

int fib(int n)
{ 
    if (n < 2) return n; 
    return fib2(n - 1) + fib2(n - 2); 
} 

Доходит до ~ 1900 мс. Кстати, если я использую #pragma inline_recursion(on), я получаю аналогичные результаты с оригиналом fib. Развертывание еще одного уровня:

int fib(int n);

int fib3(int n) 
{ 
    if (n < 2) return n; 
    return fib(n - 1) + fib(n - 2); 
} 

int fib2(int n) 
{ 
    if (n < 2) return n; 
    return fib3(n - 1) + fib3(n - 2); 
} 

int fib(int n)
{ 
    if (n < 2) return n; 
    return fib2(n - 1) + fib2(n - 2); 
} 

Получает значение ~ 1380 мс. Кроме того, он сужается.

Итак, похоже, что CLR JIT для моей машины будет встраивать рекурсивные вызовы на один уровень, тогда как компилятор С++ не сделает этого по умолчанию.

Если только весь критический код производительности был похож на fib!

Ответ 2

EDIT: Хотя исходная синхронизация С++ неверна (сравнение циклов с миллисекундами), лучший выбор времени показывает, что С# быстрее с настройками компилятора ваниля.

ОК, достаточно случайных спекуляций, времени для какой-то науки. Получив странные результаты с существующим кодом на С++, я просто попытался запустить:

int fib(int n)
{
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

int main()
{
    __int64 time = 0xFFFFFFFF;
    while (1)
    {
        int n;
        //cin >> n;
        n = 41;
        if (n < 0) break;
        LARGE_INTEGER start, end, delta, freq;
        ::QueryPerformanceFrequency( &freq );
        ::QueryPerformanceCounter( &start );
        int res = fib(n);
        ::QueryPerformanceCounter( &end );
        delta.QuadPart = end.QuadPart - start.QuadPart;
        cout << res << endl;
        cout << ( delta.QuadPart * 1000 ) / freq.QuadPart <<endl;
break;
    }

    return 0;
}

EDIT:

MSN указали, что вам нужно время С# вне отладчика, поэтому я перезапустил все:

Лучшие результаты (VC2008, запуск сборки релиза из командной строки, без специальных опций)

  • Оригинальный код С++ - 10239
  • С++ QPF - 3427
  • С# - 2166 (в отладчике было 4700).

Исходный код на С++ (с rdtsc) не возвращал миллисекунды, просто коэффициент зарегистрированных тактовых циклов, поэтому сравнение непосредственно с StopWatch() недействителен. Исходный код времени просто неверен.

Примечание StopWatch() использует вызовы QueryPerformance *: http://msdn.microsoft.com/en-us/library/system.diagnostics.stopwatch.aspx

Итак, в этом случае С++ быстрее, чем С#. Это зависит от ваших настроек компилятора - см. Ответ MSN.

Ответ 3

Не понимаю ответ с сборкой мусора или буфером консоли.

Возможно, ваш механизм таймера на С++ по своей сути ошибочен.

Согласно http://en.wikipedia.org/wiki/Rdtsc, возможно, что вы ошиблись в результатах тестов.

Цитируется:

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

Ответ 4

Я думаю, что проблема заключается в вашем временном коде в С++.

В документах MS для __rdtsc:

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

Возможно, попробуйте GetTickCount().

Ответ 5

Не сказать, что проблема, но вы можете прочитать Как использовать Таймер высокого разрешения

Также см. это... http://en.wikipedia.org/wiki/Comparison_of_Java_and_C%2B%2B#Performance

В нескольких исследованиях в основном числовых тестов утверждается, что в некоторых случаях Java может быть быстрее, чем С++, по разным причинам: [8] [9] Указатели упрощают оптимизацию, поскольку они могут указывать на произвольные данные, хотя многие компиляторы С++ предоставляют ограничение на ключевое слово C99, которое исправляет эту проблему. [10] По сравнению с реализациями С++, которые используют неограниченное использование стандартных реализаций malloc/new для распределения памяти, реализации коллекции мусора Java могут иметь лучшую согласованность кеша, поскольку ее распределения обычно выполняются последовательно.     * Компиляция во время выполнения может потенциально использовать дополнительную информацию, доступную во время выполнения, для более эффективного оптимизации кода, например, зная, какой процессор будет выполняться.

Это о Java, но начинает решать проблему производительности между циклами C и временем выполнения JITed.

Ответ 6

Возможно, С# может разворачивать стек в рекурсивных вызовах? Я думаю, что это также уменьшает количество вычислений.

Ответ 7

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

То, что имеет смысл на одном языке, может иметь ужасные побочные эффекты в другом. Чтобы действительно сравнить характеристики производительности, вам нужна версия С# и С++, а код для этих версий может быть совсем другим. Например, в С# я бы даже не использовал одну и ту же подпись функции. Я бы пошел с чем-то более похожим на это:

IEnumerable<int> Fibonacci()
{
   int n1 = 0;
   int n2 = 1;

   yield return 1;
   while (true)
   {
      int n = n1 + n2;
      n1 = n2;
      n2 = n;
      yield return n;
   }
}

а затем оберните это следующим образом:

public static int fib(int n)
{
    return Fibonacci().Skip(n).First();
}

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

И если вы действительно хотите кричать на С++, вы можете использовать метапрограммирование, чтобы компилятор предварительно вычислил ваши результаты следующим образом:

template<int N> struct fibonacci
{
    static const int value = fibonacci<N - 1>::value + fibonacci<N - 2>::value;
};

template<> struct fibonacci<1>
{
    static const int value = 1;
};

template<> struct fibonacci<0>
{
    static const int value = 0;
};

Ответ 8

Возможно, что методы выполняются во время выполнения до запуска теста... или что консоль является оболочкой API для вывода на консоль, когда код С++ для cout буферизуется. Думаю..

Надеюсь, это поможет, С наилучшими пожеланиями, Том.

Ответ 9

вы вызываете статическую функцию в коде С#, которая будет встроена, а в С++ вы используете нестатические функции. У меня есть ~ 1,4 с для С++. с g++ -O3 вы можете иметь 1.21 с.

вы просто не можете сравнивать С# с С++ с плохо переведенным кодом

Ответ 10

Если этот код действительно равен 1/2 времени выполнения, возможны следующие возможные причины:

  • Сбор мусора ускоряет выполнение кода С# над кодом С++, если это происходит где-то в приведенном выше коде.
  • Запись С# на консоль может быть буферизована (С++ может не работать, или это может быть не так эффективно)

Ответ 11

Я знаю, что компилятор .NET имеет оптимизацию Intel.

Ответ 12

Спекуляция 1

Процедура сбора мусора может сыграть свою роль.

В версии С++ все управление памятью будет выполняться во время работы программы, и это будет считаться в последний раз.

В .NET сборщик мусора (GC) Common Language Runtime (CLR) представляет собой отдельный процесс в другом потоке и часто очищает вашу программу после ее завершения. Поэтому ваша программа завершится, время будет распечатываться до освобождения памяти. Особенно для небольших программ, которые обычно не будут очищены до завершения.

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

Чтобы протестировать GC: Чтобы увидеть "замедленное" поведение .NET GC в действии, поставьте точку останова в некоторых методах деструктора объекта/финализатора. Отладчик оживет и ударит эти точки останова после завершения программы (да, после завершения Main).

Спекуляция 2

В противном случае исходный код С# скомпилирован программистом до кода IL (инструкции в байтовом коде Microsoft), а во время выполнения они, в свою очередь, компилируются компилятором CLR Just-In-Time в специфичный для процессора набор инструкций (как и в классических скомпилированных программах), поэтому нет причин, по которым программа .NET должна быть медленнее после ее запуска и запускается в первый раз.

Ответ 13

Я думаю, что все здесь пропустили "секретный ингредиент" , что делает все возможное: компилятор JIT точно знает, что такое целевая архитектура, тогда как статический компилятор этого не делает. Различные процессоры x86 имеют очень разные архитектуры и конвейеры, поэтому последовательность инструкций, которая является наиболее быстрой на одном процессоре, может быть относительно медленнее на другом.

В этом случае стратегия оптимизации компилятора Microsoft С++ была нацелена на другой процессор, чем процессор acidzombie24 фактически использовал, но gcc выбрал команды, более подходящие для его процессора. На более новом, более старом или другом процессоре производителя, скорее всего, Microsoft С++ будет быстрее, чем gcc.

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

Сказав это, я бы предположил, что тот факт, что CLR JIT выбрал лучшую последовательность команд, чем Microsoft С++, был скорее удачей, чем знанием архитектуры. Об этом свидетельствует тот факт, что на процессоре Justicle компилятор Microsoft С++ выбрал лучшую последовательность команд, чем компилятор CLR JIT.

Заметка о _rdtsc vs QueryPerformanceCounter: Да _rdtsc нарушена, но когда вы говорите о 3-4-секундной операции и выполняете ее несколько раз, чтобы проверить согласованную синхронизацию, любая ситуация, которая вызывает _rdtsc, (например, изменения скорости процессора или изменения процессора) должны вызывать отдаленные значения в данных теста, которые будут выброшены, поэтому, предполагая, что acidzombie24 правильно выполнил свои исходные тесты, я сомневаюсь, что вопрос _rdtsc vs QueryPerformanceCounter действительно имел какое-то влияние.