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.
fonte
-O3
bandeira, é provável que você esteja melhor deixando a otimização para o compilador C :-) #Respostas:
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
uint64_t
para multiplicações de 32x32 => 64 bits falha ao otimizar em uma CPU de 64 bits, portanto, você precisa de intrínseca ou__int128
de código eficiente em sistemas de 64 bits.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:
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 é:
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
popcnt
ou bit-scan para encontrar o primeiro ou último conjunto de bits . (O POSIX tem umaffs()
função, mas sua semântica não corresponde a x86bsf
/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
popcnt
instrução (se ativada no momento da compilação), mas é muito mais confiável usar__builtin_popcnt
no GNU C ou no x86 se você estiver apenas segmentando hardware com SSE4.2:_mm_popcnt_u32
from<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,
ntohl
pode 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.fonte
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.
fonte
add di,di / adc al,al / add di,di / adc ah,ah
etc. 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.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):
E alguns números do meu PC executando uma versão padrão build * :
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.
fonte
-ffast-math
. Eles têm um nível de otimização-Ofast
que 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).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 umaloop
instrução são uma demonstração muito impressionante de asm rápido.loop
aparentemente 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. Doisfstp results
insns no final claramente não são ótimos. Retirar o resultado extra da pilha seria melhor com uma não loja. Como ofstp st(0)
IIRC.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.
fonte
ocamlopt
ignora 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.No meu trabalho, há três razões para eu conhecer e usar a montagem. Por ordem de importância:
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.
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:
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.
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.
fonte
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.
fonte
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:
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:
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.
Certas operações de deslocamento de bits (rotação / transporte):
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.
fonte
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):
É 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).
fonte
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.
fonte
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.
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
Lembre-se de comparar seu assembly ao compilador gerado para ver qual é mais rápido / menor / melhor.
David.
fonte
sbi
ecbi
), que os compiladores costumavam (e às vezes ainda o fazem) não aproveitar ao máximo, devido ao seu conhecimento limitado do hardware.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 éenquanto estiver na montagem, você pode acelerar consideravelmente:
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!
fonte
(word & 0xFEFEFEFF) & (~word + 0x80808080)
é zero se todos os bytes no word forem diferentes de zero.As operações de matriz usando instruções SIMD são provavelmente mais rápidas que o código gerado pelo compilador.
fonte
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.
fonte
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.
fonte
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.
fonte
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.
fonte
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.
fonte
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, 1
vez disso ouinc rax
remover 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.
fonte
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.
fonte
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.
fonte
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.
fonte
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.
fonte
O how do assembly do Linux , faz essa pergunta e fornece os prós e os contras do uso de assembly.
fonte
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.
fonte
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).
fonte
Um dos trechos de montagem mais famosos é o loop de mapeamento de textura de Michael Abrash ( exposto em detalhes aqui ):
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.
fonte
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.
fonte
http://cr.yp.to/qhasm.html tem muitos exemplos.
fonte
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.
fonte
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:
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.
fonte