Quando a montagem é mais rápida que C?

475

Uma das razões declaradas para conhecer o assembler é que, ocasionalmente, ele pode ser empregado para escrever código com melhor desempenho do que escrever esse código em uma linguagem de nível superior, C em particular. No entanto, também ouvi dizer muitas vezes que, embora isso não seja totalmente falso, os casos em que o assembler pode realmente ser usado para gerar código com melhor desempenho são extremamente raros e exigem conhecimento e experiência com assembly.

Essa pergunta nem entra no fato de que as instruções do assembler serão específicas da máquina e não serão portáveis, ou qualquer outro aspecto do assembler. Existem muitas boas razões para conhecer o assembly além deste, é claro, mas essa é uma pergunta específica que solicita exemplos e dados, não um discurso extenso sobre assembler versus linguagens de nível superior.

Alguém pode fornecer alguns exemplos específicos de casos em que o assembly será mais rápido que o código C bem escrito, usando um compilador moderno, e você pode apoiar essa afirmação com evidências de criação de perfil? Estou bastante confiante de que esses casos existem, mas quero realmente saber exatamente como esses casos são esotéricos, pois parece ser um ponto de alguma disputa.

Adam Bellaire
fonte
17
na verdade, é bastante trivial melhorar o código compilado. Qualquer pessoa com um sólido conhecimento da linguagem assembly e C pode ver isso examinando o código gerado. Qualquer um fácil é o primeiro penhasco de desempenho do qual você cai quando fica sem registros descartáveis ​​na versão compilada. Em média, o compilador se sai muito melhor do que um humano para um projeto grande, mas não é difícil em um projeto de tamanho decente encontrar problemas de desempenho no código compilado.
old_timer
14
Na verdade, a resposta curta é: Assembler é sempre mais rápido ou igual à velocidade de C. O motivo é que você pode ter montagem sem C, mas você não pode ter C sem montagem (na forma binária, que na antiga dias chamados "código de máquina"). Dito isso, a resposta longa é: os compiladores C são muito bons em otimizar e "pensar" em coisas que você normalmente não pensa, por isso realmente depende de suas habilidades, mas normalmente você sempre pode vencer o compilador C; ainda é apenas um software que não consegue pensar e ter idéias. Você também pode escrever um assembler portátil se usar macros e for paciente.
11
Eu discordo totalmente que as respostas a essa pergunta precisam ser "baseadas em opiniões" - elas podem ser bastante objetivas - não é algo como tentar comparar o desempenho das linguagens de estimação favoritas, para as quais cada uma terá pontos fortes e desvantagens. É uma questão de entender até onde os compiladores podem nos levar e a partir de que ponto é melhor assumir o controle.
Jsbueno # 15/15
21
No início de minha carreira, eu escrevia muitos montadores de C e mainframe em uma empresa de software. Um dos meus colegas era o que eu chamaria de "purista de montador" (tudo tinha que ser montador), então aposto que poderia escrever uma determinada rotina mais rápida em C do que o que ele poderia escrever em montador. Eu venci. Mas, ainda por cima, depois que ganhei, disse a ele que queria uma segunda aposta - que eu poderia escrever algo mais rápido em assembler do que o programa C que o venceu na aposta anterior. Também ganhei isso, provando que a maior parte se resume à habilidade e habilidade do programador mais do que qualquer outra coisa.
Valerie R
3
A menos que seu cérebro tenha uma -O3bandeira, é provável que você esteja melhor deixando a otimização para o compilador C :-) #
385

Respostas:

272

Aqui está um exemplo do mundo real: o ponto fixo se multiplica nos compiladores antigos.

Eles não são úteis apenas em dispositivos sem ponto flutuante, eles brilham quando se trata de precisão, pois oferecem 32 bits de precisão com um erro previsível (o float tem apenas 23 bits e é mais difícil prever a perda de precisão). isto é, precisão absoluta uniforme em toda a faixa, em vez de precisão relativa quase uniforme ( float).


Os compiladores modernos otimizam esse exemplo de ponto fixo, portanto, para exemplos mais modernos que ainda precisam de código específico do compilador, consulte


C não possui um operador de multiplicação completa (resultado de 2N bits de entradas de N bits). A maneira usual de expressá-lo em C é converter as entradas para o tipo mais amplo e esperar que o compilador reconheça que os bits superiores das entradas não são interessantes:

// on a 32-bit machine, int can hold 32-bit fixed-point integers.
int inline FixedPointMul (int a, int b)
{
  long long a_long = a; // cast to 64 bit.

  long long product = a_long * b; // perform multiplication

  return (int) (product >> 16);  // shift by the fixed point bias
}

O problema com esse código é que fazemos algo que não pode ser expresso diretamente na linguagem C. Queremos multiplicar dois números de 32 bits e obter um resultado de 64 bits, dos quais retornamos os 32 bits do meio. No entanto, em C essa multiplicação não existe. Tudo o que você pode fazer é promover os números inteiros para 64 bits e fazer uma multiplicação de 64 * 64 = 64.

x86 (e ARM, MIPS e outros) podem, no entanto, fazer a multiplicação em uma única instrução. Alguns compiladores costumavam ignorar esse fato e gerar código que chama uma função de biblioteca de tempo de execução para fazer a multiplicação. A mudança de 16 também é frequentemente feita por uma rotina de biblioteca (também o x86 pode fazer essas mudanças).

Portanto, temos uma ou duas chamadas de biblioteca apenas para uma multiplicação. Isso tem sérias conseqüências. O turno não é apenas mais lento, os registros devem ser preservados nas chamadas de função e também não ajuda na inserção e desenrolamento de código.

Se você reescrever o mesmo código no assembler (em linha), poderá obter um aumento de velocidade significativo.

Além disso: o uso do ASM não é a melhor maneira de resolver o problema. A maioria dos compiladores permite que você use algumas instruções do assembler de forma intrínseca se não puder expressá-las em C. O compilador do VS.NET2008, por exemplo, expõe o mul 32 * 32 = 64 bits como __emul e o deslocamento de 64 bits como __ll_rshift.

Usando intrínsecos, você pode reescrever a função de uma maneira que o compilador C tenha a chance de entender o que está acontecendo. Isso permite que o código seja embutido, alocado para registro, eliminação comum de subexpressão e propagação constante também. Você obterá uma enorme melhoria de desempenho com o código do montador escrito à mão dessa maneira.

Para referência: o resultado final da multa de ponto fixo para o compilador VS.NET é:

int inline FixedPointMul (int a, int b)
{
    return (int) __ll_rshift(__emul(a,b),16);
}

A diferença de desempenho das divisões de pontos fixos é ainda maior. Eu tive melhorias até o fator 10 para o código de ponto fixo pesado de divisão escrevendo algumas linhas ASM.


O uso do Visual C ++ 2013 fornece o mesmo código de montagem para os dois lados.

O gcc4.1 de 2007 também otimiza bem a versão C pura. (O Godbolt compiler explorer não possui nenhuma versão anterior do gcc instalada, mas, presumivelmente, versões mais antigas do GCC poderiam fazer isso sem intrínseca.)

Consulte source + asm para x86 (32 bits) e ARM no explorador do compilador Godbolt . (Infelizmente, ele não possui compiladores com idade suficiente para produzir código incorreto a partir da versão simples e simples de C).


CPUs modernas podem fazer coisas C não têm operadores para em tudo , como popcntou bit-scan para encontrar o primeiro ou último conjunto de bits . (O POSIX tem uma ffs()função, mas sua semântica não corresponde a x86 bsf/ bsr. Consulte https://en.wikipedia.org/wiki/Find_first_set ).

Às vezes, alguns compiladores podem reconhecer um loop que conta o número de bits definidos em um número inteiro e compilá-lo em uma popcntinstrução (se ativada no momento da compilação), mas é muito mais confiável usar __builtin_popcntno GNU C ou no x86 se você estiver apenas segmentando hardware com SSE4.2: _mm_popcnt_u32from<immintrin.h> .

Ou em C ++, atribua a std::bitset<32>e use .count(). (Este é o caso em que o idioma encontrou uma maneira de expor portatilmente uma implementação otimizada de popcount por meio da biblioteca padrão, de uma maneira que sempre será compilada com algo correto e que possa tirar proveito do que o destino suportar.) Veja também https : //en.wikipedia.org/wiki/Hamming_weight#Language_support .

Da mesma forma, ntohlpode compilar até bswap(x86 swap de bytes de 32 bits para conversão endian) em algumas implementações em C que o possuem.


Outra área importante para intrínsecas ou asm manuscritas é a vetorização manual com instruções SIMD. Compiladores não são ruins com loops simples como dst[i] += src[i] * 10.0;, mas geralmente se saem mal ou não se auto-vectorizam quando as coisas ficam mais complicadas. Por exemplo, é improvável que você obtenha algo como Como implementar o atoi usando o SIMD? gerado automaticamente pelo compilador a partir do código escalar.

Nils Pipenbrinck
fonte
6
Que tal coisas como {x = c% d; y = c / d;}, os compiladores são inteligentes o suficiente para transformar isso em uma única div ou idiv?
Jens Björnhager
4
Na verdade, um bom compilador produziria o código ideal a partir da primeira função. Obscurecer o código fonte com montagem intrínseca ou em linha com absolutamente nenhum benefício não é a melhor coisa a fazer.
slacker
65
Oi Slacker, acho que você nunca teve que trabalhar com código crítico de tempo antes ... o assembly embutido pode fazer uma * enorme diferença. Também para o compilador, um intrínseco é o mesmo que aritmética normal em C. Esse é o ponto em intrínsecos. Eles permitem que você use um recurso de arquitetura sem precisar lidar com as desvantagens.
Nils Pipenbrinck
6
@ slacker Na verdade, o código aqui é bastante legível: o código embutido faz uma operação única, que é imediatamente subestimada lendo a assinatura do método. O código perdeu apenas lentamente a legibilidade quando uma instrução obscura é usada. O que importa aqui é que temos um método que realiza apenas uma operação claramente identificável, e essa é realmente a melhor maneira de produzir código legível para essas funções atômicas. A propósito, isso não é tão obscuro um pequeno comentário como / * (a * b) >> 16 * / não pode explicá-lo imediatamente.
Dereckson
5
Para ser justo, este é um exemplo ruim, pelo menos hoje. Há muito tempo os compiladores C conseguem multiplicar 32 x 32 -> 64 mesmo que a linguagem não o ofereça diretamente: eles reconhecem que quando você lança argumentos de 32 bits para 64 bits e os multiplica, não é necessário faça uma multiplicação completa de 64 bits, mas que um 32x32 -> 64 funcionará perfeitamente. Eu verifiquei e todos os clang, gcc e MSVC em sua versão atual acertaram . Isso não é novidade - eu lembro de observar a saída do compilador e perceber isso uma década atrás.
BeeOnRope
143

Muitos anos atrás, eu estava ensinando alguém a programar em C. O exercício era girar um gráfico 90 graus. Ele voltou com uma solução que levou vários minutos para ser concluída, principalmente porque estava usando multiplica e divide etc.

Eu mostrei a ele como reformular o problema usando mudanças de bits, e o tempo para processar caiu para cerca de 30 segundos no compilador não otimizador que ele possuía.

Acabei de obter um compilador de otimização e o mesmo código girou o gráfico em <5 segundos. Eu olhei para o código do assembly que o compilador estava gerando e, pelo que vi, decidi lá e então que meus dias de escrever assembler haviam terminado.

Peter Cordes
fonte
3
Sim, era um sistema monocromático de um bit, especificamente os blocos de imagem monocromática em um Atari ST.
22411 lilburne
16
O compilador otimizador compilou o programa original ou sua versão?
Thorbjørn Ravn Andersen
Em que processador? Em 8086, eu esperaria que o código ideal para uma rotação 8x8 carregasse DI com 16 bits de dados usando SI, repita add di,di / adc al,al / add di,di / adc ah,ahetc. para todos os oito registros de 8 bits, depois faça todos os 8 registros novamente e repita todo o procedimento três. mais vezes e, finalmente, salve quatro palavras em ax / bx / cx / dx. De maneira alguma um montador chegará perto disso.
Supercat
1
Realmente não consigo pensar em nenhuma plataforma em que um compilador possa estar dentro de um fator ou dois do código ideal para uma rotação 8x8.
precisa saber é
65

Sempre que o compilador vê código de ponto flutuante, uma versão escrita à mão será mais rápida se você estiver usando um compilador antigo e ruim. ( Atualização de 2019: isso geralmente não é verdade para os compiladores modernos. Especialmente quando compilar para algo diferente de x87; os compiladores têm mais facilidade com o SSE2 ou AVX para matemática escalar ou qualquer outro não-x86 com um conjunto de registradores FP simples, ao contrário do x87 pilha de registradores.)

O principal motivo é que o compilador não pode executar otimizações robustas. Consulte este artigo do MSDN para uma discussão sobre o assunto. Aqui está um exemplo em que a versão do assembly tem o dobro da velocidade da versão C (compilada com o VS2K5):

#include "stdafx.h"
#include <windows.h>

float KahanSum(const float *data, int n)
{
   float sum = 0.0f, C = 0.0f, Y, T;

   for (int i = 0 ; i < n ; ++i) {
      Y = *data++ - C;
      T = sum + Y;
      C = T - sum - Y;
      sum = T;
   }

   return sum;
}

float AsmSum(const float *data, int n)
{
  float result = 0.0f;

  _asm
  {
    mov esi,data
    mov ecx,n
    fldz
    fldz
l1:
    fsubr [esi]
    add esi,4
    fld st(0)
    fadd st(0),st(2)
    fld st(0)
    fsub st(0),st(3)
    fsub st(0),st(2)
    fstp st(2)
    fstp st(2)
    loop l1
    fstp result
    fstp result
  }

  return result;
}

int main (int, char **)
{
  int count = 1000000;

  float *source = new float [count];

  for (int i = 0 ; i < count ; ++i) {
    source [i] = static_cast <float> (rand ()) / static_cast <float> (RAND_MAX);
  }

  LARGE_INTEGER start, mid, end;

  float sum1 = 0.0f, sum2 = 0.0f;

  QueryPerformanceCounter (&start);

  sum1 = KahanSum (source, count);

  QueryPerformanceCounter (&mid);

  sum2 = AsmSum (source, count);

  QueryPerformanceCounter (&end);

  cout << "  C code: " << sum1 << " in " << (mid.QuadPart - start.QuadPart) << endl;
  cout << "asm code: " << sum2 << " in " << (end.QuadPart - mid.QuadPart) << endl;

  return 0;
}

E alguns números do meu PC executando uma versão padrão build * :

  C code: 500137 in 103884668
asm code: 500137 in 52129147

Por interesse, troquei o loop com um dec / jnz e não fez diferença nos tempos - às vezes mais rápidos, às vezes mais lentos. Eu acho que o aspecto de memória limitada supera outras otimizações. (Nota do editor: é mais provável que o gargalo de latência do FP seja suficiente para ocultar o custo extra loop. Fazer duas somas Kahan em paralelo para os elementos pares / ímpares e adicioná-las no final, talvez possa acelerar isso por um fator de 2. )

Opa, eu estava executando uma versão ligeiramente diferente do código e ele exibiu os números da maneira errada (ou seja, C foi mais rápido!). Corrigido e atualizado os resultados.

Skizz
fonte
20
Ou no GCC, você pode desatar as mãos do compilador na otimização de ponto flutuante (desde que prometa não fazer nada com infinitos ou NaNs) usando a flag -ffast-math. Eles têm um nível de otimização -Ofastque atualmente é equivalente a -O3 -ffast-math, mas no futuro podem incluir mais otimizações que podem levar à geração incorreta de código em casos extremos (como código que depende de NaNs IEEE).
David Stone
2
Sim, os carros alegóricos não são comutativos, o compilador deve fazer exatamente o que você escreveu, basicamente o que o @DavidStone disse.
Alec Teal
2
Você tentou matemática SSE? O desempenho foi uma das razões pelas quais MS abandonado x87 completamente em x86_64 e 80 bits de comprimento duplo em x86
phuclv
4
@Praxeolitic: FP add é comutativo ( a+b == b+a), mas não associativo (reordenação de operações, portanto o arredondamento de intermediários é diferente). re: este código: Eu não acho que x87 descomentado e uma loopinstrução são uma demonstração muito impressionante de asm rápido. loopaparentemente não é realmente um gargalo por causa da latência do FP. Não tenho certeza se ele está planejando operações de FP ou não; x87 é difícil para os humanos lerem. Dois fstp resultsinsns no final claramente não são ótimos. Retirar o resultado extra da pilha seria melhor com uma não loja. Como o fstp st(0)IIRC.
Peter Cordes
2
@ PeterCordes: Uma conseqüência interessante de tornar a adição comutativa é que, enquanto 0 + x e x + 0 são equivalentes entre si, nem sempre é equivalente a x.
Supercat
58

Sem fornecer nenhum exemplo específico ou evidência de criação de perfil, você pode escrever um assembler melhor que o compilador quando souber mais do que o compilador.

No caso geral, um compilador C moderno sabe muito mais sobre como otimizar o código em questão: sabe como o pipeline do processador funciona, pode tentar reordenar instruções mais rapidamente do que um humano, e assim por diante - é basicamente o mesmo que um computador seja tão bom ou melhor que o melhor jogador humano para jogos de tabuleiro, etc. simplesmente porque ele pode fazer pesquisas no espaço do problema mais rapidamente do que a maioria dos humanos. Embora você teoricamente possa ter um desempenho tão bom quanto o computador em um caso específico, certamente não pode fazê-lo na mesma velocidade, tornando-o inviável por mais de alguns casos (ou seja, o compilador certamente superará você se você tentar escrever mais do que algumas rotinas no assembler).

Por outro lado, há casos em que o compilador não possui tanta informação - eu diria principalmente ao trabalhar com diferentes formas de hardware externo, dos quais o compilador não tem conhecimento. O exemplo principal provavelmente é o de drivers de dispositivo, em que o assembler, combinado com o conhecimento íntimo de um ser humano sobre o hardware em questão, pode produzir melhores resultados do que um compilador C.

Outros mencionaram instruções de propósito especial, que é o que estou falando no parágrafo acima - instruções sobre as quais o compilador pode ter conhecimento limitado ou nenhum conhecimento, possibilitando que um humano escreva códigos mais rapidamente.

Liedman
fonte
Geralmente, esta afirmação é verdadeira. O compilador faz o melhor para o DWIW, mas em alguns casos extremos, o montador de codificação manual realiza o trabalho quando o desempenho em tempo real é obrigatório.
22139 spoulson
1
@Liedman: "ele pode tentar reordenar as instruções mais rapidamente do que o humano". O OCaml é conhecido por ser rápido e, surpreendentemente, seu compilador de código nativo ocamloptignora o agendamento de instruções no x86 e, em vez disso, deixa para a CPU porque pode reordenar de forma mais eficaz em tempo de execução.
precisa saber é o seguinte
1
Compiladores modernos fazem muito, e isso levaria muito tempo para ser feito à mão, mas eles não são nem de longe perfeitos. Pesquise nos rastreadores de erros do gcc ou llvm os erros de "otimização perdida". Há muitos. Além disso, ao escrever em asm, você pode aproveitar mais facilmente condições prévias como "essa entrada não pode ser negativa", que seria difícil para um compilador provar.
Peter Cordes
48

No meu trabalho, há três razões para eu conhecer e usar a montagem. Por ordem de importância:

  1. Depuração - geralmente recebo código de biblioteca com erros ou documentação incompleta. Eu descubro o que está fazendo entrando no nível da montagem. Eu tenho que fazer isso cerca de uma vez por semana. Também o uso como uma ferramenta para depurar problemas nos quais meus olhos não detectam o erro idiomático em C / C ++ / C #. Olhar para a assembléia passa disso.

  2. Otimizando - o compilador se sai muito bem na otimização, mas eu jogo em um estádio diferente do que a maioria. Eu escrevo um código de processamento de imagem que geralmente começa com um código que se parece com isso:

    for (int y=0; y < imageHeight; y++) {
        for (int x=0; x < imageWidth; x++) {
           // do something
        }
    }

    a "parte de fazer alguma coisa" normalmente acontece na ordem de vários milhões de vezes (ou seja, entre 3 e 30). Ao eliminar ciclos na fase "fazer alguma coisa", os ganhos de desempenho são enormemente ampliados. Normalmente, não começo por aí - geralmente começo escrevendo o código para trabalhar primeiro e depois faço o possível para refatorar o C para ser naturalmente melhor (algoritmo melhor, menos carga no loop, etc.). Normalmente, preciso ler a montagem para ver o que está acontecendo e raramente preciso escrever. Faço isso talvez a cada dois ou três meses.

  3. fazendo algo que a linguagem não vai me deixar. Isso inclui: obter a arquitetura do processador e os recursos específicos do processador, acessar sinalizadores que não estão na CPU (cara, eu realmente gostaria que C lhe desse acesso ao sinalizador de transporte), etc. Eu faço isso talvez uma vez por ano ou dois anos.

rodapé
fonte
Você não telha seus loops? :-)
Jon Harrop
1
@ plinth: como você quer dizer "ciclos de raspagem"?
Lang2
@ lang2: significa livrar-se do máximo de tempo supérfluo gasto no loop interno - tudo o que o compilador não conseguiu extrair, o que pode incluir o uso de álgebra para levantar uma multiplicação de um loop e torná-lo um complemento no interior, etc
plinto
1
A disposição em loop parece desnecessária se você estiver fazendo apenas uma passagem pelos dados.
James M. Lay
@ JamesM.Lay: Se você tocar em todos os elementos apenas uma vez, uma ordem de travessia melhor poderá fornecer a localização espacial. (por exemplo, usar todos os bytes de uma linha de cache que você tocou, em vez de looping para baixo colunas de uma matriz usando um elemento por linha de cache.)
Peter Cordes
42

Somente ao usar algumas instruções de finalidade especial, o compilador não suporta.

Para maximizar o poder de computação de uma CPU moderna com vários pipelines e ramificação preditiva, você precisa estruturar o programa de montagem de uma maneira que torne a) quase impossível para um ser humano escrever b) ainda mais impossível de manter.

Além disso, melhores algoritmos, estruturas de dados e gerenciamento de memória fornecerão pelo menos uma ordem de magnitude mais desempenho do que as micro-otimizações que você pode fazer na montagem.

Nir
fonte
4
+1, mesmo que a última sentença não pertença realmente a esta discussão - seria de supor que o assembler só entra em cena depois que todas as possíveis melhorias no algoritmo etc. forem realizadas.
23410 mghie
18
@Matt: O ASM escrito à mão geralmente é muito melhor em alguns dos pequenos trabalhos de CPUs EE com suporte de compilador de fornecedor de baixa qualidade.
Zan Lynx
5
"Somente ao usar alguns conjuntos de instruções para fins especiais" ?? Você provavelmente nunca escreveu um código ASM otimizado à mão antes. Um conhecimento moderadamente íntimo da arquitetura em que você está trabalhando oferece uma boa chance de gerar um código (tamanho e velocidade) melhor que o seu compilador. Obviamente, como o @mghie comentou, você sempre começa a codificar os melhores algos que pode encontrar para o seu problema. Mesmo para compiladores muito bons, você realmente precisa escrever seu código C de uma maneira que leve o compilador ao melhor código compilado. Caso contrário, o código gerado ficará abaixo do ideal.
ysap
2
@ysap - em computadores reais (e não pequenos chips incorporados com pouca potência) no uso no mundo real, o código "ideal" não será mais rápido porque, para qualquer conjunto de dados grande, o desempenho será limitado pelo acesso à memória e falhas de página ( e se você não tiver um conjunto grande de dados, isso será rápido de qualquer maneira e não adianta otimizá-lo) - nesses dias eu trabalho principalmente em C # (nem mesmo em c) e o desempenho melhora com o gerenciador de memória compacto. ponderar a sobrecarga da coleta de lixo, compactação e compilação JIT.
Nr
4
+1 por afirmar que os compiladores (especialmente o JIT) podem fazer um trabalho melhor que os humanos, se forem otimizados para o hardware em que são executados.
Sebastian
38

Embora C esteja "próximo" da manipulação de baixo nível de dados de 8 bits, 16 bits, 32 bits e 64 bits, existem algumas operações matemáticas não suportadas por C que geralmente podem ser executadas com elegância em determinadas instruções de montagem conjuntos:

  1. Multiplicação de ponto fixo: o produto de dois números de 16 bits é um número de 32 bits. Mas as regras em C dizem que o produto de dois números de 16 bits é um número de 16 bits e o produto de dois números de 32 bits é um número de 32 bits - a metade inferior nos dois casos. Se você deseja a metade superior de uma multiplicação de 16x16 ou de 32x32, é necessário jogar com o compilador. O método geral é converter em uma largura de bit maior que o necessário, multiplicar, reduzir e converter:

    int16_t x, y;
    // int16_t is a typedef for "short"
    // set x and y to something
    int16_t prod = (int16_t)(((int32_t)x*y)>>16);`

    Nesse caso, o compilador pode ser inteligente o suficiente para saber que você realmente está apenas tentando obter a metade superior de uma multiplicação de 16x16 e fazer a coisa certa com o 16x16multiply nativo da máquina. Ou pode ser estúpido e exigir uma chamada de biblioteca para fazer a multiplicação de 32 x 32, que é um exagero, porque você só precisa de 16 bits do produto - mas o padrão C não oferece nenhuma maneira de se expressar.

  2. Certas operações de deslocamento de bits (rotação / transporte):

    // 256-bit array shifted right in its entirety:
    uint8_t x[32];
    for (int i = 32; --i > 0; )
    {
       x[i] = (x[i] >> 1) | (x[i-1] << 7);
    }
    x[0] >>= 1;

    Isso não é muito deselegante em C, mas, novamente, a menos que o compilador seja inteligente o suficiente para perceber o que você está fazendo, ele fará muito trabalho "desnecessário". Muitos conjuntos de instruções de montagem permitem que você gire ou desloque para a esquerda / direita com o resultado no registro de transporte, para que você possa realizar o acima em 34 instruções: carregar um ponteiro para o início da matriz, limpar o transporte e executar 32 8- bocado para a direita, usando incremento automático no ponteiro.

    Por outro exemplo, existem registradores de deslocamento de realimentação linear (LFSR) que são executados com elegância na montagem: Pegue um pedaço de N bits (8, 16, 32, 64, 128, etc), mova a coisa toda para a direita por 1 (veja acima algoritmo), se o transporte resultante for 1, você fará XOR em um padrão de bits que representa o polinômio.

Dito isto, não recorreria a essas técnicas a menos que tivesse sérias restrições de desempenho. Como outros já disseram, a montagem é muito mais difícil de documentar / depurar / testar / manter do que o código C: o ganho de desempenho traz alguns custos sérios.

edit: 3. A detecção de estouro é possível na montagem (realmente não é possível fazê-lo em C), isso facilita alguns algoritmos.

Jason S
fonte
23

Resposta curta? As vezes.

Tecnicamente, toda abstração tem um custo e uma linguagem de programação é uma abstração de como a CPU funciona. C, no entanto, é muito próximo. Anos atrás, lembro-me de rir alto quando entrei na minha conta UNIX e recebi a seguinte mensagem da sorte (quando essas coisas eram populares):

A Linguagem de Programação C - Uma linguagem que combina a flexibilidade da linguagem assembly com o poder da linguagem assembly.

É engraçado porque é verdade: C é como linguagem assembly portátil.

Vale a pena notar que a linguagem assembly é executada da maneira que você a escreve. No entanto, existe um compilador entre C e a linguagem assembly que ele gera e isso é extremamente importante porque a velocidade do seu código C tem muito a ver com a qualidade do seu compilador.

Quando o gcc entrou em cena, uma das coisas que o tornou tão popular foi que muitas vezes era muito melhor do que os compiladores C que vinham com muitos sabores comerciais do UNIX. Não apenas o ANSI C (nada desse lixo K&R C), era mais robusto e normalmente produzia um código melhor (mais rápido). Nem sempre, mas frequentemente.

Digo tudo isso porque não há uma regra geral sobre a velocidade de C e assembler porque não há um padrão objetivo para C.

Da mesma forma, o assembler varia muito, dependendo do processador que você está executando, das especificações do sistema, do conjunto de instruções que você está usando e assim por diante. Historicamente, existem duas famílias de arquitetura de CPU: CISC e RISC. O maior player do CISC foi e ainda é a arquitetura Intel x86 (e conjunto de instruções). O RISC dominou o mundo UNIX (MIPS6000, Alpha, Sparc e assim por diante). A CISC venceu a batalha pelos corações e mentes.

De qualquer forma, a sabedoria popular quando eu era um desenvolvedor mais jovem era que o x86 escrito à mão costumava ser muito mais rápido que o C, porque o modo como a arquitetura funcionava tinha uma complexidade que se beneficiava de um ser humano. O RISC, por outro lado, parecia projetado para compiladores, então ninguém (eu sabia) escreveu o Sparc assembler. Tenho certeza de que essas pessoas existiram, mas sem dúvida elas ficaram loucas e foram institucionalizadas até agora.

Os conjuntos de instruções são um ponto importante, mesmo na mesma família de processadores. Certos processadores Intel têm extensões como SSE a SSE4. A AMD tinha suas próprias instruções SIMD. O benefício de uma linguagem de programação como C era que alguém poderia escrever sua biblioteca, por isso foi otimizada para qualquer processador em que você estivesse executando. Esse foi um trabalho árduo na montadora.

Ainda existem otimizações que você pode fazer no assembler que nenhum compilador poderia fazer, e um algo que é bem escrito para o assembler será tão rápido ou mais rápido do que o equivalente em C. A questão maior é: vale a pena?

No final das contas, o assembler era um produto de seu tempo e era mais popular no momento em que os ciclos da CPU eram caros. Atualmente, uma CPU que custa de US $ 5 a 10 para fabricar (Intel Atom) pode fazer praticamente qualquer coisa que alguém possa desejar. A única razão real para escrever assembler hoje em dia é para coisas de baixo nível, como algumas partes de um sistema operacional (mesmo que a grande maioria do kernel do Linux seja escrita em C), drivers de dispositivo, possivelmente dispositivos incorporados (embora C tenda a dominar lá também) e assim por diante. Ou apenas para chutes (o que é um pouco masoquista).

cletus
fonte
Havia muitas pessoas que usavam o ARM assembler como idioma de escolha nas máquinas Acorn (início dos anos 90). O IIRC disseram que o pequeno conjunto de instruções risc tornava mais fácil e divertido. Mas suspeito que seja porque o compilador C chegou atrasado à Acorn e o compilador C ++ nunca foi concluído.
23730 Andrew M
3
"... porque não há padrão subjetivo para C." Você quer dizer objetivo .
Thomas
@ AndrewM: Sim, eu escrevi aplicativos de linguagem mista no BASIC e ARM assembler por cerca de 10 anos. Eu aprendi C durante esse período, mas não foi muito útil porque é tão complicado quanto o montador e mais lento. Norcroft fez algumas otimizações incríveis, mas acho que o conjunto de instruções condicionais foi um problema para os compiladores da época.
precisa saber é o seguinte
1
@ AndrewM: bem, na verdade o ARM é uma espécie de RISC feito ao contrário. Outros RISC ISAs foram projetados começando com o que um compilador usaria. O ARM ISA parece ter sido projetado a partir do que a CPU fornece (deslocador de barril, sinalizadores de condição → vamos expô-los em todas as instruções).
Ninjalj
16

Um caso de uso que pode não ser mais aplicável, mas para o seu prazer nerd: No Amiga, a CPU e os chips gráficos / de áudio lutam para acessar uma determinada área da RAM (os primeiros 2 MB de RAM para ser específico). Portanto, quando você tinha apenas 2 MB de RAM (ou menos), exibir gráficos complexos e reproduzir som prejudicaria o desempenho da CPU.

No assembler, você poderia intercalar seu código de maneira tão inteligente que a CPU só tentaria acessar a RAM quando os chips gráficos / áudio estivessem ocupados internamente (ou seja, quando o barramento estivesse livre). Assim, reordenando suas instruções, uso inteligente do cache da CPU, o tempo do barramento, você pode obter alguns efeitos que simplesmente não eram possíveis usando qualquer linguagem de nível superior, porque você tinha que cronometrar cada comando, até inserir NOPs aqui e ali para manter os vários chips fora do radar um do outro.

Essa é outra razão pela qual a instrução NOP (No Operation - not nothing) da CPU pode realmente fazer com que todo o aplicativo seja executado mais rapidamente.

[EDIT] Naturalmente, a técnica depende de uma configuração de hardware específica. Qual foi a principal razão pela qual muitos jogos Amiga não conseguiram lidar com CPUs mais rápidas: o tempo das instruções estava fora.

Aaron Digulla
fonte
O Amiga não tinha 16 MB de RAM, mais entre 512 kB e 2 MB, dependendo do chipset. Além disso, muitos jogos Amiga não funcionavam com CPUs mais rápidas devido a técnicas como você descreve.
bk1e
1
@ bk1e - A Amiga produziu uma grande variedade de modelos diferentes de computadores, o Amiga 500 enviado com 512K de RAM estendido para 1Meg no meu caso. amigahistory.co.uk/amiedevsys.html é uma amiga com 128Meg Ram
David Waters
@ bk1e: Eu estou corrigido. Minha memória pode falhar, mas a RAM do chip não foi restrita ao primeiro espaço de endereço de 24 bits (ou seja, 16 MB)? E Fast foi mapeado acima disso?
Aaron Digulla 23/02/09
@ Aaron Digulla: Wikipedia tem mais informações sobre as distinções entre chips / rápido / RAM lenta: en.wikipedia.org/wiki/Amiga_Chip_RAM
bk1e
@ bk1e: Meu erro. A CPU de 68k tinha apenas 24 faixas de endereços, por isso eu tinha 16 MB na cabeça.
Aaron Digulla
15

Ponto um que não é a resposta.
Mesmo se você nunca programar nele, acho útil conhecer pelo menos um conjunto de instruções do assembler. Isso faz parte da busca interminável dos programadores de saber mais e, portanto, ser melhor. Também é útil ao entrar em estruturas para as quais você não tem o código-fonte e ter pelo menos uma idéia aproximada do que está acontecendo. Também ajuda a entender JavaByteCode e .Net IL, pois ambos são semelhantes ao assembler.

Para responder à pergunta quando você tiver uma pequena quantidade de código ou uma grande quantidade de tempo. Mais útil para uso em chips incorporados, onde a baixa complexidade de chips e a baixa concorrência nos compiladores direcionados a esses chips podem dar um pulo à balança em favor dos seres humanos. Também para dispositivos restritos, você costuma trocar o tamanho do código / tamanho da memória / desempenho de uma maneira que seria difícil instruir um compilador a fazer. Por exemplo, eu sei que essa ação do usuário não é chamada com frequência, por isso terei um tamanho de código pequeno e desempenho ruim, mas essa outra função semelhante é usada a cada segundo, então terei um tamanho de código maior e desempenho mais rápido. Esse é o tipo de troca que um programador de montagem qualificado pode usar.

Eu também gostaria de acrescentar que existe muito meio-termo no qual você pode codificar em C compilar e examinar o Assembly produzido, depois alterar o código C ou ajustar e manter como assembly.

Meu amigo trabalha em microcontroladores, atualmente chips para controlar pequenos motores elétricos. Ele trabalha em uma combinação de baixo nível ce Assembléia. Ele me contou uma vez um bom dia de trabalho, onde reduziu o loop principal de 48 instruções para 43. Ele também se depara com escolhas como o código cresceu para preencher o chip de 256k e a empresa está querendo um novo recurso.

  1. Remover um recurso existente
  2. Reduza o tamanho de alguns ou de todos os recursos existentes, talvez ao custo de desempenho.
  3. Defenda a mudança para um chip maior com um custo mais alto, maior consumo de energia e maior fator de forma.

Gostaria de adicionar como desenvolvedor comercial um portfólio ou idiomas, plataformas, tipos de aplicativos que nunca senti a necessidade de mergulhar na montagem de escrita. Eu sempre apreciei o conhecimento que adquiri sobre isso. E às vezes depurado nele.

Sei que respondi muito mais à pergunta "por que devo aprender montador", mas sinto que é uma pergunta mais importante do que quando é mais rápida.

então vamos tentar mais uma vez Você deve estar pensando em montagem

  • trabalhando na função do sistema operacional de baixo nível
  • Trabalhando em um compilador.
  • Trabalhando em um chip extremamente limitado, sistema embarcado etc.

Lembre-se de comparar seu assembly ao compilador gerado para ver qual é mais rápido / menor / melhor.

David.

David Waters
fonte
4
+1 por considerar aplicativos incorporados em chips minúsculos. Muitos engenheiros de software aqui não consideram incorporado ou pensam que isso significa um telefone inteligente (32 bits, MB RAM, MB flash).
Martin
1
Aplicativos incorporados no tempo são um ótimo exemplo! Muitas vezes existem instruções estranhas (mesmo as realmente simples, como avr's sbie cbi), que os compiladores costumavam (e às vezes ainda o fazem) não aproveitar ao máximo, devido ao seu conhecimento limitado do hardware.
Felixphew
15

Estou surpreso que ninguém tenha dito isso. A strlen()função é muito mais rápida se escrita em assembly! Em C, a melhor coisa que você pode fazer é

int c;
for(c = 0; str[c] != '\0'; c++) {}

enquanto estiver na montagem, você pode acelerar consideravelmente:

mov esi, offset string
mov edi, esi
xor ecx, ecx

lp:
mov ax, byte ptr [esi]
cmp al, cl
je  end_1
cmp ah, cl
je end_2
mov bx, byte ptr [esi + 2]
cmp bl, cl
je end_3
cmp bh, cl
je end_4
add esi, 4
jmp lp

end_4:
inc esi

end_3:
inc esi

end_2:
inc esi

end_1:
inc esi

mov ecx, esi
sub ecx, edi

o comprimento está em ecx. Isso compara 4 caracteres por vez, por isso é 4 vezes mais rápido. E pense que, usando a palavra de ordem alta de eax e ebx, será 8 vezes mais rápido que a rotina C anterior!

BlackBear
fonte
3
Como isso se compara com os de strchr.nfshost.com/optimized_strlen_function ?
Njalj
@ninjalj: eles são a mesma coisa :) eu não pensei que isso pode ser feito desta forma em C. Ele pode ser ligeiramente melhorada eu acho
BlackBear
Ainda existe uma operação AND bit a bit antes de cada comparação no código C. É possível que o compilador seja inteligente o suficiente para reduzir isso a comparações de bytes altos e baixos, mas eu não apostaria dinheiro nisso. Na verdade, existe um algoritmo de loop mais rápido baseado na propriedade que (word & 0xFEFEFEFF) & (~word + 0x80808080)é zero se todos os bytes no word forem diferentes de zero.
precisa saber é o seguinte
@MichaWiedenmann true, eu deveria carregar bx depois de comparar os dois caracteres em ax. Obrigado
BlackBear
14

As operações de matriz usando instruções SIMD são provavelmente mais rápidas que o código gerado pelo compilador.

Mehrdad Afshari
fonte
Alguns compiladores (o VectorC, se bem me lembro) geram código SIMD, portanto, mesmo isso provavelmente não é mais um argumento para o uso de código de montagem.
OregonGhost 23/02/09
Compiladores criar um código ciente SSE, de modo que o argumento não é verdadeiro
vartec
5
Para muitas dessas situações, você pode usar as intrísicas SSE em vez da montagem. Isso tornará seu código mais portátil (gcc visual c ++, 64bit, 32bit etc) e você não precisará alocar registradores.
23411 Laserallan
1
Claro que sim, mas a pergunta não perguntou onde devo usar o assembly em vez de C. Ele dizia quando o compilador C não gera um código melhor. Eu assumi uma fonte C que não está usando chamadas diretas do SSE ou assembly embutido.
Mehrdad Afshari
9
Mehrdad está certo, no entanto. Conseguir o SSE correto é bastante difícil para o compilador e até mesmo em situações óbvias (para humanos, ou seja) que a maioria dos compiladores não o emprega.
Konrad Rudolph
13

Não posso dar exemplos específicos porque isso aconteceu há muitos anos, mas havia muitos casos em que o montador escrito à mão podia superar qualquer compilador. Por quais razões:

  • Você pode se desviar das convenções de chamada, passando argumentos nos registros.

  • Você pode considerar cuidadosamente como usar registradores e evitar o armazenamento de variáveis ​​na memória.

  • Para coisas como tabelas de salto, você pode evitar ter que checar o índice.

Basicamente, os compiladores fazem um bom trabalho de otimização, e isso quase sempre é "bom o suficiente", mas em algumas situações (como renderização de gráficos) em que você está pagando caro por cada ciclo, pode usar atalhos porque conhece o código , onde um compilador não poderia porque precisa estar do lado seguro.

De fato, ouvi falar de alguns códigos de renderização gráfica em que uma rotina, como uma rotina de desenho de linha ou preenchimento de polígono, na verdade gerava um pequeno bloco de código de máquina na pilha e o executava ali, para evitar a tomada contínua de decisões sobre estilo de linha, largura, padrão, etc.

Dito isto, o que eu quero que um compilador faça é gerar um bom código de montagem para mim, mas não seja muito inteligente, e eles geralmente fazem isso. De fato, uma das coisas que eu odeio no Fortran é embaralhar o código na tentativa de "otimizá-lo", geralmente sem nenhum objetivo significativo.

Geralmente, quando os aplicativos têm problemas de desempenho, isso ocorre devido ao design desnecessário. Hoje em dia, eu nunca recomendaria o assembler para desempenho, a menos que o aplicativo geral já tivesse sido ajustado dentro de uma polegada de sua vida útil, ainda não fosse rápido o suficiente e estivesse gastando todo o seu tempo em loops internos apertados.

Adicionado: eu já vi muitos aplicativos escritos em linguagem assembly, e a principal vantagem da velocidade em relação a um idioma como C, Pascal, Fortran etc. foi porque o programador teve muito mais cuidado ao codificar no assembler. Ele ou ela escreverá aproximadamente 100 linhas de código por dia, independentemente do idioma, e em um idioma do compilador que será igual a 3 ou 400 instruções.

Mike Dunlavey
fonte
8
+1: "Você pode se desviar das convenções de chamada". Os compiladores C / C ++ tendem a ser péssimos ao retornar vários valores. Eles costumam usar o formulário sret, em que a pilha do chamador aloca um bloco contíguo para uma estrutura e passa uma referência a ele para o destinatário preencher. O retorno de vários valores nos registradores é várias vezes mais rápido.
precisa saber é o seguinte
1
@ Jon: compiladores C / C ++ fazer isso muito bem quando a função é embutido (funções não inlined tem que estar de acordo com a ABI, esta não é uma limitação de C e C ++, mas o modelo que liga)
Ben Voigt
@BenVoigt: Aqui está um exemplo contrário flyingfrogblog.blogspot.co.uk/2012/04/…
Jon Harrop
2
Não vejo nenhuma chamada de função sendo incorporada lá.
Ben Voigt
13

Alguns exemplos da minha experiência:

  • Acesso a instruções que não são acessíveis a partir de C. Por exemplo, muitas arquiteturas (como x86-64, IA-64, DEC Alpha e MIPS ou PowerPC de 64 bits) suportam uma multiplicação de 64 por 64 bits, produzindo um resultado de 128 bits. Recentemente, o GCC adicionou uma extensão que fornece acesso a essas instruções, mas antes dessa montagem era necessária. E o acesso a essas instruções pode fazer uma enorme diferença nas CPUs de 64 bits ao implementar algo como RSA - às vezes até um fator de 4 melhorias no desempenho.

  • Acesso a sinalizadores específicos da CPU. O que mais me mordeu é a bandeira de transporte; ao fazer uma adição de precisão múltipla, se você não tiver acesso ao bit de transporte da CPU, deve-se comparar o resultado para ver se ele transbordou, o que requer mais 3-5 instruções por membro; e pior, que são bastante seriais em termos de acesso a dados, o que mata o desempenho em processadores superescalares modernos. Ao processar milhares de números inteiros seguidos, poder usar o addc é uma grande vitória (também existem problemas superescalares com contenção no bit de transporte, mas as CPUs modernas lidam muito bem com ele).

  • SIMD. Mesmo os compiladores de autovectorização só podem executar casos relativamente simples; portanto, se você deseja um bom desempenho SIMD, infelizmente é necessário escrever o código diretamente. É claro que você pode usar intrínsecos em vez de assembly, mas quando estiver no nível intrínseco, basicamente estará escrevendo o assembly de qualquer maneira, apenas usando o compilador como alocador de registro e (nominalmente) agendador de instruções. (Costumo usar intrínsecas para o SIMD simplesmente porque o compilador pode gerar os prólogos de funções e outros enfeites para mim, para que eu possa usar o mesmo código no Linux, OS X e Windows sem precisar lidar com problemas de ABI, como convenções de chamada de função, mas outros que os intrínsecos do SSE realmente não são muito bons - os do Altivec parecem melhores, embora eu não tenha muita experiência com eles).correção de erro AES ou SIMD com fragmentação de bits - pode-se imaginar um compilador que pode analisar algoritmos e gerar esse código, mas parece-me que um compilador tão inteligente está a pelo menos 30 anos da existência (na melhor das hipóteses).

Por outro lado, máquinas multicore e sistemas distribuídos mudaram muitas das maiores vitórias de desempenho em outra direção - obtenha uma velocidade extra de 20% escrevendo seus loops internos em montagem, ou 300% executando-os em vários núcleos, ou 10000% em executando-os em um cluster de máquinas. E, é claro, otimizações de alto nível (coisas como futuros, memorização etc.) geralmente são muito mais fáceis de fazer em uma linguagem de nível superior, como ML ou Scala do que C ou asm, e geralmente podem proporcionar uma vitória de desempenho muito maior. Portanto, como sempre, há trocas a serem feitas.

Jack Lloyd
fonte
2
@Dennis, razão pela qual escrevi 'É claro que você pode usar intrínsecos ao invés de assembly, mas quando estiver no nível intrínseco, basicamente estará escrevendo o assembly de qualquer maneira, apenas usando o compilador como alocador de registro e (nominalmente) agendador de instruções'.
Jack Lloyd
Além disso, o código SIMD com base intrínseca tende a ser menos legível do que o mesmo código escrito no assembler: Muito código SIMD se baseia em reinterpretações implícitas dos dados nos vetores, o que é uma PITA a ser usada pelos intrínsecos do compilador de tipos de dados.
cmaster - reinstate monica em
10

Loops apertados, como ao brincar com imagens, já que uma imagem pode consistir em milhões de pixels. Sentar e descobrir como fazer o melhor uso do número limitado de registros do processador pode fazer a diferença. Aqui está uma amostra da vida real:

http://danbystrom.se/2008/12/22/optimizing-away-ii/

Geralmente, os processadores têm algumas instruções esotéricas que são especializadas demais para um compilador se incomodar, mas às vezes um programador de assembler pode fazer bom uso delas. Veja a instrução XLAT, por exemplo. Realmente ótimo se você precisar fazer pesquisas de tabela em um loop e a tabela estiver limitada a 256 bytes!

Atualizado: Ah, apenas pense no que é mais crucial quando falamos de loops em geral: o compilador geralmente não tem idéia de quantas iterações serão o caso comum! Somente o programador sabe que um loop será iterado MUITAS vezes e que, portanto, será benéfico se preparar para o loop com algum trabalho extra, ou se será iterado tão poucas vezes que a configuração realmente levará mais tempo do que as iterações esperado.

Dan Byström
fonte
3
A otimização direcionada por perfil fornece ao compilador informações sobre a frequência com que um loop é usado.
Zan Lynx
10

Com mais freqüência do que você pensa, C precisa fazer coisas que parecem desnecessárias do ponto de vista de um codificador de Assembléia, apenas porque os padrões C dizem isso.

Promoção inteira, por exemplo. Se você deseja alterar uma variável char em C, normalmente se espera que o código faça exatamente isso, uma única mudança de bit.

Os padrões, no entanto, impõem ao compilador fazer um sinal estender para int antes da mudança e truncar o resultado para char posteriormente, o que pode complicar o código, dependendo da arquitetura do processador de destino.

mfro
fonte
Os compiladores de qualidade para micros pequenos, durante anos, foram capazes de evitar o processamento das partes superiores dos valores nos casos em que isso nunca poderia afetar significativamente os resultados. As regras de promoção causam problemas, mas geralmente nos casos em que um compilador não tem como saber quais casos de canto são e não são relevantes.
Supercat
9

Na verdade, você não sabe se o seu código C bem escrito é realmente rápido se você não examinou a desmontagem do que o compilador produz. Muitas vezes você olha para ele e vê que "bem escrito" era subjetivo.

Portanto, não é necessário escrever no assembler para obter o código mais rápido de todos os tempos, mas certamente vale a pena conhecer o assembler pelo mesmo motivo.

sharptooth
fonte
2
"Portanto, não é necessário escrever no assembler para obter o código mais rápido de todos os tempos" Bem, eu nunca vi um compilador fazer a coisa ideal em nenhum caso que não fosse trivial. Um ser humano experiente pode fazer melhor que o compilador em praticamente todos os casos. Portanto, é absolutamente necessário escrever no assembler para obter "o código mais rápido de todos os tempos".
cmaster - reinstate monica
@ master Na minha experiência, a saída do compilador é bem aleatória. Às vezes é realmente bom e ideal e às vezes é "como esse lixo pode ter sido emitido".
Sharptooth #
9

Li todas as respostas (mais de 30) e não encontrei um motivo simples: o montador é mais rápido que C se você leu e praticou o Manual de referência da otimização de arquiteturas Intel® 64 e IA-32 , portanto , o motivo pelo qual a montagem pode ser mais lento é que as pessoas que escrevem uma montagem mais lenta não leram o Manual de Otimização .

Nos velhos tempos da Intel 80286, cada instrução era executada em uma contagem fixa de ciclos de CPU, mas desde o Pentium Pro, lançado em 1995, os processadores Intel tornaram-se superescalares, utilizando o pipelining complexo: Execução fora de ordem e renomeação de registros. Antes disso, no Pentium, produzido em 1993, havia tubulações em U e V: tubulações duplas que podiam executar duas instruções simples em um ciclo de clock se não dependessem uma da outra; mas isso não foi nada para comparar com o que é Renomeação de Registro e Execução Fora de Ordem apareceu no Pentium Pro e quase permaneceu inalterado atualmente.

Para explicar em poucas palavras, o código mais rápido é onde as instruções não dependem dos resultados anteriores, por exemplo, você deve sempre limpar registros inteiros (por movzx) ou usar em add rax, 1vez disso ou inc raxremover a dependência do estado anterior dos sinalizadores, etc.

Você pode ler mais sobre Execução fora de ordem e renomeação de registros, se o tempo permitir, há muitas informações disponíveis na Internet.

Também existem outras questões importantes, como previsão de ramificação, número de unidades de carga e armazenamento, número de portões que executam micro-ops etc., mas a coisa mais importante a considerar é a execução fora de ordem.

A maioria das pessoas simplesmente não está ciente da execução fora de ordem; portanto, eles escrevem seus programas de montagem como para 80286, esperando que suas instruções levem um tempo fixo para serem executadas, independentemente do contexto; enquanto os compiladores C estão cientes da execução fora de ordem e geram o código corretamente. É por isso que o código de pessoas tão inconscientes é mais lento, mas se você perceber, seu código será mais rápido.

Maxim Masiutin
fonte
8

Eu acho que o caso geral em que o assembler é mais rápido é quando um programador de montagem inteligente analisa a saída do compilador e diz "este é um caminho crítico para o desempenho e posso escrever para ser mais eficiente", e então a pessoa o ajusta ou o reescreve. do princípio.

Doug T.
fonte
7

Tudo depende da sua carga de trabalho.

Nas operações do dia-a-dia, C e C ++ são excelentes, mas existem certas cargas de trabalho (quaisquer transformações que envolvam vídeo (compactação, descompactação, efeitos de imagem etc.)) que praticamente exigem desempenho de montagem.

Geralmente, eles também envolvem o uso de extensões de chipset específicas da CPU (MME / MMX / SSE / qualquer que seja) que são ajustadas para esses tipos de operação.

ReinstateMonica Larry Osterman
fonte
6

Eu tenho uma operação de transposição de bits que precisa ser feita, em 192 ou 256 bits a cada interrupção, que acontece a cada 50 microssegundos.

Isso acontece por um mapa fixo (restrições de hardware). Usando C, demorou cerca de 10 microssegundos para fazer. Quando traduzi isso para o Assembler, levando em consideração os recursos específicos deste mapa, o cache específico do registro e o uso de operações orientadas a bits; demorou menos de 3,5 microssegundos para executar.

SurDin
fonte
6

Talvez valha a pena examinar Optimizing Immutable and Purity por Walter Bright . Não é um teste com perfil, mas mostra um bom exemplo de diferença entre o ASM manuscrito e o compilador gerado. Walter Bright escreve otimizadores de compiladores para que valha a pena olhar para os outros posts do blog.

James Brooks
fonte
5

A resposta simples ... Quem conhece bem a montagem (também tem a referência ao lado e está tirando proveito de todos os pequenos recursos de cache e pipeline do processador, etc.) é capaz de produzir código muito mais rápido do que qualquer outro compilador.

No entanto, a diferença atualmente não importa no aplicativo típico.

Longpoke
fonte
1
Você esqueceu de dizer "com muito tempo e esforço" e "criando um pesadelo de manutenção". Um colega meu estava trabalhando na otimização de uma seção crítica do desempenho do código do SO, e ele trabalhou muito mais em C do que em assembly, pois permitiu investigar o impacto no desempenho de alterações de alto nível dentro de um prazo razoável.
Artelius
Concordo. Às vezes, você usa macros e scripts para gerar código de montagem para economizar tempo e desenvolver-se rapidamente. Atualmente, a maioria dos montadores tem macros; caso contrário, você pode criar um pré-processador (simples) de macro usando um script Perl (bastante simples RegEx).
Este. Precisamente. O compilador para vencer os especialistas em domínio ainda não foi inventado.
cmaster - reinstate monica em
4

Uma das possibilidades da versão CP / M-86 do PolyPascal (irmão de Turbo Pascal) era substituir o recurso "usar bios para exibir caracteres de saída na tela" por uma rotina de linguagem de máquina que, em essência, foi dado o x, y, e a corda para colocar lá.

Isso permitiu atualizar a tela muito, muito mais rápido do que antes!

Havia espaço no binário para incorporar o código da máquina (algumas centenas de bytes) e também havia outras coisas, então era essencial espremer o máximo possível.

Acontece que, como a tela tinha 80x25, as duas coordenadas poderiam caber em um byte cada, portanto, ambas cabiam em uma palavra de dois bytes. Isso permitiu fazer os cálculos necessários em menos bytes, pois uma única adição poderia manipular os dois valores simultaneamente.

Que eu saiba, não há compiladores C que possam mesclar vários valores em um registro, fazer instruções SIMD neles e dividi-los novamente mais tarde (e não acho que as instruções da máquina sejam mais curtas).

Thorbjørn Ravn Andersen
fonte
4

Um dos trechos de montagem mais famosos é o loop de mapeamento de textura de Michael Abrash ( exposto em detalhes aqui ):

add edx,[DeltaVFrac] ; add in dVFrac
sbb ebp,ebp ; store carry
mov [edi],al ; write pixel n
mov al,[esi] ; fetch pixel n+1
add ecx,ebx ; add in dUFrac
adc esi,[4*ebp + UVStepVCarry]; add in steps

Atualmente, a maioria dos compiladores expressa instruções específicas avançadas da CPU como intrínsecas, isto é, funções que são compiladas até as instruções reais. O MS Visual C ++ oferece suporte a intrínsecas para MMX, SSE, SSE2, SSE3 e SSE4, portanto, você precisa se preocupar menos em ir para a montagem para aproveitar as instruções específicas da plataforma. O Visual C ++ também pode tirar proveito da arquitetura real que você está direcionando com a configuração / ARCH apropriada.

MSN
fonte
Melhor ainda, essas intrínsecas SSE são especificadas pela Intel e, na verdade, são bastante portáteis.
James
4

Dado o programador certo, os programas Assembler sempre podem ser feitos mais rapidamente que seus equivalentes C (pelo menos marginalmente). Seria difícil criar um programa C em que você não pudesse executar pelo menos uma instrução do Assembler.

Bip Bip
fonte
Isso seria um pouco mais correto: "Seria difícil criar um programa C não trivial onde ..." Como alternativa, você poderia dizer: "Seria difícil encontrar um programa C do mundo real onde ..." , existem loops triviais para os quais os compiladores produzem saída ideal. No entanto, boa resposta.
cmaster - restabelece monica
4

O gcc se tornou um compilador amplamente usado. Suas otimizações em geral não são tão boas. Muito melhor do que o programador comum que escreve o assembler, mas o desempenho real não é tão bom. Existem compiladores que são simplesmente incríveis no código que eles produzem. Portanto, como resposta geral, haverá muitos lugares em que você poderá acessar a saída do compilador e ajustar o montador para obter desempenho e / ou simplesmente reescrever a rotina do zero.

old_timer
fonte
8
O GCC realiza otimizações "inteligentes e independentes de plataforma" extremamente inteligentes. No entanto, não é tão bom em utilizar conjuntos de instruções específicos ao máximo. Para um compilador tão portátil, ele faz um bom trabalho.
22609 Artelius
2
acordado. Sua portabilidade, idiomas entrando e metas saindo são surpreendentes. Ser tão portátil pode atrapalhar e realmente ser bom em um idioma ou destino. Portanto, as oportunidades para um humano fazer melhor existem para uma otimização específica em um alvo específico.
old_timer 22/06/09
+1: o GCC certamente não é competitivo na geração de código rápido, mas não tenho certeza porque é portátil. O LLVM é portátil e eu já vi gerar código 4x mais rápido que os GCCs.
31512 Jon Jonop
Prefiro o GCC, já que ele é sólido há muitos anos, além de estar disponível para quase todas as plataformas que podem executar um compilador portátil moderno. Infelizmente, não consegui criar o LLVM (Mac OS X / PPC), por isso provavelmente não poderei mudar para ele. Uma das coisas boas do GCC é que, se você escrever um código criado no GCC, provavelmente estará próximo aos padrões e terá certeza de que ele pode ser criado para praticamente qualquer plataforma.
4

Longpoke, há apenas uma limitação: o tempo. Quando você não tem os recursos para otimizar todas as alterações no código e gastar seu tempo alocando registros, otimize alguns vazamentos e o que não acontecer, o compilador vencerá sempre. Você faz sua modificação no código, recompila e mede. Repita se necessário.

Além disso, você pode fazer muito no lado de alto nível. Além disso, inspecionar o assembly resultante pode dar à IMPRESSÃO que o código é uma porcaria, mas, na prática, ele será executado mais rapidamente do que você pensa que seria mais rápido. Exemplo:

int y = dados [i]; // faça algumas coisas aqui .. call_function (y, ...);

O compilador lerá os dados, empurre-o para empilhar (espalhe) e depois leia da pilha e passe como argumento. Parece uma merda? Na verdade, pode ser uma compensação de latência muito eficaz e resultar em um tempo de execução mais rápido.

// versão otimizada call_function (data [i], ...); // não tão otimizado, afinal ..

A idéia com a versão otimizada era que reduzimos a pressão do registro e evitamos derrames. Mas, na verdade, a versão "merda" foi mais rápida!

Olhando para o código de montagem, apenas olhando para as instruções e concluindo: mais instruções, mais lentas, seriam um julgamento errado.

O importante aqui para prestar atenção é: muitos especialistas em montagem pensam que sabem muito, mas sabem muito pouco. As regras também mudam da arquitetura para a próxima. Não há código x86 com bala de prata, por exemplo, que é sempre o mais rápido. Hoje em dia é melhor seguir as regras práticas:

  • a memória está lenta
  • cache é rápido
  • tente usar em cache melhor
  • quantas vezes você vai sentir falta? você tem estratégia de compensação de latência?
  • você pode executar 10-100 instruções ALU / FPU / SSE para uma única falha de cache
  • arquitetura de aplicativos é importante ..
  • .. mas não ajuda quando o problema não está na arquitetura

Além disso, confiar demais no compilador, transformando magicamente o código C / C ++ mal pensado em código "teoricamente ideal", é uma ilusão. Você precisa conhecer o compilador e a cadeia de ferramentas que usa, se se preocupa com o "desempenho" neste nível inferior.

Compiladores em C / C ++ geralmente não são muito bons em reordenar subexpressões porque as funções têm efeitos colaterais para iniciantes. Linguagens funcionais não sofrem com essa ressalva, mas não se encaixam muito bem no ecossistema atual. Existem opções do compilador para permitir regras de precisão relaxadas que permitem alterar a ordem das operações pelo compilador / vinculador / gerador de código.

Este tópico é um beco sem saída; para a maioria, não é relevante, e o resto, eles sabem o que já estão fazendo.

Tudo se resume a isso: "para entender o que você está fazendo", é um pouco diferente de saber o que você está fazendo.

tiredcoder
fonte