C # vs C - Grande diferença de desempenho


94

Estou descobrindo enormes diferenças de desempenho entre códigos semelhantes em C anc C #.

O código 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 o C # (aplicativo de 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));
        }
    }
}

Com o código acima, o C # é concluído em 0,328125 segundos (versão de lançamento) e o C leva 11,14 segundos para ser executado.

OC está sendo compilado para um executável do Windows usando o mingw.

Sempre presumi que C / C ++ era mais rápido ou pelo menos comparável a C # .net. O que exatamente está fazendo o C funcionar 30 vezes mais lento?

EDITAR: Parece que o otimizador C # estava removendo a raiz porque ela não estava sendo usada. Mudei a atribuição de raiz para root + = e imprimi o total no final. Também compilei o C usando cl.exe com o sinalizador / O2 definido para velocidade máxima.

Os resultados são agora: 3,75 segundos para o C 2.61 segundos para o C #

OC ainda está demorando mais, mas isso é aceitável


18
Eu sugeriria que você use um StopWatch em vez de apenas um DateTime.
Alex Fort

2
Quais sinalizadores de compilador? Ambos são compilados com otimizações habilitadas?
jalf

2
E quando você usa -ffast-math com o compilador C ++?
Dan McClain

10
Que pergunta fascinante!
Robert S.

4
Talvez a função C sqrt não seja tão boa quanto esta em C #. Então não seria um problema com C, mas com a biblioteca anexada a ele. Experimente alguns cálculos sem funções matemáticas.
klew

Respostas:


61

Como você nunca usa 'root', o compilador pode ter removido a chamada para otimizar seu método.

Você pode tentar acumular os valores da raiz quadrada em um acumulador, imprimi-lo no final do método e ver o que está acontecendo.

Editar: veja a resposta de Jalf abaixo


1
Um pouco de experimentação sugere que não é o caso. O código para o loop é gerado, embora talvez o tempo de execução seja inteligente o suficiente para ignorá-lo. Mesmo acumulando, C # ainda bate nas calças de C.
Dana

3
Parece que o problema está do outro lado. C # se comporta razoavelmente em todos os casos. Seu código C aparentemente foi compilado sem otimizações
jalf

2
Muitos de vocês estão perdendo o ponto aqui. Tenho lido muitos casos semelhantes em que c # supera c / c ++ e sempre a refutação é empregar alguma otimização de nível de especialista. 99% dos programadores não têm conhecimento para usar essas técnicas de otimização apenas para fazer com que seu código seja executado um pouco mais rápido do que o código c #. Os casos de uso para c / c ++ estão se estreitando.

167

Você deve comparar compilações de depuração. Acabei de compilar seu código C e

Time elapsed: 0.000000

Se você não habilitar as otimizações, qualquer benchmarking que você fizer será completamente inútil. (E se você habilitar as otimizações, o loop é otimizado. Portanto, seu código de benchmarking também apresenta falhas. Você precisa forçá-lo a executar o loop, geralmente somando o resultado ou algo semelhante e imprimindo-o no final)

Parece que o que você está medindo é basicamente "qual compilador insere a maior sobrecarga de depuração". E a resposta é C. Mas isso não nos diz qual programa é mais rápido. Porque quando você quer velocidade, você habilita otimizações.

A propósito, você evitará muitas dores de cabeça a longo prazo se abandonar qualquer noção de que as línguas são "mais rápidas" que as outras. C # não tem mais velocidade do que o inglês.

Há certas coisas na linguagem C que seriam eficientes mesmo em um compilador ingênuo e não otimizador, e há outras que dependem muito de um compilador para otimizar tudo. E, claro, o mesmo vale para C # ou qualquer outra linguagem.

A velocidade de execução é determinada por:

  • a plataforma em que você está executando (sistema operacional, hardware, outro software em execução no sistema)
  • o compilador
  • seu código fonte

Um bom compilador C # produzirá um código eficiente. Um compilador C ruim gerará código lento. Que tal um compilador C que gerou código C #, que você poderia executar por meio de um compilador C #? Quão rápido isso funcionaria? Os idiomas não têm velocidade. Seu código sim.



18
Boa resposta, mas discordo sobre a velocidade da linguagem, pelo menos em analogia: foi descoberto que o Welsch é uma língua mais lenta do que a maioria por causa da alta frequência de vogais longas. Além disso, as pessoas se lembram das palavras (e listas de palavras) melhor se forem mais rápidas de dizer. web.missouri.edu/~cowann/docs/articles/before%201993/… en.wikipedia.org/wiki/Vowel_length en.wikipedia.org/wiki/Welsh_language
exceptionerror

1
Isso não depende do que você está dizendo em Welsch? Acho improvável que tudo seja mais lento.
jalf

5
++ Ei pessoal, não se desviem aqui. Se o mesmo programa for executado mais rápido em uma linguagem do que em outra, é porque um código assembly diferente é gerado. Neste exemplo específico, 99% ou mais do tempo ficará flutuando ie sqrt, portanto, é isso que está sendo medido.
Mike Dunlavey

116

Vou ser breve, já está marcado como respondido. C # tem a grande vantagem de ter um modelo de ponto flutuante bem definido. Isso simplesmente coincide com o modo de operação nativo das instruções FPU e SSE definidas nos processadores x86 e x64. Nenhuma coincidência aí. O JITter compila Math.Sqrt () para algumas instruções embutidas.

C / C ++ nativo está sobrecarregado com anos de compatibilidade com versões anteriores. As opções de compilação / fp: precise, / fp: fast e / fp: strict são as mais visíveis. Da mesma forma, ele deve chamar uma função CRT que implementa sqrt () e verifica as opções de ponto flutuante selecionadas para ajustar o resultado. Isso é lento.


66
Esta é uma convicção estranha entre os programadores C ++, eles parecem pensar que o código de máquina gerado pelo C # é de alguma forma diferente do código de máquina gerado por um compilador nativo. Existe apenas um tipo. Não importa qual opção do compilador gcc você usa ou assembly inline que você escreve, ainda há apenas uma instrução FSQRT. Nem sempre é mais rápido porque um idioma nativo o gerou, a cpu não liga.
Hans Passant

16
Isso é o que o pre-jitting com ngen.exe resolve. Estamos falando sobre C #, não Java.
Hans Passant

20
@ user877329 - realmente? Uau.
Andras Zoltan

7
Não, o jitter x64 usa SSE. Math.Sqrt () é traduzido para a instrução de código de máquina sqrtsd.
Hans Passant

6
Embora tecnicamente não seja uma diferença entre as linguagens, o .net JITter faz otimizações bastante limitadas em comparação com um compilador C / C ++ típico. Uma das maiores limitações é a falta de suporte SIMD, tornando o código quase 4x mais lento. Não expor muitos intrínsecos também pode ser um grande malus, mas isso depende muito do que você está fazendo.
CodesInChaos

57

Sou um desenvolvedor C ++ e C #. Tenho desenvolvido aplicativos C # desde a primeira versão beta do .NET framework e tenho mais de 20 anos de experiência no desenvolvimento de aplicativos C ++. Em primeiro lugar, o código C # NUNCA será mais rápido do que um aplicativo C ++, mas não vou passar por uma longa discussão sobre o código gerenciado, como ele funciona, a camada de interoperabilidade, componentes internos de gerenciamento de memória, o sistema de tipo dinâmico e o coletor de lixo. No entanto, deixe-me continuar dizendo que todos os benchmarks listados aqui produzem resultados INCORRETOS.

Deixe-me explicar: a primeira coisa que precisamos considerar é o compilador JIT para C # (.NET Framework 4). Agora, o JIT produz código nativo para a CPU usando vários algoritmos de otimização (que tendem a ser mais agressivos do que o otimizador C ++ padrão que vem com o Visual Studio) e o conjunto de instruções usado pelo compilador .NET JIT é um reflexo mais próximo da CPU real na máquina, de modo que certas substituições no código da máquina possam ser feitas para reduzir os ciclos de clock e melhorar a taxa de acerto no cache do pipeline da CPU e produzir mais otimizações de hyper-threading, como reordenamento de instruções e melhorias relacionadas à previsão de ramificação.

O que isso significa é que, a menos que você compile seu aplicativo C ++ usando os parâmetros corretos para o build RELEASE (não o build DEBUG), seu aplicativo C ++ pode executar mais lentamente do que o aplicativo baseado em C # ou .NET correspondente. Ao especificar as propriedades do projeto em seu aplicativo C ++, certifique-se de habilitar "otimização total" e "favorecer código rápido". Se você tiver uma máquina de 64 bits, você DEVE especificar gerar x64 como a plataforma de destino, caso contrário, seu código será executado por meio de uma subcamada de conversão (WOW64) que reduzirá substancialmente o desempenho.

Depois de executar as otimizações corretas no compilador, obtenho 0,72 segundo para o aplicativo C ++ e 1,16 segundo para o aplicativo C # (ambos na versão de compilação). Como o aplicativo C # é muito básico e aloca a memória usada no loop na pilha e não no heap, ele está, na verdade, tendo um desempenho muito melhor do que um aplicativo real envolvido em objetos, cálculos pesados ​​e com conjuntos de dados maiores. Portanto, os números fornecidos são números otimistas tendenciosos para C # e a estrutura .NET. Mesmo com essa tendência, o aplicativo C ++ é concluído em pouco mais da metade do tempo do que o aplicativo C # equivalente. Lembre-se de que o compilador Microsoft C ++ que usei não tinha o pipeline correto e as otimizações de hyperthreading (usando WinDBG para visualizar as instruções de montagem).

Agora, se usarmos o compilador Intel (que, a propósito, é um segredo da indústria para gerar aplicativos de alto desempenho em processadores AMD / Intel), o mesmo código será executado em 0,54 segundos para o executável C ++ versus 0,72 segundos usando o Microsoft Visual Studio 2010 . Portanto, no final, os resultados finais são 0,54 segundos para C ++ e 1,16 segundos para C #. Portanto, o código produzido pelo compilador .NET JIT leva 214% mais tempo do que o executável C ++. A maior parte do tempo gasto nos 0,54 segundos foi para obter o tempo do sistema e não dentro do próprio loop!

O que também está faltando nas estatísticas são os tempos de inicialização e limpeza, que não estão incluídos nas temporizações. Os aplicativos C # tendem a gastar muito mais tempo na inicialização e no encerramento do que os aplicativos C ++. A razão por trás disso é complicada e tem a ver com as rotinas de validação de código em tempo de execução .NET e o subsistema de gerenciamento de memória que executa muito trabalho no início (e, conseqüentemente, no final) do programa para otimizar as alocações de memória e o lixo colecionador.

Ao medir o desempenho de C ++ e .NET IL, é importante observar o código do assembly para ter certeza de que TODOS os cálculos estão lá. O que descobri é que, sem colocar algum código adicional em C #, a maior parte do código nos exemplos acima foi realmente removida do binário. Este também foi o caso com C ++ quando você usou um otimizador mais agressivo, como o que vem com o compilador Intel C ++. Os resultados que forneci acima são 100% corretos e validados no nível de montagem.

O principal problema com muitos fóruns na internet é que muitos novatos ouvem a propaganda de marketing da Microsoft sem entender a tecnologia e fazem falsas alegações de que C # é mais rápido que C ++. A alegação é que, em teoria, C # é mais rápido que C ++ porque o compilador JIT pode otimizar o código para a CPU. O problema com essa teoria é que existem muitos encanamentos na estrutura .NET que tornam o desempenho mais lento; encanamento que não existe no aplicativo C ++. Além disso, um desenvolvedor experiente saberá o compilador certo a ser usado para a plataforma fornecida e usará os sinalizadores apropriados ao compilar o aplicativo. Nas plataformas Linux ou de código aberto, isso não é um problema porque você pode distribuir sua origem e criar scripts de instalação que compilam o código usando a otimização apropriada. Na plataforma Windows ou de código fechado, você terá que distribuir vários executáveis, cada um com otimizações específicas. Os binários do Windows que serão implantados são baseados na CPU detectada pelo instalador msi (usando ações personalizadas).


22
1. A Microsoft nunca fez essas afirmações sobre o C # ser mais rápido, suas afirmações são cerca de 90% da velocidade, mais rápido para desenvolver (e, portanto, mais tempo para ajustar) e mais livre de bugs devido à memória e segurança de tipo. Tudo isso é verdade (tenho 20 anos em C ++ e 10 em C #) 2. O desempenho de inicialização não faz sentido na maioria dos casos. 3. Existem também compiladores C # mais rápidos como LLVM (então trazer Intel não é Apples to Apples)
ben

13
O desempenho de inicialização não é insignificante. É muito importante na maioria dos aplicativos corporativos baseados na Web, por isso a Microsoft introduziu as páginas da Web para serem pré-carregadas (autostart) no .NET 4.0. Quando o pool de aplicativos é reciclado de vez em quando, a primeira vez que cada página for carregada adicionará um atraso significativo para páginas complexas e causará tempos limite no navegador.
Richard

8
A Microsoft afirmou que o desempenho do .NET era mais rápido em materiais de marketing anteriores. Eles também fizeram várias afirmações sobre o coletor de lixo que teve pouco ou nenhum impacto no desempenho. Algumas dessas afirmações apareceram em vários livros (sobre ASP.NET e .NET) em suas edições anteriores. Embora a Microsoft não diga especificamente que seu aplicativo C # será mais rápido do que seu aplicativo C ++, eles podem varrer comentários genéricos e slogans de marketing como "Just-In-Time Significa Run-It-Fast" ( msdn.microsoft.com/ en-us / library / ms973894.aspx ).
Richard

71
-1, este discurso está cheio de declarações incorretas e enganosas, como o óbvio "código C # NUNCA será mais rápido que um aplicativo C ++"
BCoates

32
-1. Você deve ler a batalha de desempenho C # vs C de Rico Mariani vs Raymond Chen: blogs.msdn.com/b/ricom/archive/2005/05/16/418051.aspx . Resumindo: um dos caras mais espertos da Microsoft precisou de muita otimização para tornar a versão C mais rápida do que uma simples versão C #.
Rolf Bjarne Kvinge

10

meu primeiro palpite é uma otimização do compilador porque você nunca usa root. Você apenas atribui e sobrescreve novamente e novamente.

Edit: droga, venceu por 9 segundos!


2
Eu digo que você está correto. A variável real é substituída e nunca usada além disso. O csc provavelmente abandonaria todo o loop enquanto o compilador c ++ provavelmente o deixaria. Um teste mais preciso seria acumular os resultados e imprimir esse resultado no final. Também não se deve codificar permanentemente o valor da semente, mas sim deixá-lo ser definido pelo usuário. Isso não daria ao compilador c # espaço para deixar coisas de fora.

7

Para ver se o loop está sendo otimizado, tente alterar seu código para

root += Math.Sqrt(i);

ans da mesma forma no código C, e então imprimir o valor de root fora do loop.


6

Talvez o compilador c # esteja percebendo que você não usa root em nenhum lugar, então ele simplesmente pula todo o loop for. :)

Pode não ser o caso, mas suspeito que seja qual for a causa, é dependente da implementação do compilador. Tente compilar seu programa C com o compilador Microsoft (cl.exe, disponível como parte do win32 sdk) com otimizações e modo de lançamento. Aposto que você verá uma melhoria de desempenho em relação ao outro compilador.

EDIT: Eu não acho que o compilador pode apenas otimizar o loop for, porque ele teria que saber que Math.Sqrt () não tem efeitos colaterais.


2
Talvez saiba disso.

2
@Neil, @jeff: Concordo, ele poderia saber disso facilmente. Dependendo da implementação, a análise estática em Math.Sqrt () pode não ser tão difícil, embora eu não tenha certeza de quais otimizações são realizadas especificamente.
John Feminella

5

Seja qual for o tempo diff. pode ser, esse "tempo decorrido" é inválido. Só seria válido se você pudesse garantir que ambos os programas rodariam exatamente nas mesmas condições.

Talvez você devesse tentar uma vitória. equivalente a $ / usr / bin / time my_cprog; / usr / bin / time my_csprog


1
Por que isso foi rejeitado? Alguém está assumindo que as interrupções e mudanças de contexto não afetam o desempenho? Alguém pode fazer suposições sobre falhas de TLB, troca de página, etc?
Tom,

5

Eu reuni (com base em seu código) mais dois testes comparáveis ​​em C e C #. Esses dois escrevem uma matriz menor usando o operador de módulo para indexação (adiciona um pouco de overhead, mas, ei, estamos tentando comparar o desempenho [em um nível bruto]).

Código 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);
}

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

Esses testes gravam dados em um array (portanto, o tempo de execução do .NET não deve ter permissão para eliminar o sqrt op), embora o array seja significativamente menor (não queria usar memória excessiva). Compilei-os na configuração de lançamento e executei-os de dentro de uma janela de console (em vez de começar pelo VS).

No meu computador, o programa C # varia entre 6,2 e 6,9 ​​segundos, enquanto a versão C varia entre 6,9 ​​e 7.1.


5

Se você fizer uma única etapa do código no nível do assembly, incluindo a etapa de rotina da raiz quadrada, provavelmente obterá a resposta para sua pergunta.

Não há necessidade de adivinhação educada.


Eu gostaria de saber como fazer isso
Josh Stodola

Depende do seu IDE ou depurador. Faça uma pausa no início do pgm. Exiba a janela de desmontagem e comece a executar uma etapa. Se estiver usando GDB, existem comandos para avançar uma instrução por vez.
Mike Dunlavey

Essa é uma boa dica, ajuda a entender muito mais o que realmente está acontecendo lá embaixo. Isso também mostra otimizações JIT, como chamadas inlining e tail?
gjvdkamp

FYI: para mim, isso mostrou VC ++ usando fadd e fsqrt, enquanto C # usava cvtsi2sd e sqrtsd que, pelo que entendi, são instruções SSE2 e, portanto, consideravelmente mais rápidas onde suportadas.
danio

2

O outro fator que pode ser um problema aqui é que o compilador C compila para o código nativo genérico para a família de processadores que você almeja, enquanto o MSIL gerado quando você compilou o código C # é então JIT compilado para atingir o processador exato que você completou com qualquer otimizações que podem ser possíveis. Portanto, o código nativo gerado a partir do C # pode ser consideravelmente mais rápido do que o C.


Em teoria, sim. Na prática, isso praticamente nunca faz uma diferença mensurável. Um ou dois por cento, talvez, se você tiver sorte.
jalf

ou - se você tiver determinado tipo de código que usa extensões que não estão na lista permitida para o processador 'genérico'. Coisas como sabores SSE. Experimente com a meta do processador mais alta, para ver quais diferenças você consegue.
gbjbaanb

1

Parece-me que isso não tem nada a ver com as próprias linguagens, mas sim com as diferentes implementações da função de raiz quadrada.


Duvido muito que implementações de sqrt diferentes causem tanta disparidade.
Alex Fort

Especialmente porque, mesmo em C #, a maioria das funções matemáticas ainda são consideradas críticas para o desempenho e são implementadas como tal.
Matthew Olenik

fsqrt é uma instrução do processador IA-32, portanto, a implementação da linguagem é irrelevante atualmente.
Não tenho certeza,

Entre na função sqrt do MSVC com um depurador. Ele está fazendo muito mais do que apenas executar a instrução fsqrt.
bk1e

1

Na verdade pessoal, o loop NÃO está sendo otimizado. Compilei o código de John e examinei o .exe resultante. As entranhas do loop são as seguintes:

 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 menos que o tempo de execução seja inteligente o suficiente para perceber que o loop não faz nada e o pula?

Edit: Alterando o C # para ser:

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

Resulta no tempo decorrido (na minha máquina) indo de 0,047 a 2,17. Mas isso é apenas a sobrecarga de adicionar 100 milhões de operadores adicionais?


3
Olhar para o IL não diz muito sobre otimizações porque, embora o compilador C # faça algumas coisas como dobrar constantemente e remover código morto, o IL assume o controle e faz o resto no tempo de carregamento.
Daniel Earwicker

Isso é o que eu pensei que poderia ser o caso. Mesmo forçando-o a funcionar, porém, ainda é 9 segundos mais rápido que a versão C. (Eu não esperava isso de forma alguma)
Dana
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.