"Otimização prematura é a raiz de todo o mal"
Eu acho que todos nós podemos concordar. E eu tento muito evitar evitar isso.
Mas, recentemente, tenho me perguntado sobre a prática de passar parâmetros por const const em vez de por Value . Aprendi / aprendi que argumentos de função não triviais (ou seja, a maioria dos tipos não primitivos) devem ser passados preferencialmente por referência const - muitos livros que li recomendam isso como uma "melhor prática".
Ainda assim, não posso deixar de me perguntar: compiladores modernos e novos recursos de linguagem podem fazer maravilhas; portanto, o conhecimento que aprendi pode muito bem estar desatualizado e nunca me preocupei em criar um perfil se houver alguma diferença de desempenho entre eles.
void fooByValue(SomeDataStruct data);
e
void fooByReference(const SomeDataStruct& data);
A prática que aprendi - passando referências const (por padrão para tipos não triviais) - é otimização prematura?
fonte
Respostas:
"Otimização prematura" não se trata de usar otimizações antecipadamente . Trata-se de otimizar antes que o problema seja entendido, antes que o tempo de execução seja entendido e, muitas vezes, tornando o código menos legível e menos sustentável para resultados duvidosos.
Usar "const &" em vez de passar um objeto por valor é uma otimização bem compreendida, com efeitos bem entendidos no tempo de execução, praticamente sem esforço e sem efeitos negativos na legibilidade e manutenção. Na verdade, melhora os dois, porque me diz que uma chamada não modifica o objeto transmitido. Portanto, adicionar "const &" logo que você escreve o código NÃO É PREMATURO.
fonte
const&
, então acho que a pergunta é bastante sensata.const& foo
diz que a função não modifica foo, portanto o chamador está seguro. Mas um valor copiado diz que nenhum outro thread pode ser alterado, portanto o chamado é seguro. Certo? Portanto, em um aplicativo multithread, a resposta depende da correção, não da otimização.TL; DR: Passar pela referência const ainda é uma boa idéia em C ++, considerando todas as coisas. Não é uma otimização prematura.
TL; DR2: A maioria dos provérbios não faz sentido, até que eles façam.
Alvo
Esta resposta apenas tenta estender o item vinculado nas Diretrizes Principais do C ++ (mencionadas pela primeira vez no comentário da amon) um pouco.
Esta resposta não tenta abordar a questão de como pensar e aplicar adequadamente os vários provérbios que foram amplamente divulgados nos círculos dos programadores, especialmente a questão da reconciliação entre conclusões ou evidências conflitantes.
Aplicabilidade
Esta resposta se aplica apenas a chamadas de função (escopos aninhados não destacáveis no mesmo encadeamento).
(Nota lateral.) Quando coisas passáveis podem escapar do escopo (ou seja, ter uma vida útil que potencialmente exceda o escopo externo), torna-se mais importante satisfazer a necessidade do aplicativo de gerenciamento da vida útil do objeto antes de qualquer outra coisa. Geralmente, isso requer o uso de referências que também são capazes de gerenciar a vida útil, como ponteiros inteligentes. Uma alternativa pode estar usando um gerente. Observe que lambda é um tipo de escopo destacável; As capturas lambda se comportam como ter escopo de objeto. Portanto, tenha cuidado com capturas lambda. Também tenha cuidado com o modo como o próprio lambda é passado - por cópia ou por referência.
Quando passar por valor
Para valores escalares (primitivas padrão que se ajustam a um registro de máquina e têm valor semântico) para os quais não há necessidade de comunicação por mutabilidade (referência compartilhada), passe por valor.
Para situações em que o chamado exige uma clonagem de um objeto ou agregado, passe por valor, em que a cópia do chamado atende à necessidade de um objeto clonado.
Quando passar por referência, etc.
para todas as outras situações, passe por ponteiros, referências, ponteiros inteligentes, alças (consulte: idioma do corpo da alça), etc. Sempre que este conselho for seguido, aplique o princípio da correção constante como de costume.
Coisas (agregados, objetos, matrizes, estruturas de dados) que são suficientemente grandes no espaço ocupado pela memória devem sempre ser projetadas para facilitar a passagem por referência, por razões de desempenho. Esse conselho se aplica definitivamente quando tem centenas de bytes ou mais. Este conselho é limítrofe quando tem dezenas de bytes.
Paradigmas incomuns
Existem paradigmas de programação para fins especiais, que são pesados por intenção. Por exemplo, processamento de strings, serialização, comunicação de rede, isolamento, quebra de bibliotecas de terceiros, comunicação entre processos de memória compartilhada, etc. Nessas áreas de aplicação ou paradigmas de programação, os dados são copiados de estruturas em estruturas ou, às vezes, reembalados matrizes de bytes.
Como a especificação do idioma afeta essa resposta antes que a otimização seja considerada.
Sub-TL; DR A propagação de uma referência não deve invocar nenhum código; passar por const-reference satisfaz este critério. No entanto, todos os outros idiomas atendem a esse critério sem esforço.
(Programadores iniciantes em C ++ são aconselhados a ignorar esta seção completamente.)
(O início desta seção é parcialmente inspirado na resposta de gnasher729. No entanto, uma conclusão diferente é alcançada.)
O C ++ permite construtores de cópia definidos pelo usuário e operadores de atribuição.
(Esta é (foi) uma escolha ousada que é (foi) incrível e lamentável. É definitivamente uma divergência da norma aceitável de hoje em design de linguagem.)
Mesmo que o programador C ++ não defina um, o compilador C ++ deve gerar esses métodos com base nos princípios da linguagem e determinar se outro código precisa ser executado além de
memcpy
. Por exemplo, umclass
/struct
que contém umstd::vector
membro precisa ter um construtor de cópias e um operador de atribuição que não sejam triviais.Em outros idiomas, os construtores de cópias e a clonagem de objetos são desencorajados (exceto quando absolutamente necessário e / ou significativo para a semântica do aplicativo), porque os objetos têm semântica de referência, por design da linguagem. Esses idiomas geralmente têm um mecanismo de coleta de lixo baseado na acessibilidade, em vez de propriedade baseada no escopo ou contagem de referência.
Quando uma referência ou ponteiro (incluindo referência const) é passada em C ++ (ou C), o programador tem a garantia de que nenhum código especial (funções definidas pelo usuário ou funções geradas pelo compilador) será executado, além da propagação do valor do endereço (referência ou ponteiro). Essa é uma clareza de comportamento com a qual os programadores de C ++ acham confortável.
No entanto, o pano de fundo é que a linguagem C ++ é desnecessariamente complicada, de modo que essa clareza de comportamento é como um oásis (um habitat sobrevivível) em algum lugar em torno de uma zona de precipitação nuclear.
Para adicionar mais bênçãos (ou insulto), o C ++ introduz referências universais (valores-r) para facilitar os operadores de movimentação definidos pelo usuário (construtores de movimentação e operadores de atribuição de movimentação) com bom desempenho. Isso beneficia um caso de uso altamente relevante (a movimentação (transferência) de objetos de uma instância para outra), por meio da redução da necessidade de cópia e clonagem profunda. No entanto, em outras línguas, é ilógico falar dessa movimentação de objetos.
(Seção fora do tópico) Uma seção dedicada a um artigo, "Quer velocidade? Passe por valor!" escrito por volta de 2009.
Esse artigo foi escrito em 2009 e explica a justificativa de design para o valor r em C ++. Esse artigo apresenta um contra-argumento válido para minha conclusão na seção anterior. No entanto, o exemplo de código do artigo e a declaração de desempenho foram refutados há muito tempo.
Sub-TL; DR O design da semântica de valor-r em C ++ permite uma semântica surpreendentemente elegante do lado do usuário em uma
Sort
função, por exemplo. Este elegante é impossível de modelar (imitar) em outras línguas.Uma função de classificação é aplicada a toda uma estrutura de dados. Como mencionado acima, seria lento se houver muitas cópias envolvidas. Como uma otimização de desempenho (que é praticamente relevante), uma função de classificação é projetada para ser destrutiva em várias linguagens além do C ++. Destrutivo significa que a estrutura de dados de destino é modificada para atingir a meta de classificação.
No C ++, o usuário pode optar por chamar uma das duas implementações: uma destrutiva com melhor desempenho ou uma normal que não modifica a entrada. (O modelo é omitido por questões de brevidade.)
Além da classificação, essa elegância também é útil na implementação do algoritmo destrutivo de mediana de localização em uma matriz (inicialmente não classificada), por particionamento recursivo.
No entanto, observe que a maioria dos idiomas aplicaria uma abordagem de árvore de pesquisa binária equilibrada à classificação, em vez de aplicar um algoritmo destrutivo de classificação às matrizes. Portanto, a relevância prática dessa técnica não é tão alta quanto parece.
Como a otimização do compilador afeta esta resposta
Quando o inlining (e também a otimização de todo o programa / otimização do tempo do link) é aplicado em vários níveis de chamadas de função, o compilador pode ver (às vezes exaustivamente) o fluxo de dados. Quando isso acontece, o compilador pode aplicar muitas otimizações, algumas das quais podem eliminar a criação de objetos inteiros na memória. Normalmente, quando essa situação se aplica, não importa se os parâmetros são passados por valor ou por referência constante, porque o compilador pode analisar exaustivamente.
No entanto, se a função de nível inferior chamar algo que está além da análise (por exemplo, algo em uma biblioteca diferente fora da compilação ou um gráfico de chamada que seja simplesmente muito complicado), o compilador deverá otimizar defensivamente.
Objetos maiores que um valor de registro de máquina podem ser copiados por instruções explícitas de carregamento / armazenamento de memória ou por uma chamada para a
memcpy
função venerável . Em algumas plataformas, o compilador gera instruções SIMD para mover-se entre dois locais de memória, cada instrução movendo dezenas de bytes (16 ou 32).Discussão sobre a questão da verbosidade ou desordem visual
Os programadores de C ++ estão acostumados a isso, ou seja, desde que um programador não odeie C ++, a sobrecarga de escrever ou ler referências constantes no código fonte não é horrível.
As análises de custo-benefício podem ter sido feitas muitas vezes antes. Não sei se há algum científico que deva ser citado. Eu acho que a maioria das análises seria não científica ou não reproduzível.
Aqui está o que eu imagino (sem provas ou referências credíveis) ...
fonte
Isso não está aconselhando os programadores a usar as técnicas mais lentas disponíveis. Trata-se de focar na clareza ao escrever programas. Freqüentemente, clareza e eficiência são uma troca: se você deve escolher apenas um, escolha clareza. Mas se você pode conseguir os dois com facilidade, não há necessidade de prejudicar a clareza (como sinalizar que algo é uma constante) apenas para evitar a eficiência.
fonte
Passar por ([const] [rvalue] reference) | (value) deve ser sobre a intenção e as promessas feitas pela interface. Não tem nada a ver com desempenho.
A regra de ouro de Richy:
fonte
Teoricamente, a resposta deve ser sim. E, de fato, é sim algumas vezes - na verdade, passar por referência const em vez de apenas passar um valor pode ser uma pessimização, mesmo nos casos em que o valor passado é muito grande para caber em um único registrar (ou a maioria das outras pessoas que a heurística tenta usar para determinar quando passar por valor ou não). Anos atrás, David Abrahams escreveu um artigo chamado "Quer velocidade? Passe por valor!" cobrindo alguns desses casos. Não é mais fácil encontrar, mas se você puder desenterrar uma cópia, vale a pena ler (IMO).
No caso específico de passar por referência const, no entanto, eu diria que o idioma está tão bem estabelecido que a situação é mais ou menos invertida: a menos que você saiba que o tipo será
char
/short
/int
/long
, as pessoas esperam vê-lo passar por const referência por padrão, então provavelmente é melhor concordar com isso, a menos que você tenha um motivo bastante específico para fazer o contrário.fonte