A linguagem assembly embutida é mais lenta que o código C ++ nativo?


183

Tentei comparar o desempenho da linguagem assembly embutida e do código C ++, então escrevi uma função que adiciona duas matrizes de tamanho 2000 por 100000 vezes. Aqui está o código:

#define TIMES 100000
void calcuC(int *x,int *y,int length)
{
    for(int i = 0; i < TIMES; i++)
    {
        for(int j = 0; j < length; j++)
            x[j] += y[j];
    }
}


void calcuAsm(int *x,int *y,int lengthOfArray)
{
    __asm
    {
        mov edi,TIMES
        start:
        mov esi,0
        mov ecx,lengthOfArray
        label:
        mov edx,x
        push edx
        mov eax,DWORD PTR [edx + esi*4]
        mov edx,y
        mov ebx,DWORD PTR [edx + esi*4]
        add eax,ebx
        pop edx
        mov [edx + esi*4],eax
        inc esi
        loop label
        dec edi
        cmp edi,0
        jnz start
    };
}

Aqui está main():

int main() {
    bool errorOccured = false;
    setbuf(stdout,NULL);
    int *xC,*xAsm,*yC,*yAsm;
    xC = new int[2000];
    xAsm = new int[2000];
    yC = new int[2000];
    yAsm = new int[2000];
    for(int i = 0; i < 2000; i++)
    {
        xC[i] = 0;
        xAsm[i] = 0;
        yC[i] = i;
        yAsm[i] = i;
    }
    time_t start = clock();
    calcuC(xC,yC,2000);

    //    calcuAsm(xAsm,yAsm,2000);
    //    for(int i = 0; i < 2000; i++)
    //    {
    //        if(xC[i] != xAsm[i])
    //        {
    //            cout<<"xC["<<i<<"]="<<xC[i]<<" "<<"xAsm["<<i<<"]="<<xAsm[i]<<endl;
    //            errorOccured = true;
    //            break;
    //        }
    //    }
    //    if(errorOccured)
    //        cout<<"Error occurs!"<<endl;
    //    else
    //        cout<<"Works fine!"<<endl;

    time_t end = clock();

    //    cout<<"time = "<<(float)(end - start) / CLOCKS_PER_SEC<<"\n";

    cout<<"time = "<<end - start<<endl;
    return 0;
}

Então eu executo o programa cinco vezes para obter os ciclos do processador, que podem ser vistos como tempo. Cada vez que chamo apenas uma das funções mencionadas acima.

E aí vem o resultado.

Função da versão de montagem:

Debug   Release
---------------
732        668
733        680
659        672
667        675
684        694
Average:   677

Função da versão C ++:

Debug     Release
-----------------
1068      168
 999      166
1072      231
1002      166
1114      183
Average:  182

O código C ++ no modo de liberação é quase 3,7 vezes mais rápido que o código do assembly. Por quê?

Acho que o código do assembly que escrevi não é tão eficaz quanto os gerados pelo GCC. É difícil para um programador comum como eu escrever código mais rápido que seu oponente gerado por um compilador. Isso significa que eu não devo confiar no desempenho da linguagem assembly escrita por minhas mãos, focar em C ++ e esquecer a linguagem assembly?


29
Bastante. O assembly codificado manualmente é apropriado em algumas circunstâncias, mas é necessário ter cuidado para garantir que a versão do assembly seja realmente mais rápida do que o que pode ser alcançado com uma linguagem de nível superior.
Magnus Hoff

161
Você pode achar instrutivo estudar o código gerado pelo compilador e tentar entender por que é mais rápido que a sua versão de montagem.
Paul R

34
Sim, parece que o compilador é melhor em escrever asm do que você. Compiladores modernos são realmente muito bons.
David Heffernan

20
Você já viu a montagem que o GCC produziu? Seu possível GCC usou as instruções da MMX. Sua função é muito paralela - você pode potencialmente usar N processadores para calcular a soma 1/1 da hora. Tente uma função em que não há esperança de paralelização.
31712 Chris

11
Hm, eu teria esperado um bom compilador para fazer isso ~ 100000 vezes mais rápido ...
PlasmaHH

Respostas:


261

Sim, na maioria das vezes.

Antes de tudo, você começa com a suposição errada de que uma linguagem de baixo nível (assembly neste caso) sempre produzirá código mais rápido que a linguagem de alto nível (C ++ e C neste caso). Não é verdade. O código C é sempre mais rápido que o código Java? Não, porque existe outra variável: programador. A maneira como você escreve código e o conhecimento dos detalhes da arquitetura influenciam muito o desempenho (como você viu neste caso).

Você sempre pode produzir um exemplo em que o código de montagem artesanal é melhor que o código compilado, mas geralmente é um exemplo fictício ou uma única rotina, não um verdadeiro programa de mais de 500.000 linhas de código C ++. Eu acho que compiladores irá produzir melhor montagem código de 95% vezes e , por vezes, apenas algumas raras vezes, você pode precisar de escrever montagem código para alguns curta,, muito utilizado , desempenho crítico rotinas ou quando você tem para acessar os recursos sua linguagem favorita de alto nível não expõe. Você quer um toque dessa complexidade? Leia esta resposta incrível aqui no SO.

Porque isso?

Antes de tudo, porque os compiladores podem fazer otimizações que nem imaginamos (veja esta lista curta ) e elas farão em segundos (quando precisarmos de dias ).

Ao codificar na montagem, é necessário criar funções bem definidas com uma interface de chamada bem definida. No entanto, eles podem levar em consideração a otimização de todo o programa e a otimização entre procedimentos , como alocação de registros , propagação constante , eliminação de subexpressão comum , programação de instruções e outras otimizações complexas e não óbvias ( modelo Polytope , por exemplo). Na arquitetura RISC , os caras pararam de se preocupar com isso há muitos anos (o agendamento de instruções, por exemplo, é muito difícil de ajustar manualmente ) e as CPUs CISC modernas têm pipelines muito longos também.

Para alguns microcontroladores complexos, até as bibliotecas do sistema são escritas em C, em vez de assembly, porque seus compiladores produzem um código final melhor (e fácil de manter).

Às vezes, os compiladores podem usar automaticamente algumas instruções do MMX / SIMDx e, se você não as usar, simplesmente não pode comparar (outras respostas já revisaram muito bem o seu código de montagem). Apenas para loops, esta é uma pequena lista de otimizações de loop do que é comumente verificado por um compilador (você acha que poderia fazê-lo sozinho quando sua agenda foi decidida para um programa C #?) Se você escrever algo em assembly, eu pense que você deve considerar pelo menos algumas otimizações simples . O exemplo de livro escolar para matrizes é desenrolar o ciclo (seu tamanho é conhecido no momento da compilação). Faça isso e execute seu teste novamente.

Hoje em dia também é realmente incomum precisar usar a linguagem assembly por outro motivo: a infinidade de CPUs diferentes . Deseja apoiar todos eles? Cada um possui uma microarquitetura específica e alguns conjuntos de instruções específicos . Eles têm um número diferente de unidades funcionais e as instruções de montagem devem ser organizadas para mantê-las todas ocupadas . Se você escreve em C, pode usar o PGO, mas no assembly precisará de um grande conhecimento dessa arquitetura específica (e repensar e refazer tudo para outra arquitetura ). Para tarefas pequenas, o compilador geralmente melhora e, para tarefas complexas, geralmente o trabalho não é reembolsado (ecompilador pode fazer melhor de qualquer maneira).

Se você se sentar e der uma olhada no seu código, provavelmente verá que ganhará mais para redesenhar seu algoritmo do que traduzir para assembly (leia este ótimo post aqui no SO ), há otimizações de alto nível (e dicas para o compilador), você pode aplicar efetivamente antes de precisar recorrer à linguagem assembly. Provavelmente vale a pena mencionar que muitas vezes usando intrínsecos você terá um ganho de desempenho que está procurando e o compilador ainda poderá executar a maioria de suas otimizações.

Tudo isso dito, mesmo quando você pode produzir um código de montagem 5 a 10 vezes mais rápido, pergunte aos seus clientes se eles preferem pagar uma semana do seu tempo ou comprar uma CPU 50 $ mais rápida . A otimização extrema mais frequentemente do que não (e especialmente em aplicativos LOB) simplesmente não é necessária para a maioria de nós.


9
Claro que não. Eu acho que é melhor de 95% das pessoas em 99% das vezes. Às vezes, porque é simplesmente caro (por causa de matemática complexa ) ou gasto de tempo (depois caro novamente). Às vezes, porque nós simplesmente se esqueceu sobre otimizações ...
Adriano Repetti

62
@ ja72 - não, não é melhor escrever código. É melhor na otimização de código.
Mike Baranczak 7/03/12

14
É contra-intuitivo até você realmente considerar. Da mesma forma, as máquinas baseadas em VM estão começando a fazer otimizações de tempo de execução que os compiladores simplesmente não têm as informações a fazer.
Bill K

6
@ M28: Os compiladores podem usar as mesmas instruções. Claro, eles pagam por isso em termos de tamanho binário (porque precisam fornecer um caminho de fallback caso essas instruções não sejam suportadas). Além disso, na maioria das vezes, as "novas instruções" que seriam adicionadas são instruções SMID de qualquer maneira, que as VMs e os Compiladores são horríveis de utilizar. As VMs pagam por esse recurso, pois precisam compilar o código na inicialização.
Billy ONeal

9
@ BillK: PGO faz a mesma coisa para compiladores.
Billy ONeal

194

Seu código de montagem é abaixo do ideal e pode ser aprimorado:

  • Você está pressionando e exibindo um registro ( EDX ) em seu loop interno. Isso deve ser movido para fora do loop.
  • Você recarrega os ponteiros da matriz em todas as iterações do loop. Isso deve sair do loop.
  • Você usa a loopinstrução, que é conhecida como lenta nas CPUs mais modernas (possivelmente um resultado do uso de um livro de montagem antigo *)
  • Você não tira vantagem do desenrolar manual do loop.
  • Você não usa as instruções SIMD disponíveis .

Portanto, a menos que você melhore bastante seu conjunto de habilidades em relação ao assembler, não faz sentido escrever código do assembler para desempenho.

* É claro que não sei se você realmente recebeu as loopinstruções de um livro antigo de montagem. Mas você quase nunca o vê no código do mundo real, como todo compilador por aí é inteligente o suficiente para não emitir loop, você o vê apenas em livros ruins e desatualizados da IMHO.


compiladores ainda pode emitir loop(e muitos "obsoleta" instruções) se você otimizar para tamanho
phuclv

1
@ phuclv bem, sim, mas a pergunta original era exatamente sobre velocidade, não tamanho.
IGR94

60

Mesmo antes de se aprofundar na montagem, existem transformações de código que existem em um nível superior.

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
  for (int i = 0; i < TIMES; i++) {
    for (int j = 0; j < length; j++) {
      x[j] += y[j];
    }
  }
}

pode ser transformado em rotação de loop :

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      for (int i = 0; i < TIMES; ++i) {
        x[j] += y[j];
      }
    }
}

o que é muito melhor no que diz respeito à localização da memória.

Isso pode ser otimizado ainda mais, fazer a += bX vezes é equivalente a fazê- a += X * blo, obtemos:

static int const TIMES = 100000;

void calcuC(int *x, int *y, int length) {
    for (int j = 0; j < length; ++j) {
      x[j] += TIMES * y[j];
    }
}

no entanto, parece que meu otimizador favorito (LLVM) não realiza essa transformação.

[editar] Eu descobri que a transformação é executada se tivéssemos o restrictqualificador para xe y. Na verdade, sem essa restrição, x[j]e y[j]poderia alias para o mesmo local que torna esta transformação errônea. [fim de edição]

De qualquer forma, acho que essa é a versão C otimizada. Já é muito mais simples. Com base nisso, aqui está o meu crack no ASM (deixei Clang gerá-lo, sou inútil):

calcuAsm:                               # @calcuAsm
.Ltmp0:
    .cfi_startproc
# BB#0:
    testl   %edx, %edx
    jle .LBB0_2
    .align  16, 0x90
.LBB0_1:                                # %.lr.ph
                                        # =>This Inner Loop Header: Depth=1
    imull   $100000, (%rsi), %eax   # imm = 0x186A0
    addl    %eax, (%rdi)
    addq    $4, %rsi
    addq    $4, %rdi
    decl    %edx
    jne .LBB0_1
.LBB0_2:                                # %._crit_edge
    ret
.Ltmp1:
    .size   calcuAsm, .Ltmp1-calcuAsm
.Ltmp2:
    .cfi_endproc

Receio não entender de onde vêm todas essas instruções, mas você sempre pode se divertir e tentar ver como elas se comparam ... mas ainda assim usaria a versão C otimizada em vez da montagem, no código, muito mais portável.


Obrigado pela sua resposta. Bem, é um pouco confuso que, quando fiz a classe chamada "Princípios do compilador", aprendi que o compilador otimizará nosso código de várias maneiras. Isso significa que precisamos otimizar nosso código manualmente? Podemos fazer um trabalho melhor que o compilador? Essa é a pergunta que sempre me confunde.
precisa saber é o seguinte

2
@ user957121: podemos otimizá-lo melhor quando tivermos mais informações. Especificamente aqui, o que dificulta o compilador é o possível alias entre xe y. Ou seja, o compilador não pode ter certeza que para todos i,jno [0, length)que temos x + i != y + j. Se houver sobreposição, a otimização é impossível. A linguagem C introduziu a restrictpalavra-chave para informar ao compilador que dois ponteiros não podem usar o pseudônimo, no entanto, ele não funciona para matrizes, porque ainda podem se sobrepor, mesmo que não façam exatamente o pseudônimo.
Matthieu M.

O GCC atual e o Clang vetorizam automaticamente (após verificar se não há sobreposição, se você omitir __restrict). O SSE2 é a linha de base para x86-64 e, com o embaralhamento, o SSE2 pode fazer multiplicações 2x de 32 bits de uma só vez (produzindo produtos de 64 bits, daí o embaralhamento para reunir os resultados). godbolt.org/z/r7F_uo . (SSE4.1 é necessário para pmulld: 32x32 compactado => multiplicação de 32 bits). O GCC tem um truque para transformar multiplicadores inteiros constantes em shift / add (e / ou subtrair), o que é bom para multiplicadores com poucos bits definidos. O código de embaralhamento pesado de Clang vai prejudicar o rendimento de embaralhamento nas CPUs Intel.
Peter Cordes

41

Resposta curta: sim.

Resposta longa: sim, a menos que você realmente saiba o que está fazendo e tenha um motivo para fazê-lo.


3
e só então se você executar um nível de montagem ferramenta de perfil como VTune para chips Intel para ver onde você pode ser capaz de melhorar as coisas
Mark Mullin

1
Isso tecnicamente responde à pergunta, mas também é completamente inútil. A -1 de mim.
Navin

2
Resposta muito longa: "Sim, a menos que você queira alterar todo o seu código sempre que uma nova (er) CPU for usada. Escolha o melhor algoritmo, mas deixe o compilador fazer a otimização"
Tommylee2k

35

Eu corrigi meu código asm:

  __asm
{   
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,1
    mov edi,y
label:
    movq mm0,QWORD PTR[esi]
    paddd mm0,QWORD PTR[edi]
    add edi,8
    movq QWORD PTR[esi],mm0
    add esi,8
    dec ecx 
    jnz label
    dec ebx
    jnz start
};

Resultados para a versão Release:

 Function of assembly version: 81
 Function of C++ version: 161

O código de montagem no modo de liberação é quase duas vezes mais rápido que o C ++.


18
Agora, se você começar a usar SSE em vez de MMX (nome de registro é xmm0em vez de mm0), você vai ter outra aceleração por um fator de dois ;-)
Gunther Piez

8
Eu mudei, tenho 41 para a versão de montagem. É em 4 vezes mais rápido :)
sasha

3
também pode obter até 5% mais se usam todos os registros XMM
sasha

7
Agora, se você pensar no tempo que realmente levou: montagem, cerca de 10 horas ou mais? C ++, alguns minutos, eu acho? Há um vencedor claro aqui, a menos que seja um código crítico de desempenho.
Calimo 23/07

1
Um bom compilador já será vetorizado automaticamente com paddd xmm(após verificar a sobreposição entrex e y, porque você não usou int *__restrict x). Por exemplo, o gcc faz isso: godbolt.org/z/c2JG0- . Ou depois de fazer a inclusão main, não é necessário verificar a sobreposição, pois pode ver a alocação e provar que não há sobreposição. (E também assumirá o alinhamento de 16 bytes em algumas implementações x86-64, o que não é o caso da definição autônoma.) E se você compilar gcc -O3 -march=native, poderá obter 256 ou 512 bits vetorização.
Peter Cordes

24

Isso significa que não devo confiar no desempenho da linguagem assembly escrita por minhas mãos

Sim, é exatamente isso que significa, e é verdade para todos idiomas. Se você não sabe escrever código eficiente no idioma X, não deve confiar na sua capacidade de escrever código eficiente no X. Portanto, se você deseja código eficiente, deve usar outro idioma.

A montagem é particularmente sensível a isso, porque, bem, o que você vê é o que obtém. Você escreve as instruções específicas que deseja que a CPU execute. Com linguagens de alto nível, existe um compilador no betweeen, que pode transformar seu código e remover muitas ineficiências. Com a montagem, você está por sua conta.


2
Eu acho que é para escrever que, especialmente para um processador x86 moderno, é excepcionalmente difícil escrever código de montagem eficiente devido à presença de pipelines, várias unidades de execução e outros truques dentro de cada núcleo. Escrever código que equilibra o uso de todos esses recursos para obter a maior velocidade de execução geralmente resultará em código com lógica direta que "não deveria" ser rápida de acordo com a sabedoria de montagem "convencional". Mas para CPUs menos complexas, é minha experiência que a geração de código do compilador C pode ser melhorada significativamente.
precisa saber é o seguinte

4
O código dos compiladores C geralmente pode ser aprimorado, mesmo em uma CPU x86 moderna. Mas você precisa entender bem a CPU, o que é mais difícil de fazer com uma CPU x86 moderna. Esse é meu argumento. Se você não entender o hardware que está segmentando, não poderá otimizá-lo. E, em seguida, o compilador irá provavelmente fazer um trabalho melhor
jalf

1
E se você realmente quer impressionar o compilador, precisa ser criativo e otimizar de maneira que o compilador não possa. É uma troca de tempo / recompensa, e é por isso que C é uma linguagem de script para alguns e código intermediário para uma linguagem de nível superior para outros. Para mim, porém, a montagem é mais divertida :). muito parecido com grc.com/smgassembly.htm
Hawken

22

Atualmente, o único motivo para usar a linguagem assembly é usar alguns recursos não acessíveis pelo idioma.

Isso se aplica á:

  • Programação de kernel que precisa acessar certos recursos de hardware, como o MMU
  • Programação de alto desempenho que utiliza instruções vetoriais ou multimídia muito específicas não suportadas pelo seu compilador.

Mas os compiladores atuais são bastante inteligentes; eles podem até substituir duas instruções separadas, como d = a / b; r = a % b;por uma única instrução que calcula a divisão e o restante de uma só vez, se estiver disponível, mesmo que C não possua esse operador.


10
Existem outros lugares para o ASM além desses dois. Nomeadamente, uma biblioteca de bignum geralmente será significativamente mais rápida no ASM que C, devido ao acesso a portadores de sinalizadores e a parte superior da multiplicação. Você também pode fazer essas coisas no C portátil, mas elas são muito lentas.
Mooing Duck

@MooingDuck Isso pode ser considerado como acessar recursos de hardware de hardware que não estão diretamente disponíveis no idioma ... Mas desde que você esteja apenas traduzindo seu código de alto nível para montagem manualmente, o compilador irá derrotá-lo.
fortran

1
é isso, mas não é programação de kernel, nem específico de fornecedor. Embora com pequenas alterações no trabalho, ele poderia facilmente se encaixar em qualquer categoria. Eu acho que adivinhe o ASM quando quiser o desempenho das instruções do processador que não possuem mapeamento em C.
Mooing Duck

1
@ fortran Você basicamente está dizendo que, se você não otimizar seu código, não será tão rápido quanto o código otimizado pelo compilador. A otimização é a razão pela qual alguém escreveria uma montagem em primeiro lugar. Se você quer dizer traduzir, otimizar, não há razão para que o compilador o derrote, a menos que você não seja bom em otimizar a montagem. Portanto, para vencer o compilador, você precisa otimizar de maneiras que o compilador não pode. É bastante auto-explicativo. O único motivo para escrever uma montagem é se você é melhor que um compilador / intérprete . Essa sempre foi a razão prática para escrever montagem.
quer

1
Apenas dizendo: Clang tem acesso aos sinalizadores de transporte, multiplicação de 128 bits e assim por diante através de funções internas. E pode integrar tudo isso em seus algoritmos normais de otimização.
precisa saber é o seguinte

19

É verdade que um compilador moderno faz um trabalho incrível na otimização de código, mas eu ainda o encorajo a continuar aprendendo assembly.

Antes de tudo, você claramente não se sente intimidado por isso ; isso é uma ótima, ótima vantagem, a seguir - você está no caminho certo ao criar um perfil para validar ou descartar suas suposições de velocidade , você está pedindo informações de pessoas experientes e você tem a maior ferramenta de otimização conhecida pela humanidade: um cérebro .

À medida que sua experiência aumenta, você aprenderá quando e onde usá-lo (geralmente os loops mais estreitos e internos do seu código, depois de otimizar profundamente em nível algorítmico).

Para inspiração, recomendo que você procure os artigos de Michael Abrash (se você não ouviu falar dele, ele é um guru da otimização; ele até colaborou com John Carmack na otimização do renderizador do software Quake!)

"não existe o código mais rápido" - Michael Abrash


2
Acredito que um dos livros de Michael Abrash é o livro negro de programação gráfica. Mas ele não é o único a usar montagem, Chris Sawyer escreveu os dois primeiros jogos de magnata de montanha-russa em montagem sozinho.
Hawken

14

Eu mudei o código asm:

 __asm
{ 
    mov ebx,TIMES
 start:
    mov ecx,lengthOfArray
    mov esi,x
    shr ecx,2
    mov edi,y
label:
    mov eax,DWORD PTR [esi]
    add eax,DWORD PTR [edi]
    add edi,4   
    dec ecx 
    mov DWORD PTR [esi],eax
    add esi,4
    test ecx,ecx
    jnz label
    dec ebx
    test ebx,ebx
    jnz start
};

Resultados para a versão Release:

 Function of assembly version: 41
 Function of C++ version: 161

O código de montagem no modo de liberação é quase 4 vezes mais rápido que o C ++. IMHo, a velocidade do código de montagem depende do programador


Sim, meu código realmente precisa ser otimizado. Bom trabalho para você e obrigado!
User957121

5
É quatro vezes mais rápido porque você faz apenas um quarto do trabalho :-) O shr ecx,2é supérfluo, porque o comprimento da matriz já é fornecido em inte não em byte. Então você basicamente alcança a mesma velocidade. Você pode tentar a padddresposta de harolds, isso será realmente mais rápido.
Gunther Piez

13

é um tópico muito interessante!
Mudei o MMX por SSE no código de Sasha
Aqui estão meus resultados:

Function of C++ version:      315
Function of assembly(simply): 312
Function of assembly  (MMX):  136
Function of assembly  (SSE):  62

O código de montagem com SSE é 5 vezes mais rápido que o C ++


12

A maioria dos compiladores de idiomas de alto nível é muito otimizada e sabe o que está fazendo. Você pode tentar despejar o código de desmontagem e compará-lo com seu assembly nativo. Eu acredito que você verá alguns truques legais que seu compilador está usando.

Apenas por exemplo, mesmo que eu não tenha mais certeza :):

Fazendo:

mov eax,0

custam mais ciclos do que

xor eax,eax

que faz a mesma coisa.

O compilador conhece todos esses truques e os usa.


4
Ainda é verdade, consulte stackoverflow.com/questions/1396527/… . Não por causa dos ciclos usados, mas por causa da redução na pegada de memória.
Gunther Piez

10

O compilador venceu você. Vou tentar, mas não darei garantias. Vou assumir que a "multiplicação" por TIMES destina-se a fazer-lhe um teste de desempenho mais relevante, que ye xsão 16-alinhados, e que lengthé um diferente de zero múltiplo de 4. Isso é provavelmente tudo verdade de qualquer maneira.

  mov ecx,length
  lea esi,[y+4*ecx]
  lea edi,[x+4*ecx]
  neg ecx
loop:
  movdqa xmm0,[esi+4*ecx]
  paddd xmm0,[edi+4*ecx]
  movdqa [edi+4*ecx],xmm0
  add ecx,4
  jnz loop

Como eu disse, não dou garantias. Mas ficarei surpreso se isso puder ser feito muito mais rapidamente - o gargalo aqui é a taxa de transferência de memória, mesmo que tudo seja um sucesso L1.


Eu acho que o endereçamento complexo está diminuindo a velocidade do seu código, se você alterar o código para mov ecx, length, lea ecx,[ecx*4], mov eax,16... add ecx,eaxe depois usar [esi + ecx] em todos os lugares, evitará 1 travamento de ciclo por instrução, acelerando os lotes do loop. (Se você possui o Skylake mais recente, isso não se aplica). O add reg, reg apenas torna o loop mais apertado, o que pode ou não ajudar.
22416 Johan Johan

@ John, que não deve ser um estol, apenas uma latência de ciclo extra, mas com certeza não machucará não tê-lo. Eu escrevi esse código para o Core2 que não tinha esse problema. O r + r também não é "complexo"?
Harold

7

Apenas implementar cegamente o mesmo algoritmo, instrução por instrução, em assembly é garantido que é mais lento do que o compilador pode fazer.

É porque mesmo a menor otimização que o compilador faz é melhor que o seu código rígido, sem nenhuma otimização.

Obviamente, é possível vencer o compilador, especialmente se for uma parte pequena e localizada do código, eu mesmo precisei fazer isso sozinho para obter um aprox. Acelere 4x, mas, neste caso, precisamos confiar muito no bom conhecimento do hardware e em vários truques aparentemente contra-intuitivos.


3
Eu acho que isso depende da linguagem e do compilador. Eu posso imaginar um compilador C extremamente ineficiente, cuja saída poderia ser facilmente superada por uma montagem direta de escrita humana. O GCC, nem tanto.
Casey Rodarmor 07/03/12

Com os compiladores C / ++ sendo um empreendimento desses e apenas os três principais, eles tendem a ser bastante bons no que fazem. Ainda é (muito) possível em certas circunstâncias que a montagem manuscrita será mais rápida; muitas bibliotecas de matemática caem para asm para lidar melhor com valores múltiplos / amplos. Portanto, embora a garantia seja um pouco forte demais, é provável.
ssube

@ peachykeen: Eu não quis dizer que a montagem é garantida para ser mais lenta que o C ++ em geral. Eu quis dizer essa "garantia" no caso em que você tem um código C ++ e o traduz cegamente linha por linha para montagem. Leia o último parágrafo da minha resposta também :)
vsz

5

Como compilador, eu substituiria um loop com um tamanho fixo para muitas tarefas de execução.

int a = 10;
for (int i = 0; i < 3; i += 1) {
    a = a + i;
}

vai produzir

int a = 10;
a = a + 0;
a = a + 1;
a = a + 2;

e eventualmente saberá que "a = a + 0;" é inútil, portanto ele removerá essa linha. Espero que algo em sua cabeça esteja agora disposto a anexar algumas opções de otimização como um comentário. Todas essas otimizações muito eficazes tornarão a linguagem compilada mais rápida.


4
E a menos que aseja volátil, há uma boa chance de que o compilador faça isso int a = 13;desde o início.
vsz 15/02/16

4

É exatamente o que isso significa. Deixe as micro otimizações para o compilador.


4

Adoro este exemplo porque demonstra uma lição importante sobre código de baixo nível. Sim, você pode escrever uma montagem tão rápida quanto o seu código C. Isso é tautologicamente verdadeiro, mas não significa necessariamente nada. Claramente alguém pode, caso contrário, o montador não conhecerá as otimizações apropriadas.

Da mesma forma, o mesmo princípio se aplica à medida que você sobe na hierarquia de abstração da linguagem. Sim, você pode escrever um analisador em C tão rápido quanto um script perl rápido e sujo, e muitas pessoas o fazem. Mas isso não significa que, como você usou C, seu código será rápido. Em muitos casos, os idiomas de nível superior fazem otimizações que você talvez nunca tenha considerado.


3

Em muitos casos, a maneira ideal de executar alguma tarefa pode depender do contexto em que a tarefa é executada. Se uma rotina é escrita em linguagem assembly, geralmente não será possível que a sequência de instruções varie com base no contexto. Como um exemplo simples, considere o seguinte método simples:

inline void set_port_high(void)
{
  (*((volatile unsigned char*)0x40001204) = 0xFF);
}

Um compilador para código ARM de 32 bits, dado o exposto acima, provavelmente o renderizará como algo como:

ldr  r0,=0x40001204
mov  r1,#0
strb r1,[r0]
[a fourth word somewhere holding the constant 0x40001204]

ou talvez

ldr  r0,=0x40001000  ; Some assemblers like to round pointer loads to multiples of 4096
mov  r1,#0
strb r1,[r0+0x204]
[a fourth word somewhere holding the constant 0x40001000]

Isso pode ser otimizado levemente no código montado à mão, como:

ldr  r0,=0x400011FF
strb r0,[r0+5]
[a third word somewhere holding the constant 0x400011FF]

ou

mvn  r0,#0xC0       ; Load with 0x3FFFFFFF
add  r0,r0,#0x1200  ; Add 0x1200, yielding 0x400011FF
strb r0,[r0+5]

Ambas as abordagens montadas manualmente exigiriam 12 bytes de espaço de código em vez de 16; o último substituiria uma "carga" por uma "adição", que executaria em um ARM7-TDMI dois ciclos mais rápidos. Se o código fosse executado em um contexto em que r0 não sabia / não se importa, as versões da linguagem assembly seriam assim um pouco melhores que a versão compilada. Por outro lado, suponha que o compilador sabia que algum registro [por exemplo, r5] manteria um valor dentro de 2047 bytes do endereço desejado 0x40001204 [por exemplo 0x40001000] e ainda sabia que outro registro [por exemplo, r7] para manter um valor cujos bits baixos fossem 0xFF. Nesse caso, um compilador pode otimizar a versão C do código para simplesmente:

strb r7,[r5+0x204]

Muito mais curto e rápido do que o código de montagem otimizado manualmente. Além disso, suponha que set_port_high ocorreu no contexto:

int temp = function1();
set_port_high();
function2(temp); // Assume temp is not used after this

Não é de todo implausível ao codificar para um sistema incorporado. Se set_port_highestiver escrito no código do assembly, o compilador precisaria mover r0 (que retém o valor de retorno function1) para outro lugar antes de chamar o código do assembly e, em seguida, mover esse valor de volta para r0 posteriormente (pois function2esperará seu primeiro parâmetro em r0), portanto, o código de montagem "otimizado" precisaria de cinco instruções. Mesmo que o compilador não soubesse de nenhum registro contendo o endereço ou o valor a ser armazenado, sua versão de quatro instruções (que poderia ser adaptada para usar os registros disponíveis - não necessariamente r0 e r1) venceria o assembly "otimizado" versão em vários idiomas. Se o compilador tiver o endereço e os dados necessários em r5 e r7, conforme descrito anteriormente, com uma única instrução -function1 não alteraria esses registros e, portanto, poderia substituirset_port_highstrb quatro instruções menores e mais rápidas que o código de montagem "otimizado manualmente".

Observe que o código de montagem otimizado manualmente pode superar um compilador nos casos em que o programador conhece o fluxo preciso do programa, mas os compiladores brilham nos casos em que um trecho de código é escrito antes de seu contexto ser conhecido ou onde um trecho de código-fonte pode ser invocado de vários contextos [se set_port_highfor usado em cinquenta lugares diferentes no código, o compilador poderá decidir independentemente para cada um qual a melhor forma de expandi-lo].

Em geral, eu sugeriria que a linguagem assembly é capaz de fornecer as maiores melhorias de desempenho nos casos em que cada parte do código pode ser abordada a partir de um número muito limitado de contextos e é prejudicial ao desempenho em locais onde uma parte da código pode ser abordado de muitos contextos diferentes. Curiosamente (e convenientemente) os casos em que a montagem é mais benéfica para o desempenho são geralmente aqueles em que o código é mais direto e fácil de ler. Os locais em que o código da linguagem assembly se tornaria uma bagunça pegajosa são geralmente aqueles em que a escrita em assembly ofereceria o menor benefício de desempenho.

[Nota secundária: existem alguns lugares onde o código de montagem pode ser usado para gerar uma bagunça pegajosa hiper otimizada; por exemplo, um pedaço de código que fiz para o ARM precisava buscar uma palavra da RAM e executar uma das cerca de doze rotinas com base nos seis bits superiores do valor (muitos valores mapeados para a mesma rotina). Eu acho que otimizei esse código para algo como:

ldrh  r0,[r1],#2! ; Fetch with post-increment
ldrb  r1,[r8,r0 asr #10]
sub   pc,r8,r1,asl #2

O registrador r8 sempre mantinha o endereço da tabela principal de despacho (dentro do loop em que o código gasta 98% de seu tempo, nada o utilizava para qualquer outro propósito); todas as 64 entradas se referiam a endereços nos 256 bytes anteriores a ela. Como o loop primário tinha, na maioria dos casos, um limite de tempo de execução rígido de cerca de 60 ciclos, a busca e o despacho de nove ciclos foram muito úteis para atingir esse objetivo. O uso de uma tabela de 256 endereços de 32 bits seria um ciclo mais rápido, mas consumiria 1 KB de RAM muito preciosa [o flash teria adicionado mais de um estado de espera]. Usar 64 endereços de 32 bits exigiria adicionar uma instrução para mascarar alguns bits da palavra buscada e ainda assim devoraria 192 bytes a mais do que a tabela que realmente usei. O uso da tabela de compensações de 8 bits produziu código muito compacto e rápido, mas não é algo que eu esperaria que um compilador pudesse inventar; Eu também não esperaria que um compilador dedicasse um registro "em tempo integral" para manter o endereço da tabela.

O código acima foi projetado para funcionar como um sistema independente; poderia chamar periodicamente o código C, mas somente em determinados momentos em que o hardware com o qual estava se comunicando podia ser colocado com segurança em um estado "inativo" por dois intervalos de aproximadamente um milissegundo a cada 16ms.


2

Nos últimos tempos, todas as otimizações de velocidade que eu fiz foram substituir o código lento com dano cerebral por apenas um código razoável. Mas, como as coisas eram rápidas, a velocidade era realmente crítica e eu esforçava seriamente para tornar algo rápido, o resultado sempre foi um processo iterativo, em que cada iteração dava mais informações sobre o problema, encontrando maneiras de resolvê-lo com menos operações. A velocidade final sempre dependia de quanto insight eu chegava ao problema. Se em qualquer estágio eu usasse código de montagem ou código C otimizado demais, o processo de encontrar uma solução melhor teria sofrido e o resultado final seria mais lento.


2

O C ++ é mais rápido, a menos que você esteja usando a linguagem assembly com conhecimento mais profundo da maneira correta.

Quando codifico no ASM, reorganizo as instruções manualmente para que a CPU possa executar mais delas em paralelo quando logicamente possível. Eu mal uso a RAM quando codifico no ASM, por exemplo: Pode haver mais de 20.000 linhas de código no ASM e nunca usei push / pop.

Você pode potencialmente pular no meio do código de operação para modificar o código e o comportamento sem a possível penalidade do código de modificação automática. O acesso a registros leva 1 tick (às vezes leva 0,25 ticks) da CPU. O acesso à RAM pode levar centenas.

Para minha última aventura no ASM, nunca usei a RAM para armazenar uma variável (para milhares de linhas de ASM). O ASM pode ser potencialmente inimaginavelmente mais rápido que o C ++. Mas isso depende de muitos fatores variáveis, como:

1. I was writing my apps to run on the bare metal.
2. I was writing my own boot loader that was starting my programs in ASM so there was no OS management in the middle.

Agora estou aprendendo C # e C ++ porque percebi que a produtividade importa! Você pode tentar executar os programas mais rápidos que se possa imaginar usando o ASM puro sozinho no tempo livre. Mas, para produzir algo, use alguma linguagem de alto nível.

Por exemplo, o último programa que eu codifiquei estava usando JS e GLSL e nunca notei nenhum problema de desempenho, mesmo falando sobre JS, que é lento. Isso ocorre porque o mero conceito de programação da GPU para 3D torna a velocidade da linguagem que envia os comandos para a GPU quase irrelevante.

A velocidade da montadora sozinha no metal puro é irrefutável. Poderia ser ainda mais lento dentro do C ++? - Pode ser porque você está escrevendo código de montagem com um compilador que não usa um montador para começar.

Meu conselho pessoal é nunca escrever código de montagem se você puder evitá-lo, mesmo que eu goste de montagem.


1

Todas as respostas aqui parecem excluir um aspecto: às vezes não escrevemos código para atingir um objetivo específico, mas apenas por diversão . Pode não ser econômico investir tempo para fazê-lo, mas é possível que não exista maior satisfação do que vencer o snippet de código otimizado do compilador mais rápido em velocidade com uma alternativa ASM rolada manualmente.


Quando você só quer vencer o compilador, geralmente é mais fácil usar a saída ASM para sua função e transformá-la em uma função ASM independente que você ajusta. Usar o asm inline é um trabalho extra para obter a interface entre C ++ e asm correta e verificar se está compilando para obter o código ideal. (Mas pelo menos ao fazer isso por diversão, você não precisa se preocupar em derrotar otimizações como propagação constante quando a função se alinha em outra coisa. Gcc.gnu.org/wiki/DontUseInlineAsm ).
22816 Peter Cordes

Consulte também as perguntas e respostas C + da conjectura Collatz contra asm, escritas à mão para obter mais informações sobre como vencer o compilador por diversão :) E também sugestões sobre como usar o que você aprendeu para modificar o C ++ para ajudar o compilador a criar um código melhor.
22816 Peter Cordes

@ PeterCordes Então, o que você está dizendo é que concorda.
Madoki

1
Sim, asm é divertido, só que em linha asm é geralmente a escolha errada mesmo para brincar. Tecnicamente, essa é uma pergunta inline-asm; portanto, seria bom pelo menos abordar esse ponto na sua resposta. Além disso, isso é realmente mais um comentário do que uma resposta.
22616 Peter Cordes

OK concordo. Eu costumava ser um cara só asm, mas isso foi nos anos 80.
Madoki

-2

Um compilador c ++, após a otimização no nível organizacional, produz código que utilizaria as funções internas da CPU de destino. A HLL nunca superará ou superará o montador por várias razões; 1.) A HLL será compilada e gerada com o código do acessador, verificação de limites e, possivelmente, coleta de lixo embutida (anteriormente abordando o escopo no maneirismo OOP), todos exigindo ciclos (inversões e falhas). Atualmente, o HLL faz um excelente trabalho (incluindo C ++ mais recente e outros como GO), mas se eles superam o assembler (ou seja, o seu código), você precisa consultar a documentação da CPU - as comparações com código superficial são certamente inconclusivas e os compilados como o assembler resolvem até o código operacional, a HLL abstrai os detalhes e não os elimina, caso contrário o aplicativo não será executado se for reconhecido pelo sistema operacional host.

A maioria dos códigos de assembler (principalmente objetos) são exibidos como "decapitados" para inclusão em outros formatos executáveis ​​com muito menos processamento necessário, portanto, será muito mais rápido, mas muito mais inseguro; se um executável for emitido pelo assembler (NAsm, YAsm; etc.), ele ainda será executado mais rapidamente até corresponder completamente ao código HLL na funcionalidade, e os resultados poderão ser pesados ​​com precisão.

Chamar um objeto de código baseado em assembler da HLL em qualquer formato adicionará inerentemente sobrecarga de processamento, além de chamadas de espaço de memória usando memória alocada globalmente para tipos de dados variáveis ​​/ constantes (isso se aplica a LLL e HLL). Lembre-se de que a saída final está usando a CPU como api e abi em relação ao hardware (opcode) e, tanto os montadores quanto os "compiladores HLL" são essencialmente / fundamentalmente idênticos, com a única exceção verdadeira sendo a legibilidade (gramatical).

A aplicação Hello World Console em assembler usando FAsm é de 1,5 KB (e isso é no Windows ainda menor no FreeBSD e Linux) e supera tudo o que o GCC pode jogar no seu melhor dia; razões são preenchimento implícito com nops, validação de acesso e verificação de limites para citar alguns. O objetivo real são as bibliotecas HLL limpas e um compilador otimizável que tem como alvo um processador de maneira "hardcore" e a maioria deles atualmente (finalmente). O GCC não é melhor que o YAsm - são as práticas de codificação e o entendimento do desenvolvedor que estão em questão e a "otimização" ocorre após a exploração iniciante, o treinamento e a experiência interinos.

Os compiladores precisam vincular e montar a saída no mesmo código de operação que um assembler, porque esses códigos são tudo o que uma CPU exclui (CISC ou RISC [PIC também]). O YAsm otimizou e limpou bastante o NAsm inicial, acelerando finalmente toda a saída desse montador, mas mesmo assim o YAsm ainda, como o NAsm, produz executáveis ​​com dependências externas visando as bibliotecas de SO em nome do desenvolvedor, para que a milhagem possa variar. No fechamento, o C ++ está em um ponto incrível e muito mais seguro do que o assembler para mais de 80%, especialmente no setor comercial ...


1
C e C ++ não têm nenhuma verificação de limites, a menos que você solicite, e nenhuma coleta de lixo, a menos que você a implemente ou use uma biblioteca. A verdadeira questão é se o compilador faz melhores loops (e otimizações globais) do que um humano. Geralmente sim, a menos que o humano realmente saiba o que está fazendo e gaste muito tempo com isso .
Peter Cordes

1
Você pode criar executáveis ​​estáticos usando NASM ou YASM (sem código externo). Os dois podem produzir em formato binário plano, para que você possa montar os cabeçalhos ELF se você realmente não deseja executar ld, mas isso não faz diferença, a menos que você esteja tentando otimizar o tamanho do arquivo (não apenas o tamanho do arquivo). segmento de texto). Veja um tutorial turbilhão sobre a criação de executáveis ​​ELF realmente adolescentes para Linux .
Peter Cordes

1
Talvez você esteja pensando em C # ou std::vectorcompilado no modo de depuração. Matrizes C ++ não são assim. Os compiladores podem verificar as coisas em tempo de compilação, mas, a menos que você habilite opções adicionais de proteção, não há verificação em tempo de execução. Veja, por exemplo, uma função que incrementa os primeiros 1024 elementos de um int array[]arg. A saída asm não possui verificações de tempo de execução: godbolt.org/g/w1HF5t . Tudo o que obtém é um ponteiro rdi, sem informações de tamanho. Cabe ao programador para evitar um comportamento indefinido por não chamá-lo com um leque menor do que 1024.
Peter Cordes

1
O que você está falando não é uma matriz C ++ simples (aloque com new, exclua manualmente com delete, sem verificação de limites). Você pode usar o C ++ para produzir código asm / máquina cheio de merda (como a maioria dos softwares), mas isso é culpa do programador, não do C ++. Você pode até usar allocapara alocar o espaço da pilha como uma matriz.
Peter Cordes

1
Vincule um exemplo em gcc.godbolt.org para g++ -O3gerar código de verificação de limites para uma matriz simples ou fazer o que mais estiver falando. O C ++ facilita muito a geração de binários inchados (e, de fato, você precisa ter cuidado para não ter o objetivo de obter desempenho), mas isso não é literalmente inevitável. Se você entender como o C ++ é compilado para asm, é possível obter um código apenas um pouco pior do que você poderia escrever à mão, mas com inline e propagação constante em uma escala maior do que você poderia gerenciar manualmente.
Peter Cordes

-3

A montagem pode ser mais rápida se o seu compilador gerar muito código de suporte OO .

Editar:

Para os que rejeitam: o OP escreveu "devo ... focar em C ++ e esquecer a linguagem assembly?" e eu mantenho minha resposta. Você sempre precisa ficar de olho no código que o OO gera, principalmente ao usar métodos. Não esquecer a linguagem assembly significa que você revisará periodicamente o assembly que seu código OO gera, que eu acredito ser uma obrigação para escrever um software com bom desempenho.

Na verdade, isso pertence a todo código compilável, não apenas ao OO.


2
-1: Não vejo nenhum recurso de OO sendo usado. Seu argumento é o mesmo que "o assembly também pode ser mais rápido se o seu compilador adicionar um milhão de NOPs".
Sjoerd

Eu não estava claro, esta é realmente uma pergunta em C. Se você escreve código C para um compilador C ++, não está escrevendo código C ++ e não receberá nada de OO. Depois que você começa a escrever em C ++ real, usando coisas de OO, você precisa ter muito conhecimento para que o compilador não gere o código de suporte de OO.
Olof Forshell

então sua resposta não é sobre a pergunta? (Além disso, esclarecimentos ir na resposta, não comenta comentários podem ser excluídos a qualquer momento sem aviso, notificação ou história..
Mooing Duck

1
Não sei o que exatamente você quer dizer com "código de suporte" do OO. Obviamente, se você usa muito RTTI e afins, o compilador precisará criar muitas instruções extras para dar suporte a esses recursos - mas qualquer problema que seja suficientemente alto para ratificar o uso do RTTI é muito complexo para ser possível de ser gravado na montagem. . O que você pode fazer, é claro, é escrever apenas a interface externa abstrata como OO, enviando para um código processual puro com desempenho otimizado, onde é crítico. Mas, dependendo do aplicativo, C, Fortran, CUDA ou simplesmente C ++ sem herança virtual pode ser melhor que a montagem aqui.
usar o seguinte comando

2
Não. Pelo menos, não muito provável. Existe uma coisa no C ++ chamada regra de sobrecarga zero, e isso se aplica na maioria das vezes. Aprenda mais sobre OO - você descobrirá que, no final, melhora a legibilidade do seu código, melhora a qualidade do código, aumenta a velocidade de codificação, aumenta a robustez. Também para incorporado - mas use C ++, pois ele oferece mais controle, + OO incorporado da maneira Java custará.
Zane
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.