web-dev-qa-db-fra.com

C # vs C - Grande différence de performances

Je trouve d'énormes différences de performances entre un code similaire en C anc C #.

Le code C est:

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

}

Et le C # (application console) est:

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

Avec le code ci-dessus, le C # se termine en 0,328125 secondes (version finale) et le C prend 11,14 secondes pour s'exécuter.

Le c est en cours de compilation dans un exécutable Windows à l'aide de mingw.

J'ai toujours pensé que C/C++ était plus rapide ou au moins comparable à C # .net. Qu'est-ce qui fait que le C fonctionne plus de 30 fois plus lentement?

EDIT: Il semble que l'optimiseur C # supprimait la racine car il n'était pas utilisé. J'ai changé l'affectation racine en racine + = et imprimé le total à la fin. J'ai également compilé le C en utilisant cl.exe avec l'indicateur/O2 défini pour la vitesse maximale.

Les résultats sont maintenant: 3,75 secondes pour le C 2,61 secondes pour le C #

Le C prend encore plus de temps, mais cela est acceptable

92
John

Comme vous n'utilisez jamais 'root', le compilateur peut avoir supprimé l'appel pour optimiser votre méthode.

Vous pouvez essayer d'accumuler les valeurs de racine carrée dans un accumulateur, de l'imprimer à la fin de la méthode et de voir ce qui se passe.

Modifier: voir réponse de Jalf ci-dessous

60
Brann

Vous devez comparer les versions de débogage. Je viens de compiler votre code C et j'ai

Time elapsed: 0.000000

Si vous n'activez pas les optimisations, toute analyse comparative que vous effectuez n'a aucune valeur. (Et si vous activez les optimisations, la boucle est optimisée. Donc, votre code de référence est également imparfait. Vous devez le forcer à exécuter la boucle, généralement en résumant le résultat ou similaire, et en l'imprimant à la fin)

Il semble que ce que vous mesurez soit fondamentalement "quel compilateur insère la surcharge de débogage". Et il s'avère que la réponse est C. Mais cela ne nous dit pas quel programme est le plus rapide. Parce que lorsque vous voulez de la vitesse, vous activez les optimisations.

Soit dit en passant, vous vous épargnerez beaucoup de maux de tête à long terme si vous abandonnez toute notion de langues "plus rapides" les unes que les autres. C # n'a pas plus de vitesse que l'anglais.

Il y a certaines choses dans le langage C qui seraient efficaces même dans un compilateur naïf non optimisant, et il y en a d'autres qui s'appuient fortement sur un compilateur pour tout optimiser. Et bien sûr, il en va de même pour C # ou tout autre langage.

La vitesse d'exécution est déterminée par:

  • la plate-forme sur laquelle vous exécutez (OS, matériel, autres logiciels exécutés sur le système)
  • le compilateur
  • votre code source

Un bon compilateur C # produira un code efficace. Un mauvais compilateur C générera du code lent. Qu'en est-il d'un compilateur C qui a généré du code C #, que vous pouvez ensuite exécuter via un compilateur C #? À quelle vitesse cela fonctionnerait-il? Les langues n'ont pas de vitesse. Votre code le fait.

160
jalf

Je serai bref, il est déjà marqué comme répondu. C # a le grand avantage d'avoir un modèle à virgule flottante bien défini. Cela correspond simplement au mode de fonctionnement natif de la FPU et au jeu d'instructions SSE sur les processeurs x86 et x64. Pas de coïncidence. Le JITter compile Math.Sqrt () en quelques instructions en ligne.

Native C/C++ est sellé avec des années de compatibilité descendante. Les options de compilation/fp: precise,/fp: fast et/fp: strict sont les plus visibles. Par conséquent, il doit appeler une fonction CRT qui implémente sqrt () et vérifie les options en virgule flottante sélectionnées pour ajuster le résultat. C'est lent.

113
Hans Passant

Je suis développeur C++ et C #. J'ai développé des applications C # depuis la première version bêta du framework .NET et j'ai plus de 20 ans d'expérience dans le développement d'applications C++. Premièrement, le code C # ne sera JAMAIS plus rapide qu'une application C++, mais je ne passerai pas par une longue discussion sur le code managé, son fonctionnement, la couche interopératoire, les internes de gestion de la mémoire, le système de type dynamique et le garbage collector. Néanmoins, permettez-moi de continuer en disant que les repères énumérés ici produisent tous des résultats INCORRECTS.

Laissez-moi vous expliquer: La première chose que nous devons considérer est le compilateur JIT pour C # (.NET Framework 4). Maintenant, le JIT produit du code natif pour le CPU en utilisant divers algorithmes d'optimisation (qui ont tendance à être plus agressifs que l'optimiseur C++ par défaut fourni avec Visual Studio) et le jeu d'instructions utilisé par le compilateur .NET JIT reflète plus fidèlement le CPU réel sur la machine afin que certaines substitutions dans le code machine puissent être effectuées pour réduire les cycles d'horloge et améliorer le taux de réussite dans le cache du pipeline CPU et produire d'autres optimisations d'hyper-threading telles que la réorganisation des instructions et les améliorations relatives à la prédiction de branche.

Cela signifie qu'à moins que vous ne compiliez votre application C++ en utilisant les bons pararmètres pour la version RELEASE (et non la version DEBUG), votre application C++ peut fonctionner plus lentement que l'application C # ou .NET correspondante. Lorsque vous spécifiez les propriétés du projet sur votre application C++, assurez-vous d'activer "l'optimisation complète" et de "privilégier le code rapide". Si vous avez une machine 64 bits, vous DEVEZ spécifier de générer x64 comme plate-forme cible, sinon votre code sera exécuté via une sous-couche de conversion (WOW64) qui réduira considérablement les performances.

Une fois que vous avez effectué les optimisations correctes dans le compilateur, j'obtiens 0,72 seconde pour l'application C++ et 1,16 seconde pour l'application C # (les deux dans la version Release). Étant donné que l'application C # est très basique et alloue la mémoire utilisée dans la boucle sur la pile et non sur le tas, elle fonctionne en réalité beaucoup mieux qu'une application réelle impliquée dans des objets, des calculs lourds et des ensembles de données plus volumineux. Les chiffres fournis sont donc des chiffres optimistes orientés vers C # et le framework .NET. Même avec ce biais, l'application C++ se termine en un peu plus de la moitié du temps que l'application C # équivalente. Gardez à l'esprit que le compilateur Microsoft C++ que j'ai utilisé n'avait pas le bon pipeline et les optimisations d'hyperthreading (en utilisant WinDBG pour afficher les instructions d'assemblage).

Maintenant, si nous utilisons le compilateur Intel (qui est d'ailleurs un secret de l'industrie pour générer des applications hautes performances sur les processeurs AMD/Intel), le même code s'exécute en 0,54 secondes pour l'exécutable C++ vs 0,72 secondes en utilisant Microsoft Visual Studio 2010 Donc, au final, les résultats finaux sont de 0,54 seconde pour C++ et de 1,16 seconde pour C #. Ainsi, le code produit par le compilateur .NET JIT prend 214% plus de temps que l'exécutable C++. La plupart du temps passé dans les 0,54 secondes était pour obtenir le temps du système et non dans la boucle elle-même!

Ce qui manque également dans les statistiques, ce sont les temps de démarrage et de nettoyage qui ne sont pas inclus dans les horaires. Les applications C # ont tendance à passer beaucoup plus de temps au démarrage et à l'arrêt que les applications C++. La raison derrière cela est compliquée et a à voir avec les routines de validation du code d'exécution .NET et le sous-système de gestion de la mémoire qui effectue beaucoup de travail au début (et par conséquent, à la fin) du programme pour optimiser les allocations de mémoire et les ordures collectionneur.

Lors de la mesure des performances de C++ et .NET IL, il est important de regarder le code Assembly pour vous assurer que TOUS les calculs sont là. Ce que j'ai trouvé, c'est que sans mettre de code supplémentaire en C #, la plupart du code dans les exemples ci-dessus ont été supprimés du binaire. C'était également le cas avec C++ lorsque vous utilisiez un optimiseur plus agressif tel que celui fourni avec le compilateur Intel C++. Les résultats que j'ai fournis ci-dessus sont 100% corrects et validés au niveau de l'Assemblée.

Le principal problème avec beaucoup de forums sur Internet que beaucoup de débutants écoutent la propagande marketing de Microsoft sans comprendre la technologie et font de fausses affirmations que C # est plus rapide que C++. L'affirmation est qu'en théorie, C # est plus rapide que C++ car le compilateur JIT peut optimiser le code du CPU. Le problème avec cette théorie est qu'il y a beaucoup de plomberie dans le framework .NET qui ralentit les performances; plomberie qui n'existe pas dans l'application C++. De plus, un développeur expérimenté connaîtra le bon compilateur à utiliser pour la plate-forme donnée et utilisera les indicateurs appropriés lors de la compilation de l'application. Sur les plates-formes Linux ou open source, ce n'est pas un problème car vous pouvez distribuer votre source et créer des scripts d'installation qui compilent le code en utilisant l'optimisation appropriée. Sur la plateforme Windows ou fermée, vous devrez distribuer plusieurs exécutables, chacun avec des optimisations spécifiques. Les binaires Windows qui seront déployés sont basés sur le CPU détecté par le programme d'installation msi (à l'aide d'actions personnalisées).

54
Richard

ma première supposition est une optimisation du compilateur parce que vous n'utilisez jamais root. Il vous suffit de l'attribuer, puis de l'écraser encore et encore.

Edit: putain, bat de 9 secondes!

10
Neil N

Pour voir si la boucle est optimisée, essayez de changer votre code en

root += Math.Sqrt(i);

ans de la même manière dans le code C, puis affichez la valeur de root en dehors de la boucle.

7
anon

Peut-être que le compilateur c # remarque que vous n'utilisez root nulle part, donc il saute simplement la boucle for. :)

Ce n'est peut-être pas le cas, mais je suppose que quelle qu'en soit la cause, cela dépend de l'implémentation du compilateur. Essayez de compiler votre programme C avec le compilateur Microsoft (cl.exe, disponible dans le cadre du win32 sdk) avec optimisations et mode Release. Je parie que vous verrez une amélioration de la performance par rapport à l'autre compilateur.

EDIT: Je ne pense pas que le compilateur puisse simplement optimiser la boucle for, car il devrait savoir que Math.Sqrt () n'a pas d'effets secondaires.

6
i_am_jorf

Quel que soit le temps diff. Il se peut que ce "temps écoulé" ne soit pas valide. Il ne serait valable que si vous pouvez garantir que les deux programmes s'exécutent dans les mêmes conditions.

Vous devriez peut-être essayer de gagner. équivalent à $/usr/bin/time my_cprog;/usr/bin/time my_csprog

5
Tom

J'ai mis en place (sur la base de votre code) deux autres tests comparables en C et C #. Ces deux-là écrivent un tableau plus petit en utilisant l'opérateur de module pour l'indexation (cela ajoute un peu de surcharge, mais bon, nous essayons de comparer les performances [à un niveau brut]).

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

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

Ces tests écrivent des données dans un tableau (donc le runtime .NET ne doit pas être autorisé à supprimer l'opération sqrt) bien que le tableau soit considérablement plus petit (ne voulait pas utiliser une mémoire excessive). Je les ai compilés dans la version de configuration et les ai exécutés à partir d'une fenêtre de console (au lieu de démarrer via VS).

Sur mon ordinateur, le programme C # varie entre 6,2 et 6,9 secondes, tandis que la version C varie entre 6,9 ​​et 7,1.

5
Cecil Has a Name

Si vous ne faites qu'une seule étape du code au niveau de l'assembly, y compris en passant par la routine racine carrée, vous obtiendrez probablement la réponse à votre question.

Pas besoin de deviner instruit.

5
Mike Dunlavey

L'autre facteur qui peut être un problème ici est que le compilateur C compile en code natif générique pour la famille de processeurs que vous ciblez, tandis que le MSIL généré lorsque vous avez compilé le code C # est ensuite compilé JIT pour cibler le processeur exact que vous avez terminé avec n'importe quel optimisations possibles. Ainsi, le code natif généré à partir du C # peut être considérablement plus rapide que le C.

2
David M

Il me semble que cela n'a rien à voir avec les langages eux-mêmes, mais plutôt avec les différentes implémentations de la fonction racine carrée.

1
Jack Ryan

En fait les gars, la boucle n'est PAS optimisée. J'ai compilé le code de John et examiné le fichier .exe résultant. Les entrailles de la boucle sont les suivantes:

 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

À moins que le runtime soit suffisamment intelligent pour réaliser que la boucle ne fait rien et la saute?

Edit: Changer le C # pour être:

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

Résultats dans le temps écoulé (sur ma machine) passant de 0,047 à 2,17. Mais est-ce juste l'overhead d'ajouter 100 millions d'opérateurs supplémentaires?

1
Dana