Qual é a melhor abordagem ao escrever funções para software incorporado para obter melhor desempenho? [fechadas]

13

Eu já vi algumas bibliotecas de microcontroladores e suas funções fazem uma coisa de cada vez. Por exemplo, algo como isto:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Em seguida, vêm outras funções que usam esse código de 1 linha que contém uma função para outros fins. Por exemplo:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Não tenho certeza, mas acredito que dessa maneira seria criar mais chamadas para saltos e sobrecarregar o empilhamento dos endereços de retorno toda vez que uma função é chamada ou encerrada. E isso tornaria o programa lento, certo?

Eu procurei e em todos os lugares eles dizem que a regra básica da programação é que uma função deve executar apenas uma tarefa.

Portanto, se eu escrever diretamente um módulo de função InitModule que acerte o relógio, adicione algumas configurações desejadas e faça outra coisa sem chamar funções. É uma péssima abordagem ao escrever software incorporado?


EDIT 2:

  1. Parece que muitas pessoas entenderam essa pergunta como se eu estivesse tentando otimizar um programa. Não, não tenho intenção de fazer . Estou deixando o compilador fazer isso, porque sempre será (espero que não!) Melhor do que eu.

  2. Todas as culpas em mim por escolher um exemplo que represente algum código de inicialização . A questão não tem a intenção de considerar chamadas de função feitas para a finalidade de inicialização. Minha pergunta é : dividir uma determinada tarefa em pequenas funções de várias linhas ( portanto, in-line está fora de questão ), executar dentro de um loop infinito tem alguma vantagem sobre escrever função longa sem nenhuma função aninhada?

Por favor, considere a legibilidade definida na resposta de @Jonk .

MaNyYaCk
fonte
28
Você é muito ingênuo (não pretende ser um insulto) se acredita que qualquer compilador razoável transformará o código cegamente conforme escrito em binários. A maioria dos compiladores modernos é muito boa em identificar quando uma rotina é melhor incorporada e mesmo quando um local de registro versus RAM deve ser usado para armazenar uma variável. Siga as duas regras de otimização: 1) não otimize. 2) não otimize ainda . Torne seu código legível e de manutenção e, ENTÃO, depois de criar um perfil de um sistema em funcionamento, procure otimizar.
akohlsmith
10
@akohlsmith IIRC As três regras de otimização são: 1) Não! 2) Não, realmente não! 3) primeiro perfil, então e só então optimize se você deve - Michael_A._Jackson
esoterik
3
Lembre-se de que "a otimização prematura é a raiz de todos os males (ou pelo menos a maior parte
deles
1
@ Makg: A palavra operativa é prematura . (Como o próximo parágrafo desse documento explica. Literalmente a próxima frase: "No entanto, não devemos desperdiçar nossas oportunidades nesses 3% críticos.") Não otimize até que você precise - você não encontrará a lenta bits até que você tenha algo para criar um perfil - mas também não se envolva em pessimização, por exemplo, usando ferramentas flagrantemente erradas para o trabalho.
cHao 17/07/2018
1
@ Makaw Não sei por que recebi respostas / comentários relacionados à otimização, pois nunca mencionei a palavra e pretendo fazê-lo. A questão é muito mais sobre como escrever funções na Programação Incorporada para obter melhor desempenho.
MaNyYaCk

Respostas:

28

Indiscutivelmente, no seu exemplo, o desempenho não importaria, pois o código é executado apenas uma vez na inicialização.

Como regra geral, eu uso: escreva seu código o mais legível possível e só comece a otimizar se você perceber que seu compilador não está fazendo corretamente sua mágica.

O custo de uma chamada de função em um ISR pode ser o mesmo de uma chamada de função durante a inicialização em termos de armazenamento e tempo. No entanto, os requisitos de tempo durante esse ISR podem ser muito mais críticos.

Além disso, como já observado por outras pessoas, o custo (e o significado do 'custo') de uma chamada de função difere de acordo com a plataforma, o compilador, a configuração de otimização do compilador e os requisitos do aplicativo. Haverá uma enorme diferença entre um 8051 e um córtex-m7, e um marcapasso e um interruptor de luz.

Lanting
fonte
6
OMI, o segundo parágrafo deve estar em negrito e no topo. Não há nada errado em escolher os algoritmos e as estruturas de dados corretos logo de cara, mas se preocupar com a sobrecarga de chamadas de função, a menos que você descubra que é um gargalo real, é definitivamente uma otimização prematura e deve ser evitado.
Fund Monica's Lawsuit
11

Não há vantagem em que eu possa pensar (mas veja a nota do JasonS na parte inferior), agrupando uma linha de código como uma função ou sub-rotina. Exceto, talvez, que você possa nomear a função como "legível". Mas você também pode comentar a linha. E desde que agrupar uma linha de código em uma função custa memória de código, espaço de pilha e tempo de execução, parece-me que é principalmente contraproducente. Em uma situação de ensino? Isso pode fazer algum sentido. Mas isso depende da classe dos alunos, de sua preparação prévia, do currículo e do professor. Principalmente, acho que não é uma boa ideia. Mas essa é a minha opinião.

O que nos leva à linha de fundo. Sua ampla área de perguntas tem sido, há décadas, uma questão de algum debate e permanece até hoje uma questão de algum debate. Então, pelo menos enquanto eu leio sua pergunta, parece-me uma pergunta baseada em opinião (como você fez).

Poderia deixar de ser tão baseado em opiniões quanto fosse, se você fosse mais detalhado sobre a situação e descrevesse cuidadosamente os objetivos que considerava primários. Quanto melhor você definir suas ferramentas de medição, mais objetivas serão as respostas.


Em termos gerais, você deseja fazer o seguinte para qualquer codificação. (Abaixo, assumirei que estamos comparando abordagens diferentes, todas as quais atingem os objetivos. Obviamente, qualquer código que falha ao executar as tarefas necessárias é pior do que o código que obtém êxito, independentemente de como ele seja escrito.)

  1. Seja consistente com sua abordagem, para que outra pessoa que esteja lendo seu código possa entender como você aborda seu processo de codificação. Ser inconsistente é provavelmente o pior crime possível. Isso não apenas torna difícil para os outros, mas também para você voltar ao código anos depois.
  2. Na medida do possível, tente organizar as coisas para que a inicialização de várias seções funcionais possa ser realizada sem levar em consideração pedidos. Onde a encomenda é necessária, se for devido a um acoplamento próximo de duas subfunções altamente relacionadas, considere uma inicialização única para ambas, para que possa ser reordenada sem causar danos. Se isso não for possível, documente o requisito de pedido de inicialização.
  3. O conhecimento encapsulado em exatamente um lugar, se possível. As constantes não devem ser duplicadas em todo o lugar no código. Equações que resolvem alguma variável devem existir em um e apenas um lugar. E assim por diante. Se você copiar e colar um conjunto de linhas que executam algum comportamento necessário em vários locais, considere uma maneira de capturar esse conhecimento em um local e usá-lo onde necessário. Por exemplo, se você tem uma estrutura em árvore que deve ser percorrida de uma maneira específica, nãoreplique o código de caminhada na árvore em todos os lugares em que você precisa percorrer os nós da árvore. Em vez disso, capture o método de caminhar em árvores em um só lugar e use-o. Dessa forma, se a árvore mudar e o método andando mudar, você terá apenas um lugar para se preocupar e todo o restante do código "funcionará corretamente".
  4. Se você espalhar todas as suas rotinas em uma folha de papel enorme e plana, com setas conectando-as como são chamadas por outras rotinas, você verá em qualquer aplicativo que haverá "grupos" de rotinas que têm muitas e muitas flechas entre si, mas apenas algumas flechas fora do grupo. Portanto, haverá limites naturais de rotinas estreitamente acopladas e conexões pouco acopladas entre outros grupos de rotinas estreitamente acopladas. Use esse fato para organizar seu código em módulos. Isso limitará a aparente complexidade do seu código, substancialmente.

O acima descrito é geralmente verdade sobre toda a codificação. Não discuti o uso de parâmetros, variáveis ​​globais locais ou estáticas, etc. O motivo é que, para a programação incorporada, o espaço do aplicativo geralmente coloca novas restrições extremas e muito significativas e é impossível discutir todos eles sem discutir todos os aplicativos incorporados. E isso não está acontecendo aqui, de qualquer maneira.

Essas restrições podem ser qualquer uma (e mais) delas:

  • Limitações de custo severas que exigem MCUs extremamente primitivas com RAM minúscula e quase nenhuma contagem de pinos de E / S. Para esses, novos conjuntos de regras se aplicam. Por exemplo, talvez você precise escrever no código do assembly porque não há muito espaço no código. Talvez você precise usar SOMENTE variáveis ​​estáticas porque o uso de variáveis ​​locais é muito caro e consome tempo. Pode ser necessário evitar o uso excessivo de sub-rotinas porque (por exemplo, algumas peças Microchip PIC), existem apenas 4 registros de hardware nos quais os endereços de retorno da sub-rotina são armazenados. Portanto, talvez você precise "achatar" dramaticamente seu código. Etc.
  • Limitações severas de energia que exigem código cuidadosamente criado para inicializar e desligar a maior parte do MCU e impõem severas limitações ao tempo de execução do código ao executar em velocidade máxima. Novamente, isso pode exigir alguma codificação de montagem, às vezes.
  • Requisitos de tempo severos. Por exemplo, há momentos em que tive que me certificar de que a transmissão de um dreno aberto 0 levasse EXATAMENTE o mesmo número de ciclos que a transmissão de um 1. E que a amostragem dessa mesma linha também deveria ser realizada com uma fase relativa exata para esse momento. Isso significava que C NÃO poderia ser usado aqui. A única maneira possível de fazer essa garantia é criar cuidadosamente o código de montagem. (E mesmo assim, nem sempre em todos os designs da ALU.)

E assim por diante. (O código de fiação para instrumentação médica essencial à vida também possui um mundo inteiro.)

O resultado aqui é que a codificação incorporada geralmente não é gratuita para todos, onde você pode codificar como faria em uma estação de trabalho. Muitas vezes, existem razões competitivas graves para uma grande variedade de restrições muito difíceis. E eles podem argumentar fortemente contra as respostas mais tradicionais e de ações .


Em relação à legibilidade, acho que o código é legível se for escrito de uma maneira consistente que eu possa aprender enquanto o leio. E onde não há uma tentativa deliberada de ofuscar o código. Realmente não é muito mais necessário.

O código legível pode ser bastante eficiente e pode atender a todos os requisitos acima mencionados. O principal é que você entenda completamente o que cada linha de código que você escreve produz no nível da montagem ou da máquina, conforme você a codifica. O C ++ coloca uma carga séria no programador aqui, porque há muitas situações em que trechos idênticos de código C ++ realmente geram trechos diferentes de código de máquina com desempenhos muito diferentes. Mas C, geralmente, é principalmente uma linguagem "o que você vê é o que recebe". Portanto, é mais seguro nesse sentido.


EDIT por JasonS:

Uso C desde 1978 e C ++ desde 1987 e tenho muita experiência usando ambos para mainframes, minicomputadores e (principalmente) aplicativos incorporados.

Jason traz um comentário sobre o uso de 'inline' como um modificador. (Na minha perspectiva, esse é um recurso relativamente "novo", porque ele simplesmente não existiu por talvez metade da minha vida ou mais usando C e C ++.) O uso de funções em linha pode realmente fazer essas chamadas (mesmo para uma linha de código) bastante prático. E é muito melhor, sempre que possível, do que usar uma macro devido à digitação que o compilador pode aplicar.

Mas existem limitações também. A primeira é que você não pode confiar no compilador para "entender a dica". Pode ser que sim ou que não. E há boas razões para não dar a dica. (Para um exemplo óbvio, se o endereço da função for utilizado, isso requer a instanciação da função e o uso do endereço para fazer a chamada exigirá ... uma chamada. O código não pode ser incorporado então.) outras razões também. Os compiladores podem ter uma ampla variedade de critérios pelos quais julgam como lidar com a dica. E como programador, isso significa que você devegaste algum tempo aprendendo sobre esse aspecto do compilador; caso contrário, é provável que você tome decisões com base em idéias defeituosas. Portanto, isso adiciona um ônus ao escritor do código e também a qualquer leitor e também a quem planeja portar o código para outro compilador.

Além disso, os compiladores C e C ++ oferecem suporte à compilação separada. Isso significa que eles podem compilar uma parte do código C ou C ++ sem compilar nenhum outro código relacionado para o projeto. Para codificar em linha, supondo que o compilador possa optar por fazê-lo, ele não apenas deve ter a declaração "no escopo", mas também deve ter a definição. Geralmente, os programadores trabalharão para garantir que este seja o caso se eles estiverem usando 'inline'. Mas é fácil os erros aparecerem.

Em geral, embora eu também use inline onde achar apropriado, eu suponho que não posso confiar nela. Se o desempenho é um requisito significativo, e acho que o OP já escreveu claramente que houve um impacto significativo no desempenho quando foram para uma rota mais "funcional", então eu certamente escolheria evitar confiar na linha como prática de codificação e em vez disso, seguiria um padrão ligeiramente diferente, mas inteiramente consistente, de escrever código.

Uma observação final sobre 'inline' e definições sendo "dentro do escopo" para uma etapa de compilação separada. É possível (nem sempre confiável) que o trabalho seja realizado no estágio de vinculação. Isso pode ocorrer se, e somente se, um compilador C / C ++ enterrar detalhes suficientes nos arquivos de objeto para permitir que um vinculador atue em solicitações "em linha". Pessoalmente, não experimentei um sistema de vinculação (fora da Microsoft) que suporta esse recurso. Mas isso pode ocorrer. Novamente, se deve ou não confiar, dependerá das circunstâncias. Mas geralmente presumo que isso não tenha sido colocado no linker, a menos que eu saiba de outra forma com base em boas evidências. E se eu confiar nisso, será documentado em um lugar de destaque.


C ++

Para os interessados, aqui está um exemplo de por que permaneço bastante cauteloso em C ++ ao codificar aplicativos incorporados, apesar de sua pronta disponibilidade hoje. Vou descartar alguns termos que acho que todos os programadores C ++ incorporados precisam conhecer a frio :

  • especialização parcial do modelo
  • vtables
  • objeto base virtual
  • quadro de ativação
  • ativação do quadro de ativação
  • uso de ponteiros inteligentes em construtores e por que
  • otimização do valor de retorno

Essa é apenas uma pequena lista. Se você ainda não sabe tudo sobre esses termos e por que os listei (e muitos mais não listei aqui), desaconselho o uso de C ++ para trabalho incorporado, a menos que não seja uma opção para o projeto .

Vamos dar uma olhada rápida na semântica de exceção do C ++ para obter apenas uma amostra.

UMAB

UMA

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

A

B

O compilador C ++ vê a primeira chamada para foo () e pode apenas permitir que um quadro de ativação normal ocorra, se foo () lançar uma exceção. Em outras palavras, o compilador C ++ sabe que nenhum código extra é necessário neste momento para dar suporte ao processo de desenrolamento de quadros envolvido no tratamento de exceções.

Porém, depois que a String s é criada, o compilador C ++ sabe que deve ser destruído adequadamente antes que um desenrolamento de quadros possa ser permitido, se uma exceção ocorrer posteriormente. Portanto, a segunda chamada para foo () é semanticamente diferente da primeira. Se a segunda chamada para foo () gerar uma exceção (o que pode ou não ser feito), o compilador deve ter colocado um código projetado para lidar com a destruição de String s antes de permitir que o quadro normal ocorra. Isso é diferente do código necessário para a primeira chamada para foo ().

(É possível adicionar decorações adicionais em C ++ para ajudar a limitar esse problema. Mas o fato é que os programadores que usam C ++ precisam estar muito mais cientes das implicações de cada linha de código que escrevem.)

Diferentemente do malloc de C, o novo C ++ usa exceções para sinalizar quando não é possível executar a alocação de memória bruta. Então será 'dynamic_cast'. (Consulte a 3ª edição da Stroustrup, The C ++ Programming Language, páginas 384 e 385 para obter as exceções padrão em C ++.) Os compiladores podem permitir que esse comportamento seja desabilitado. Mas, em geral, você terá uma sobrecarga devido a prólogos e epílogos de manipulação de exceção adequadamente formados no código gerado, mesmo quando as exceções realmente não ocorrerem e mesmo quando a função que está sendo compilada não tiver realmente nenhum bloco de manipulação de exceção. (Stroustrup lamentou isso publicamente).

Sem a especialização parcial de modelos (nem todos os compiladores C ++ oferecem suporte), o uso de modelos pode significar desastre para a programação incorporada. Sem ele, o código bloom é um risco sério que pode matar um projeto incorporado de memória pequena rapidamente.

Quando uma função C ++ retorna um objeto, um compilador temporário sem nome é criado e destruído. Alguns compiladores C ++ podem fornecer código eficiente se um construtor de objetos for usado na instrução de retorno, em vez de um objeto local, reduzindo as necessidades de construção e destruição de um objeto. Mas nem todo compilador faz isso e muitos programadores de C ++ nem sequer estão cientes dessa "otimização do valor de retorno".

Fornecer um construtor de objeto com um único tipo de parâmetro pode permitir que o compilador C ++ encontre um caminho de conversão entre dois tipos de maneiras completamente inesperadas para o programador. Esse tipo de comportamento "inteligente" não faz parte do C.

Uma cláusula catch que especifica um tipo de base "fatiará" um objeto derivado lançado, porque o objeto lançado é copiado usando o "tipo estático" da cláusula catch e não o "tipo dinâmico" do objeto. Uma fonte incomum de miséria de exceção (quando você sente que pode pagar exceções no seu código incorporado).

Os compiladores C ++ podem gerar automaticamente construtores, destruidores, construtores de cópia e operadores de atribuição para você, com resultados indesejados. Leva tempo para obter facilidade com os detalhes disso.

Passar matrizes de objetos derivados para uma função que aceita matrizes de objetos base, raramente gera avisos do compilador, mas quase sempre gera comportamento incorreto.

Como o C ++ não invoca o destruidor de objetos parcialmente construídos quando ocorre uma exceção no construtor de objetos, o tratamento de exceções nos construtores geralmente exige "ponteiros inteligentes" para garantir que fragmentos construídos no construtor sejam destruídos corretamente se ocorrer uma exceção no local. . (Consulte Stroustrup, páginas 367 e 368.) Esse é um problema comum ao escrever boas classes em C ++, mas é claro evitado em C, pois C não possui a semântica de construção e destruição incorporada. Escrevendo código adequado para lidar com a construção de subobjetos em um objeto significa escrever código que deve lidar com esse problema semântico exclusivo em C ++; em outras palavras, "escrevendo em torno" de comportamentos semânticos do C ++.

C ++ pode copiar objetos passados ​​para parâmetros de objeto. Por exemplo, nos seguintes fragmentos, a chamada "rA (x);" pode fazer com que o compilador C ++ chame um construtor para o parâmetro p, a fim de chamar o construtor de cópia para transferir o objeto x para o parâmetro p, então outro construtor para o objeto de retorno (um temporário sem nome) da função rA, que obviamente é copiado do parâmetro p. Pior ainda, se a classe A tiver seus próprios objetos que precisam de construção, isso pode ser causado por um telescópio desastroso. (O programador CA evitaria a maior parte desse lixo, otimizando manualmente, pois os programadores C não possuem uma sintaxe tão prática e precisam expressar todos os detalhes, um de cada vez.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Finalmente, uma breve nota para programadores C. longjmp () não possui um comportamento portátil em C ++. (Alguns programadores de C usam isso como um tipo de mecanismo de "exceção".) Alguns compiladores de C ++ realmente tentam configurar as coisas para limpar quando o longjmp é obtido, mas esse comportamento não é portátil no C ++. Se o compilador limpar objetos construídos, ele não será portátil. Se o compilador não os limpar, os objetos não serão destruídos se o código deixar o escopo dos objetos construídos como resultado do longjmp e o comportamento for inválido. (Se o uso de longjmp em foo () não deixar um escopo, o comportamento poderá ser bom.) Isso não é muito usado pelos programadores incorporados em C, mas eles devem se conscientizar desses problemas antes de usá-los.

jonk
fonte
4
Esse tipo de função usado apenas uma vez nunca é compilado como chamada de função, o código é simplesmente colocado lá sem nenhuma chamada.
Dorian
6
@Dorian - seu comentário pode ser verdadeiro em determinadas circunstâncias para determinados compiladores. Se a função for estática no arquivo, o compilador terá a opção de tornar o código embutido. se for visível externamente, mesmo que nunca seja realmente chamado, é necessário que haja uma maneira de chamar a função.
uɐɪ
1
@ Jonk - Outro truque que você não mencionou em uma boa resposta é escrever funções macro simples que executam a inicialização ou configuração como código embutido expandido. Isso é especialmente útil nos processadores muito pequenos, onde a profundidade da chamada RAM / pilha / função é limitada.
uɐɪ
@ Iouɐɪ Sim, senti falta de discutir macros em C. Essas são preteridas em C ++, mas uma discussão sobre esse ponto pode ser útil. Eu posso resolver isso, se eu puder descobrir algo útil para escrever sobre isso.
21718
1
@ Jonk - Eu discordo completamente de sua primeira frase. Um exemplo como o inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }que é chamado em vários lugares é um candidato perfeito.
Jason S
8

1) Código de legibilidade e manutenção primeiro. O aspecto mais importante de qualquer base de código é que ela é bem estruturada. Um software bem escrito tende a ter menos erros. Pode ser necessário fazer alterações em algumas semanas / meses / anos, e isso ajuda imensamente se o seu código for de boa leitura. Ou talvez alguém precise fazer uma mudança.

2) O desempenho do código executado uma vez não importa muito. Cuidado com o estilo, não com o desempenho

3) Mesmo o código em loops apertados precisa estar correto em primeiro lugar. Se você enfrentar problemas de desempenho, otimize quando o código estiver correto.

4) Se você precisa otimizar, precisa medir! Não importa se você pensa ou alguém lhe diz que isso static inlineé apenas uma recomendação para o compilador. Você precisa dar uma olhada no que o compilador faz. Você também deve medir se o alinhamento melhorou o desempenho. Em sistemas embarcados, você também precisa medir o tamanho do código, pois a memória de código geralmente é bastante limitada. Esta é a regra mais importante que distingue engenharia de adivinhação. Se você não mediu, não ajudou. Engenharia está medindo. A ciência está anotando;)

Crazor
fonte
2
A única crítica que tenho do seu excelente post é o ponto 2). É verdade que o desempenho do código de inicialização é irrelevante - mas em um ambiente incorporado, o tamanho pode importar. (Mas isso não substitui o ponto 1; comece a otimizar o tamanho quando você precisar - e não antes)
Martin Bonner suporta Monica
2
O desempenho do código de inicialização pode ser inicialmente irrelevante. Quando você adiciona o modo de baixa energia e deseja se recuperar rapidamente para lidar com o evento de ativação, ele se torna relevante.
Berendi - protestando em
5

Quando uma função é chamada apenas em um local (mesmo dentro de outra função), o compilador sempre coloca o código nesse local, em vez de realmente chamar a função. Se a função é chamada em muitos lugares, faz sentido usar uma função pelo menos do ponto de vista do tamanho do código.

Após a compilação, o código não terá várias chamadas; a legibilidade será bastante aprimorada.

Além disso, você desejará ter, por exemplo, o código de inicialização do ADC na mesma biblioteca com outras funções do ADC que não estão no arquivo c principal.

Muitos compiladores permitem especificar diferentes níveis de otimização para velocidade ou tamanho do código; portanto, se você tiver uma pequena função chamada em muitos locais, a função será "inline", copiada para lá em vez de chamar.

A otimização da velocidade incorporará as funções em todos os lugares possíveis; a otimização para o tamanho do código chamará a função; no entanto, quando uma função for chamada apenas em um local, como no seu caso, ela será sempre "incorporada".

Código como este:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

irá compilar para:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

sem usar nenhuma ligação.

E a resposta à sua pergunta, no seu exemplo ou similar, a legibilidade do código não afeta o desempenho, nada é muito em velocidade ou tamanho do código. É comum usar várias chamadas apenas para tornar o código legível; no final, elas são cumpridas como um código embutido.

Atualize para especificar que as instruções acima não são válidas para compiladores de versão grátis com defeito, como a versão gratuita Microchip XCxx. Esse tipo de chamada de função é uma mina de ouro para o Microchip mostrar quanto melhor é a versão paga e, se você compilar isso, encontrará no ASM exatamente o número de chamadas que possui no código C.

Também não é para programadores burros que esperam usar o ponteiro para uma função embutida.

Esta é a seção eletrônica, não o C C ++ geral ou a seção de programação. A questão é sobre a programação de microcontroladores em que qualquer compilador decente fará a otimização acima por padrão.

Portanto, pare de votar apenas porque, em casos raros e incomuns, isso pode não ser verdade.

Dorian
fonte
15
Se o código fica embutido ou não é um problema específico da implementação do fornecedor do compilador; mesmo usando a palavra-chave inline não garante código inline. É uma dica para o compilador. Certamente bons compiladores incorporarão funções usadas apenas uma vez se souberem sobre eles. Normalmente, isso não acontecerá se houver objetos "voláteis" no escopo.
Peter Smith
9
Esta resposta simplesmente não é verdadeira. Como o @PeterSmith diz, e de acordo com a especificação da linguagem C, o compilador tem a opção de alinhar o código, mas pode não, e em muitos casos não o fará. Existem tantos compiladores diferentes no mundo para tantos processadores de destino diferentes que fazem o tipo de declaração geral nesta resposta e assumem que todos os compiladores colocarão o código em linha quando tiverem apenas a opção de não ser sustentável.
uɐɪ
2
@ pointingouɐɪ Você está apontando casos raros onde não é possível e seria uma má idéia não chamar uma função em primeiro lugar. Eu nunca vi um compilador tão burro para realmente usar a chamada no exemplo simples dado pelo OP.
Dorian
6
Nos casos em que essas funções são chamadas apenas uma vez, otimizar a chamada de função é praticamente um problema. O sistema realmente precisa recuperar todos os ciclos do relógio durante a instalação? Como é o caso da otimização em qualquer lugar - escreva código legível e otimize somente se a criação de perfil mostrar que é necessário .
precisa
5
@ MSalters Não estou preocupado com o que o compilador acaba fazendo aqui - mais sobre como o programador o aborda. Existe um desempenho desprezível, ou desprezível, ao interromper a inicialização, como visto na pergunta.
precisa
2

Primeiro, não há melhor ou pior; é tudo uma questão de opinião. Você está muito certo de que isso é ineficiente. Pode ser otimizado ou não; depende. Normalmente você verá esses tipos de funções, relógio, GPIO, timer, etc. em arquivos / diretórios separados. Os compiladores geralmente não foram capazes de otimizar essas lacunas. Existe uma que eu conheço, mas que não é amplamente usada para coisas como essa.

Único arquivo:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Escolhendo um destino e um compilador para fins de demonstração.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

Isto é o que a maioria das respostas aqui estão dizendo, que você é ingênuo e que tudo isso é otimizado e as funções são removidas. Bem, eles não são removidos, pois são definidos globalmente por padrão. Podemos removê-los se não for necessário fora deste arquivo.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

remove-os agora como estão alinhados.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Mas a realidade é quando você compra bibliotecas de chips ou BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

Definitivamente, você começará a adicionar uma sobrecarga, que tem um custo perceptível para desempenho e espaço. Alguns a cinco por cento de cada um, dependendo do tamanho de cada função.

Por que isso é feito de qualquer maneira? Parte disso é o conjunto de regras que os professores ensinariam ou ainda ensinam para facilitar o código de classificação. As funções devem caber em uma página (quando você imprimiu seu trabalho em papel), não faça isso, não faça isso, etc. Muito disso é criar bibliotecas com nomes comuns para diferentes destinos. Se você tem dezenas de famílias de microcontroladores, algumas das quais compartilham periféricos e outras não, talvez três ou quatro tipos diferentes de UART misturados entre as famílias, GPIOs diferentes, controladores SPI etc. Você pode ter uma função genérica gpio_init (), get_timer_count (), etc. E reutilize essas abstrações para os diferentes periféricos.

Torna-se um caso de manutenção e design de software, com alguma legibilidade possível. Manutenção, legibilidade e desempenho que você não pode ter tudo; você pode escolher apenas um ou dois de cada vez, não os três.

Essa é uma pergunta muito baseada em opiniões, e o exposto acima mostra as três principais maneiras pelas quais isso pode acontecer. Quanto a qual caminho é MELHOR, é estritamente opinião. Está fazendo todo o trabalho em uma função? Uma pergunta baseada em opinião, algumas pessoas preferem desempenho, outras definem modularidade e sua versão de legibilidade como BEST. A questão interessante com o que muitas pessoas chamam de legibilidade é extremamente dolorosa; Para "ver" o código, é necessário ter 50 a 10.000 arquivos abertos de uma só vez e, de alguma forma, tentar ver linearmente as funções em ordem de execução para ver o que está acontecendo. Acho que é o oposto da legibilidade, mas outros acham legível à medida que cada item se encaixa na tela / janela do editor e pode ser consumido inteiro depois de memorizarem as funções que estão sendo chamadas e / ou de um editor que possa entrar e sair da cada função dentro de um projeto.

Esse é outro grande fator quando você vê várias soluções. Editores de texto, IDEs etc. são muito pessoais e vão além do vi vs Emacs. Eficiência de programação, as linhas por dia / mês aumentam se você estiver confortável e eficiente com a ferramenta que está usando. Os recursos da ferramenta podem / vão intencionalmente ou não se inclinam para a forma como os fãs dessa ferramenta escrevem código. E, como resultado, se um indivíduo está escrevendo essas bibliotecas, o projeto reflete até certo ponto esses hábitos. Mesmo que seja uma equipe, os hábitos / preferências do desenvolvedor líder ou do chefe podem ser forçados a permanecer no restante da equipe.

Padrões de codificação que têm muitas preferências pessoais enterradas neles, vi muito religioso vs. Emacs novamente, abas versus espaços, como os colchetes são alinhados, etc.

Como você deve escrever o seu? Como quiser, não há realmente uma resposta errada se funcionar. Existe um código ruim ou arriscado, mas se escrito para que você possa mantê-lo conforme necessário, ele atende aos seus objetivos de projeto, desiste da legibilidade e de alguma manutenção se o desempenho é importante ou vice-versa. Você gosta de nomes curtos de variáveis ​​para que uma única linha de código caiba na largura da janela do editor? Ou nomes longos e excessivamente descritivos para evitar confusão, mas a legibilidade diminui porque você não pode obter uma linha em uma página; agora está visualmente quebrado, mexendo com o fluxo.

Você não vai bater um home run pela primeira vez no bastão. Pode / deve levar décadas para realmente definir seu estilo. Ao mesmo tempo, durante esse período, seu estilo pode mudar, inclinando-se para um lado por um tempo e depois para outro.

Você ouvirá muitas coisas que não otimizam, nunca otimizam e otimização prematura. Mas, como mostrado, projetos como este desde o início criam problemas de desempenho, então você começa a ver hacks para resolver esse problema, em vez de redesenhar desde o início para executar. Concordo que há situações, uma única função que algumas linhas de código podem ser difíceis de manipular o compilador com base no medo do que o compilador fará de outra forma (observe com experiência que esse tipo de codificação se torna fácil e natural, otimizando enquanto escreve, sabendo como o compilador compilará o código), então você deseja confirmar onde o ladrão de ciclo realmente está, antes de atacá-lo.

Você também precisa criar seu código para o usuário até certo ponto. Se este for seu projeto, você é o único desenvolvedor; é o que você quiser. Se você está tentando criar uma biblioteca para doar ou vender, provavelmente deseja que seu código pareça com todas as outras bibliotecas, centenas a milhares de arquivos com pequenas funções, nomes longos de funções e nomes longos de variáveis. Apesar dos problemas de legibilidade e desempenho, na IMO, você encontrará mais pessoas capazes de usar esse código.

old_timer
fonte
4
Verdade? Que "algum destino" e "algum compilador" você usa, posso perguntar?
Dorian
Parece-me mais um ARM8 de 32/64 bits, talvez de um PI raspbery do que um microcontrolador comum. Você leu a primeira frase da pergunta?
187 Dorian
Bem, o compilador não remove funções globais não utilizadas, mas o vinculador remove. Se estiver configurado e usado corretamente, eles não aparecerão no executável.
berendi - protestando em 17/07
Se alguém está se perguntando qual compilador pode otimizar as diferenças de arquivos: os compiladores IAR suportam a compilação de vários arquivos (é assim que eles chamam), o que permite a otimização de arquivos cruzados. Se você jogar todos os arquivos c / cpp nele de uma só vez, você acaba com um executável que contém uma única função: main. Os benefícios de desempenho podem ser bastante profundos.
Arsenal
3
@Arsenal É claro que o gcc suporta inlining, mesmo em unidades de compilação, se chamado corretamente. Consulte gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html e procure a opção -flto.
Peter - Restabelece Monica
1

Regra muito geral - o compilador pode otimizar melhor que você. Obviamente, há exceções se você estiver fazendo coisas muito intensas em loop, mas no geral, se desejar uma boa otimização para velocidade ou tamanho do código, escolha seu compilador com sabedoria.

Dirk Bruere
fonte
Infelizmente, isso é verdade para a maioria dos programadores hoje.
Dorian
0

Com certeza, depende do seu próprio estilo de codificação. Uma regra geral que existe, é que nomes de variáveis ​​e nomes de funções devem ser tão claros e autoexplicativos quanto possível. Quanto mais sub-chamadas ou linhas de código você colocar em uma função, mais difícil será definir uma tarefa clara para essa função. No seu exemplo, você tem uma função initModule()que inicializa coisas e chama sub-rotinas, que depois ajustam o relógio ou definem a configuração . Você pode dizer isso apenas lendo o nome da função. Se você colocar todo o código das sub-rotinas initModule()diretamente, fica menos óbvio o que a função realmente faz. Mas, com frequência, é apenas uma diretriz.

papa
fonte
Obrigado por sua resposta. Posso mudar de estilo, se necessário, para desempenho, mas a questão aqui é: a legibilidade do código afeta o desempenho?
MaNyYaCk
Uma chamada de função resultará em uma chamada ou em um comando jmp, mas na minha opinião é um sacrifício insignificante de recursos. Se você usa padrões de design, às vezes acaba com uma dúzia de camadas de chamadas de função antes de atingir o código atual.
po.pe
@Humpawumpa - Se você está escrevendo para um microcontrolador com apenas 256 ou 64 bytes de RAM, em seguida, uma dúzia de camadas de chamadas de função não é um sacrifício insignificante, não é apenas possível
uɐɪ
Sim, mas estes são dois extremos ... geralmente você tem mais de 256 bytes e está usando menos de uma dúzia de camadas - espero.
po.pe
0

Se uma função realmente faz apenas uma coisa muito pequena, considere fazê-la static inline.

Adicione-o a um arquivo de cabeçalho em vez do arquivo C e use as palavras static inlinepara defini-lo:

static inline void setCLK()
{
    //code to set the clock
}

Agora, se a função for um pouco mais longa, como ultrapassar 3 linhas, pode ser uma boa ideia evitar static inline e adicioná-la ao arquivo .c. Afinal, os sistemas embarcados têm memória limitada e você não deseja aumentar muito o tamanho do código.

Além disso, se você definir a função file1.ce usá-la file2.c, o compilador não a embutirá automaticamente. No entanto, se você o definir file1.hcomo uma static inlinefunção, é provável que o seu compilador o incline.

Essas static inlinefunções são extremamente úteis na programação de alto desempenho. Descobri que eles aumentam o desempenho do código com frequência por um fator superior a três.

juhist
fonte
"como ter mais de 3 linhas" - a contagem de linhas não tem nada a ver com isso; custo inline tem tudo a ver com isso. Eu poderia escrever uma função de 20 linhas perfeita para embutir e uma função de 3 linhas horrível para embutir (por exemplo, functionA () que chama functionB () 3 vezes, functionB () que chama functionC () 3 vezes e alguns outros níveis).
Jason S
Além disso, se você definir a função file1.ce usá-la file2.c, o compilador não a embutirá automaticamente. Falso . Veja, por exemplo, -fltoem gcc ou clang.
Berendi - protestando em 18/07/19
0

Uma dificuldade em tentar escrever código eficiente e confiável para microcontroladores é que alguns compiladores não podem lidar com determinadas semânticas de maneira confiável, a menos que o código use diretivas específicas do compilador ou desative muitas otimizações.

Por exemplo, se tiver um sistema de núcleo único com uma rotina de serviço de interrupção [executada por uma marcação de timer ou o que seja]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

deve ser possível escrever funções para iniciar uma operação de gravação em segundo plano ou aguardar a conclusão:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

e invoque esse código usando:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Infelizmente, com as otimizações completas ativadas, um compilador "inteligente" como gcc ou clang decidirá que não há como o primeiro conjunto de gravações ter efeito sobre o observável do programa e, portanto, pode ser otimizado. Compiladores de qualidade como iccesses são menos propensos a fazer isso se o ato de definir uma interrupção e aguardar a conclusão envolver gravações e leituras voláteis (como é o caso aqui), mas a plataforma visada poricc não é tão popular para sistemas embarcados.

O Padrão ignora deliberadamente questões de qualidade de implementação, considerando que existem várias maneiras razoáveis ​​de lidar com a construção acima:

  1. Uma implementação de qualidade destinada exclusivamente a campos como processamento de números de ponta poderia razoavelmente esperar que o código escrito para esses campos não contenha construções como as mencionadas acima.

  2. Uma implementação de qualidade pode tratar todos os acessos a volatileobjetos como se eles pudessem acionar ações que acessariam qualquer objeto visível para o mundo externo.

  3. Uma implementação simples, mas de qualidade decente, destinada ao uso de sistemas embarcados, pode tratar todas as chamadas para funções não marcadas como "inline" como se elas pudessem acessar qualquer objeto exposto ao mundo exterior, mesmo que não seja tratado volatilecomo descrito em # 2)

A Norma não tenta sugerir quais das abordagens acima seriam mais apropriadas para uma implementação de qualidade, nem exigir que as implementações "em conformidade" sejam de qualidade suficientemente boa para serem utilizadas para qualquer finalidade específica. Consequentemente, alguns compiladores como o gcc ou o clang exigem efetivamente que qualquer código que queira usar esse padrão seja compilado com muitas otimizações desabilitadas.

Em alguns casos, garantir que as funções de E / S estejam em uma unidade de compilação separada e um compilador não terá escolha, mas supor que eles possam acessar qualquer subconjunto arbitrário de objetos expostos ao mundo exterior pode ser um mínimo razoável. A maneira mais perigosa de escrever código que funcione de maneira confiável com o gcc e o clang. Nesses casos, no entanto, o objetivo não é evitar o custo extra de uma chamada de função desnecessariamente, mas aceitar o custo que deveria ser desnecessário em troca da obtenção da semântica necessária.

supercat
fonte
"garantir que as funções de E / S estejam em uma unidade de compilação separada" ... não é uma maneira infalível de evitar problemas de otimização como esses. Pelo menos, o LLVM e eu acredito que o GCC executará a otimização de todo o programa em muitos casos; portanto, você pode decidir incorporar suas funções de IO, mesmo que elas estejam em uma unidade de compilação separada.
Jules
@Jules: Nem todas as implementações são adequadas para escrever software incorporado. Desabilitar a otimização de todo o programa pode ser a maneira mais barata de forçar o gcc ou o clang a se comportar como uma implementação de qualidade adequada para esse fim.
Supercat
@Jules: Uma implementação de alta qualidade destinada à programação de sistemas ou embarcados deve ser configurável para ter semânticas adequadas para esse fim, sem ter que desativar completamente a otimização de todo o programa (por exemplo, tendo a opção de tratar volatileacessos como se eles pudessem potencialmente desencadear acessos arbitrários a outros objetos), mas, por qualquer motivo, o gcc e o clang preferem tratar os problemas de qualidade de implementação como um convite para se comportar de maneira inútil.
Supercat
1
Mesmo as implementações de "alta qualidade" não corrigem o código de buggy. Se buffnão for declarado volatile, ele não será tratado como uma variável volátil, os acessos a ele poderão ser reordenados ou otimizados totalmente se aparentemente não forem usados ​​posteriormente. A regra é simples: marque todas as variáveis ​​que podem ser acessadas fora do fluxo normal do programa (como visto pelo compilador) como volatile. O conteúdo é buffacessado em um manipulador de interrupções? Sim. Então deveria ser volatile.
berendi - protestando
@berendi: Os compiladores podem oferecer garantias além do exigido pela Norma e os compiladores de qualidade o farão. Uma implementação autônoma de qualidade para o uso de sistemas embarcados permitirá que os programadores sintetizem construções mutex, que é essencialmente o que o código faz. Quando magic_write_counté zero, o armazenamento pertence à linha principal. Quando é diferente de zero, pertence ao manipulador de interrupções. Tornar buffvolátil exigiria que todas as funções em qualquer lugar que operam com ele usem volatileponteiros qualificados, o que prejudicaria a otimização muito mais do que ter um compilador ... #
31818