C # vs C: grande differenza di prestazioni


94

Sto riscontrando enormi differenze di prestazioni tra codice simile in C anc C #.

Il codice 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);   

}

E il C # (app console) è:

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

Con il codice precedente, il C # viene completato in 0,328125 secondi (versione di rilascio) e l'esecuzione del C richiede 11,14 secondi.

La c viene compilata in un eseguibile di Windows utilizzando mingw.

Sono sempre stato convinto che C / C ++ fosse più veloce o almeno paragonabile a C # .net. Cosa sta causando esattamente il funzionamento del C oltre 30 volte più lento?

EDIT: sembra che l'ottimizzatore C # rimuovesse la radice poiché non veniva utilizzato. Ho cambiato l'assegnazione di root in root + = e ho stampato il totale alla fine. Ho anche compilato il C usando cl.exe con il flag / O2 impostato per la velocità massima.

I risultati sono ora: 3,75 secondi per C 2,61 secondi per C #

La C impiega ancora più tempo, ma questo è accettabile


18
Ti suggerirei di utilizzare uno StopWatch invece di un semplice DateTime.
Alex Fort

2
Quali flag del compilatore? Entrambi sono compilati con le ottimizzazioni abilitate?
jalf

2
E quando usi -ffast-math con il compilatore C ++?
Dan McClain,

10
Che domanda affascinante!
Robert S.

4
Forse la funzione C sqrt non è buona come questa in C #. Quindi non sarebbe un problema con C, ma con la libreria allegata. Prova alcuni calcoli senza funzioni matematiche.
klew

Risposte:


61

Dato che non usi mai "root", il compilatore potrebbe aver rimosso la chiamata per ottimizzare il tuo metodo.

Potresti provare ad accumulare i valori della radice quadrata in un accumulatore, stamparlo alla fine del metodo e vedere cosa sta succedendo.

Modifica: vedi la risposta di Jalf di seguito


1
Un po 'di sperimentazione suggerisce che non è così. Il codice per il ciclo viene generato, anche se forse il runtime è abbastanza intelligente da saltarlo. Anche accumulando, C # batte ancora i pantaloni di C.
Dana

3
Sembra che il problema sia dall'altra parte. C # si comporta ragionevolmente in tutti i casi. Il suo codice C è apparentemente compilato senza ottimizzazioni
jalf

2
Molti di voi qui mancano il punto. Ho letto molti casi simili in cui c # supera c / c ++ e la confutazione è sempre quella di impiegare un'ottimizzazione a livello di esperto. Il 99% dei programmatori non ha le conoscenze per utilizzare tali tecniche di ottimizzazione solo per far sì che il loro codice venga eseguito leggermente più velocemente del codice c #. I casi d'uso per c / c ++ si stanno restringendo.

167

Devi confrontare le build di debug. Ho appena compilato il tuo codice C e ho ottenuto

Time elapsed: 0.000000

Se non abiliti le ottimizzazioni, qualsiasi benchmark che fai è completamente inutile. (E se abiliti le ottimizzazioni, il ciclo viene ottimizzato. Quindi anche il tuo codice di benchmarking è difettoso. Devi forzarlo a eseguire il ciclo, di solito sommando il risultato o simili e stampandolo alla fine)

Sembra che ciò che stai misurando sia fondamentalmente "quale compilatore inserisce il maggior overhead di debug". E si scopre che la risposta è C. Ma questo non ci dice quale programma sia il più veloce. Perché quando vuoi velocità, abiliti le ottimizzazioni.

A proposito, ti risparmierai un sacco di mal di testa a lungo termine se abbandonerai l'idea che le lingue siano "più veloci" l'una dell'altra. Il C # non ha più velocità dell'inglese.

Ci sono alcune cose nel linguaggio C che sarebbero efficienti anche in un compilatore ingenuo non ottimizzante, e ce ne sono altre che fanno affidamento su un compilatore per ottimizzare tutto. E, naturalmente, lo stesso vale per C # o qualsiasi altro linguaggio.

La velocità di esecuzione è determinata da:

  • la piattaforma su cui stai eseguendo (sistema operativo, hardware, altro software in esecuzione sul sistema)
  • il compilatore
  • il tuo codice sorgente

Un buon compilatore C # produrrà codice efficiente. Un cattivo compilatore C genererà codice lento. Che ne dici di un compilatore C che ha generato codice C #, che potresti quindi eseguire tramite un compilatore C #? Quanto velocemente correrebbe? Le lingue non hanno velocità. Il tuo codice lo fa.



18
Buona risposta, ma non sono d'accordo sulla velocità del linguaggio, almeno in analogia: è stato scoperto che il gallese è un linguaggio più lento della maggior parte a causa dell'alta frequenza delle vocali lunghe. Inoltre, le persone ricordano meglio le parole (e gli elenchi di parole) se sono più veloci da dire. web.missouri.edu/~cowann/docs/articles/before%201993/… en.wikipedia.org/wiki/Vowel_length en.wikipedia.org/wiki/Welsh_language
exceptionerror

1
Ma non dipende da quello che dici in gallese? Trovo improbabile che tutto sia più lento.
jalf

5
++ Ehi ragazzi, non fatevi distrarre qui. Se lo stesso programma viene eseguito più velocemente in una lingua rispetto a un'altra, è perché viene generato un codice assembly diverso. In questo particolare esempio, il 99% o più del tempo andrà in galleggiante i, e sqrt, in modo che di ciò che viene misurato.
Mike Dunlavey,

116

Lo terrò breve, è già contrassegnato come risposta. C # ha il grande vantaggio di avere un modello a virgola mobile ben definito. Ciò accade solo per corrispondere alla modalità operativa nativa delle istruzioni FPU e SSE impostate sui processori x86 e x64. Non a caso lì. JITter compila Math.Sqrt () in poche istruzioni inline.

Il C / C ++ nativo è dotato di anni di compatibilità con le versioni precedenti. Le opzioni di compilazione / fp: precise, / fp: fast e / fp: strict sono le più visibili. Di conseguenza, deve chiamare una funzione CRT che implementa sqrt () e controlla le opzioni in virgola mobile selezionate per regolare il risultato. È lento.


66
Questa è una strana convinzione tra i programmatori C ++, sembrano pensare che il codice macchina generato da C # sia in qualche modo diverso dal codice macchina generato da un compilatore nativo. C'è solo un tipo. Indipendentemente dallo switch del compilatore gcc che usi o dall'assembly inline che scrivi, c'è ancora una sola istruzione FSQRT. Non è sempre più veloce perché è stato generato da una lingua madre, alla CPU non interessa.
Hans Passant

16
Questo è ciò che risolve il pre-jitting con ngen.exe. Stiamo parlando di C #, non di Java.
Hans Passant

20
@ user877329 - davvero? Wow.
Andras Zoltan

7
No, il jitter x64 utilizza SSE. Math.Sqrt () viene tradotto nell'istruzione del codice macchina sqrtsd.
Hans Passant

6
Sebbene tecnicamente non sia una differenza tra i linguaggi, .net JITter esegue ottimizzazioni piuttosto limitate rispetto a un tipico compilatore C / C ++. Uno dei maggiori limiti è la mancanza del supporto SIMD che rende il codice spesso circa 4 volte più lento. Anche non esporre molti elementi intrinseci può essere un grosso malus, ma dipende molto da quello che stai facendo.
CodesInChaos

57

Sono uno sviluppatore C ++ e C #. Ho sviluppato applicazioni C # sin dalla prima beta del framework .NET e ho più di 20 anni di esperienza nello sviluppo di applicazioni C ++. In primo luogo, il codice C # non sarà MAI più veloce di un'applicazione C ++, ma non affronterò una lunga discussione sul codice gestito, su come funziona, sul livello di interoperabilità, sugli interni di gestione della memoria, sul sistema di tipi dinamici e sul garbage collector. Tuttavia, permettetemi di continuare dicendo che i benchmark qui elencati producono tutti risultati SCORRETTI.

Mi spiego meglio: la prima cosa che dobbiamo considerare è il compilatore JIT per C # (.NET Framework 4). Ora JIT produce codice nativo per la CPU utilizzando vari algoritmi di ottimizzazione (che tendono ad essere più aggressivi dell'ottimizzatore C ++ predefinito fornito con Visual Studio) e il set di istruzioni utilizzato dal compilatore .NET JIT riflette più da vicino la CPU effettiva sulla macchina in modo che alcune sostituzioni nel codice macchina possano essere effettuate per ridurre i cicli di clock e migliorare la frequenza di riscontro nella cache della pipeline della CPU e produrre ulteriori ottimizzazioni hyper-threading come riordino delle istruzioni e miglioramenti relativi alla previsione dei rami.

Ciò significa che, a meno che non si compili l'applicazione C ++ utilizzando i parametri corretti per la build RELEASE (non la build DEBUG), l'applicazione C ++ potrebbe funzionare più lentamente rispetto alla corrispondente applicazione basata su C # o .NET. Quando si specificano le proprietà del progetto sulla propria applicazione C ++, assicurarsi di abilitare "l'ottimizzazione completa" e "favorire il codice veloce". Se hai una macchina a 64 bit, DEVI specificare di generare x64 come piattaforma di destinazione, altrimenti il ​​tuo codice verrà eseguito attraverso un sottolivello di conversione (WOW64) che ridurrà sostanzialmente le prestazioni.

Dopo aver eseguito le ottimizzazioni corrette nel compilatore, ottengo 0,72 secondi per l'applicazione C ++ e 1,16 secondi per l'applicazione C # (entrambi in versione build). Poiché l'applicazione C # è molto semplice e alloca la memoria utilizzata nel ciclo sullo stack e non sull'heap, in realtà funziona molto meglio di un'applicazione reale coinvolta in oggetti, calcoli pesanti e con set di dati più grandi. Quindi le cifre fornite sono cifre ottimistiche orientate verso C # e il framework .NET. Anche con questo pregiudizio, l'applicazione C ++ viene completata in poco più della metà del tempo rispetto all'applicazione C # equivalente. Tieni presente che il compilatore Microsoft C ++ che ho usato non aveva la pipeline corretta e le ottimizzazioni hyperthreading (utilizzando WinDBG per visualizzare le istruzioni di assemblaggio).

Ora, se utilizziamo il compilatore Intel (che tra l'altro è un segreto del settore per la generazione di applicazioni ad alte prestazioni su processori AMD / Intel), lo stesso codice viene eseguito in 0,54 secondi per l'eseguibile C ++ rispetto a .72 secondi utilizzando Microsoft Visual Studio 2010 Quindi, alla fine, i risultati finali sono 0,54 secondi per C ++ e 1,16 secondi per C #. Quindi il codice prodotto dal compilatore .NET JIT impiega il 214% di volte in più rispetto all'eseguibile C ++. La maggior parte del tempo speso nei 0,54 secondi era per ottenere il tempo dal sistema e non all'interno del ciclo stesso!

Ciò che manca nelle statistiche sono i tempi di avvio e pulizia che non sono inclusi nei tempi. Le applicazioni C # tendono a dedicare molto più tempo all'avvio e alla chiusura rispetto alle applicazioni C ++. Il motivo alla base di ciò è complicato e ha a che fare con le routine di convalida del codice runtime .NET e il sottosistema di gestione della memoria che esegue molto lavoro all'inizio (e di conseguenza, alla fine) del programma per ottimizzare le allocazioni di memoria e la spazzatura collettore.

Quando si misurano le prestazioni di C ++ e .NET IL, è importante esaminare il codice dell'assembly per assicurarsi che TUTTI i calcoli siano presenti. Quello che ho scoperto è che senza inserire del codice aggiuntivo in C #, la maggior parte del codice negli esempi precedenti è stata effettivamente rimossa dal file binario. Questo era anche il caso di C ++ quando si utilizzava un ottimizzatore più aggressivo come quello fornito con il compilatore Intel C ++. I risultati che ho fornito sopra sono corretti al 100% e convalidati a livello di assemblaggio.

Il problema principale con molti forum su Internet è che molti principianti ascoltano la propaganda di marketing di Microsoft senza comprendere la tecnologia e fanno false affermazioni che C # è più veloce di C ++. L'affermazione è che in teoria C # è più veloce di C ++ perché il compilatore JIT può ottimizzare il codice per la CPU. Il problema con questa teoria è che nel framework .NET esistono molte tubature che rallentano le prestazioni; impianto idraulico che non esiste nell'applicazione C ++. Inoltre, uno sviluppatore esperto conoscerà il compilatore giusto da utilizzare per la piattaforma data e utilizzerà i flag appropriati durante la compilazione dell'applicazione. Sulle piattaforme Linux o open source, questo non è un problema perché potresti distribuire il tuo sorgente e creare script di installazione che compilano il codice utilizzando l'ottimizzazione appropriata. Su Windows o piattaforma closed source, dovrai distribuire più eseguibili, ciascuno con ottimizzazioni specifiche. I file binari di Windows che verranno distribuiti sono basati sulla CPU rilevata dal programma di installazione msi (utilizzando azioni personalizzate).


22
1. Microsoft non ha mai affermato che C # sia più veloce, le loro affermazioni sono circa il 90% della velocità, più veloce da sviluppare (e quindi più tempo per la messa a punto) e più privo di bug grazie alla memoria e alla sicurezza dei tipi. Tutto ciò è vero (ho 20 anni in C ++ e 10 in C #) 2. Le prestazioni di avvio non hanno senso nella maggior parte dei casi. 3. Ci sono anche compilatori C # più veloci come LLVM (quindi portare Intel non è Apples to Apples)
ben

13
Le prestazioni di avvio non sono prive di significato. È molto importante nella maggior parte delle applicazioni aziendali basate sul Web, motivo per cui Microsoft ha introdotto le pagine Web da precaricare (avvio automatico) in .NET 4.0. Quando il pool di applicazioni viene riciclato di tanto in tanto, la prima volta che ogni pagina viene caricata aggiungerà un ritardo significativo per le pagine complesse e causerà timeout nel browser.
Richard

8
Microsoft ha affermato che le prestazioni di .NET sono più veloci nel materiale di marketing precedente. Hanno anche affermato che il garbage collector ha avuto un impatto minimo o nullo sulle prestazioni. Alcune di queste affermazioni sono state pubblicate in vari libri (su ASP.NET e .NET) nelle loro precedenti edizioni. Sebbene Microsoft non dica specificamente che la tua applicazione C # sarà più veloce della tua applicazione C ++, potrebbero fare commenti generici e slogan di marketing come "Just-In-Time Means Run-It-Fast" ( msdn.microsoft.com/ it / library / ms973894.aspx ).
Richard

71
-1, questo sproloquio è pieno di affermazioni errate e fuorvianti come l'ovvio whopper "Il codice C # non sarà MAI più veloce di un'applicazione C ++"
BCoates

32
-1. Dovresti leggere Rico Mariani vs Raymond Chen's C # vs C performance battle: blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx . In breve: uno dei ragazzi più intelligenti di Microsoft ha impiegato molte ottimizzazioni per rendere la versione C più veloce di una semplice versione C #.
Rolf Bjarne Kvinge

10

la mia prima ipotesi è un'ottimizzazione del compilatore perché non usi mai root. Devi solo assegnarlo, quindi sovrascriverlo ancora e ancora.

Modifica: accidenti, batti di 9 secondi!


2
Dico che hai ragione La variabile effettiva viene sovrascritta e non viene mai utilizzata oltre. Il csc molto probabilmente rinuncerebbe all'intero ciclo mentre il compilatore c ++ probabilmente lo lascerebbe dentro. Un test più accurato sarebbe quello di accumulare i risultati e quindi stampare quel risultato alla fine. Inoltre non si dovrebbe codificare il valore di inizializzazione in, ma piuttosto lasciarlo definito dall'utente. Questo non darebbe al compilatore c # alcuno spazio per tralasciare le cose.

7

Per vedere se il ciclo viene ottimizzato, prova a modificare il codice in

root += Math.Sqrt(i);

ans allo stesso modo nel codice C, quindi stampa il valore di root fuori dal ciclo.


6

Forse il compilatore c # sta notando che non usi root da nessuna parte, quindi salta l'intero ciclo for. :)

Potrebbe non essere così, ma sospetto che qualunque sia la causa, dipende dall'implementazione del compilatore. Prova a compilare il tuo programma C con il compilatore Microsoft (cl.exe, disponibile come parte di win32 sdk) con ottimizzazioni e modalità di rilascio. Scommetto che vedrai un miglioramento delle prestazioni rispetto all'altro compilatore.

EDIT: Non penso che il compilatore possa semplicemente ottimizzare il ciclo for, perché dovrebbe sapere che Math.Sqrt () non ha effetti collaterali.


2
Forse lo sa.

2
@ Neil, @jeff: D'accordo, potrebbe saperlo abbastanza facilmente. A seconda dell'implementazione, l'analisi statica su Math.Sqrt () potrebbe non essere così difficile, anche se non sono sicuro di quali ottimizzazioni vengano eseguite in modo specifico.
John Feminella

5

Qualunque sia il diff. può essere che il "tempo trascorso" non sia valido. Sarebbe valido solo se si può garantire che entrambi i programmi vengano eseguiti nelle stesse identiche condizioni.

Forse dovresti provare a vincere. equivalente a $ / usr / bin / time my_cprog; / usr / bin / time my_csprog


1
Perché questo è downvoted? Qualcuno presume che interruzioni e cambi di contesto non influiscano sulle prestazioni? Qualcuno può fare supposizioni su errori TLB, scambio di pagine, ecc.?
Tom

5

Ho messo insieme (in base al tuo codice) altri due test comparabili in C e C #. Questi due scrivono un array più piccolo usando l'operatore modulo per l'indicizzazione (aggiunge un po 'di overhead, ma hey, stiamo cercando di confrontare le prestazioni [a livello grezzo]).

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

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

Questi test scrivono i dati in un array (quindi il runtime .NET non dovrebbe essere autorizzato a cullare l'operazione sqrt) sebbene l'array sia significativamente più piccolo (non voleva usare memoria eccessiva). Li ho compilati nella configurazione della versione e li ho eseguiti dall'interno di una finestra della console (invece di iniziare tramite VS).

Sul mio computer il programma C # varia tra 6.2 e 6.9 secondi, mentre la versione C varia tra 6.9 e 7.1.


5

Se esegui un solo passaggio del codice a livello di assembly, incluso il passaggio attraverso la routine della radice quadrata, probabilmente otterrai la risposta alla tua domanda.

Non c'è bisogno di indovinare colto.


Mi piacerebbe sapere come farlo
Josh Stodola

Dipende dall'IDE o dal debugger. Pausa all'inizio del PGM. Visualizza la finestra di smontaggio e inizia a fare un passo singolo. Se si utilizza GDB, sono disponibili comandi per eseguire uno stepping di un'istruzione alla volta.
Mike Dunlavey,

Questo è un buon consiglio, questo aiuta a capire molto di più cosa sta realmente accadendo laggiù. Questo mostra anche ottimizzazioni JIT come inlining e tail call?
gjvdkamp

FYI: per me questo ha mostrato VC ++ usando fadd e fsqrt mentre C # usava cvtsi2sd e sqrtsd che, come ho capito, sono istruzioni SSE2 e quindi considerevolmente più veloci dove supportate.
danio

2

L'altro fattore che potrebbe essere un problema qui è che il compilatore C compila in codice nativo generico per la famiglia di processori target, mentre l'MSIL generato quando hai compilato il codice C # viene quindi compilato JIT per indirizzare il processore esatto che hai completo con qualsiasi ottimizzazioni possibili. Quindi il codice nativo generato dal C # potrebbe essere notevolmente più veloce del C.


In teoria sì. In pratica, ciò non fa praticamente mai una differenza misurabile. Una o due percentuali, forse, se sei fortunato.
jalf

oppure - se si dispone di un certo tipo di codice che utilizza estensioni non presenti nell'elenco consentito per il processore "generico". Cose come i sapori SSE. Prova con l'obiettivo del processore impostato più in alto, per vedere quali differenze ottieni.
gbjbaanb

1

Mi sembra che questo non abbia nulla a che fare con i linguaggi stessi, ma piuttosto con le diverse implementazioni della funzione radice quadrata.


Dubito fortemente che differenti implementazioni di sqrt causerebbero tanta disparità.
Alex Fort

Tanto più che, anche in C #, la maggior parte delle funzioni matematiche sono ancora considerate critiche per le prestazioni e vengono implementate come tali.
Matthew Olenik

fsqrt è un'istruzione del processore IA-32, quindi l'implementazione del linguaggio è irrilevante al giorno d'oggi.
Non sono sicuro

Entra nella funzione sqrt di MSVC con un debugger. Sta facendo molto di più che eseguire semplicemente l'istruzione fsqrt.
bk1e

1

In realtà ragazzi, il ciclo NON viene ottimizzato. Ho compilato il codice di John ed esaminato il file .exe risultante. Le viscere del ciclo sono le seguenti:

 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

A meno che il runtime non sia abbastanza intelligente da rendersi conto che il ciclo non fa nulla e lo salta?

Modifica: modifica del C # in modo che sia:

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

Risultati nel tempo trascorso (sulla mia macchina) da 0,047 a 2,17. Ma questo è solo il sovraccarico di aggiungere 100 milioni di operatori addizionali?


3
Guardare l'IL non ti dice molto sulle ottimizzazioni perché sebbene il compilatore C # esegua alcune cose come il ripiegamento costante e la rimozione del codice inattivo, l'IL prende il sopravvento e fa il resto al momento del caricamento.
Daniel Earwicker

Questo è quello che pensavo potesse essere il caso. Anche costringendolo a funzionare, tuttavia, è ancora 9 secondi più veloce della versione C. (Non me lo sarei aspettato affatto)
Dana
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.