Почему в этом случае .NET быстрее, чем C ++? - PullRequest
12 голосов
/ 18 февраля 2010

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

-edit- I LOVE SLaks комментарий.«Количество дезинформации в этих ответах ошеломляет».: D

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

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

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

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

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

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

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

C # версия:

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

Ответы [ 13 ]

16 голосов
/ 18 февраля 2010

РЕДАКТИРОВАТЬ: версия TL / DR: CLR JIT будет встроен один уровень рекурсии, MSVC 8 SP1 не будет без #pragma inline_recursion(on). И вы должны запустить версию C # вне отладчика, чтобы получить полностью оптимизированный JIT.

Я получил результаты, аналогичные acidzombie24 с C # по сравнению с C ++ с использованием VS 2008 SP1 на ноутбуке Core2 Duo с Vista, подключенным с высокопроизводительными настройками питания (~ 1600 мс против ~ 3800 мс). Довольно сложно увидеть оптимизированный код JIT'd C #, но для 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              

Версия C # в основном встроенная 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 для моей машины будет встроенными рекурсивными вызовами одного уровня, тогда как компилятор C ++ не будет делать это по умолчанию.

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

8 голосов
/ 18 февраля 2010

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

Хорошо, достаточно случайных предположений, время для какой-то науки. После получения странных результатов с существующим кодом C ++ я просто попытался запустить:

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 указал, что вы должны время C # вне отладчика, поэтому я перезапустил все:

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

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

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

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

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

4 голосов
/ 18 февраля 2010

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

Из документов MS для __rdtsc:

Создает инструкцию rdtsc, которая возвращает метку времени процессора. Отметка времени процессора записывает количество тактов с момента последнего сброса.

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

4 голосов
/ 18 февраля 2010

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

Возможно, ваш механизм таймера в C ++ изначально несовершенен.

В соответствии с http://en.wikipedia.org/wiki/Rdtsc, возможно, что вы получите неверные результаты теста.

Цитируется:

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

3 голосов
/ 18 февраля 2010

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

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

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

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

2 голосов
/ 18 февраля 2010

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

1 голос
/ 18 февраля 2010

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

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

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

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

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

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;
};
0 голосов
/ 21 июля 2010

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

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

0 голосов
/ 18 февраля 2010

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

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

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

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

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

0 голосов
/ 18 февраля 2010

Спекуляция 1

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

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

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

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

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

Спекуляция 2

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

...