Passare gli argomenti come const fa riferimento all'ottimizzazione prematura?


20

"L'ottimizzazione precoce è la radice di tutti i mali"

Penso che possiamo essere tutti d'accordo. E faccio del mio meglio per evitare di farlo.

Ma recentemente mi sono chiesto quale sia la pratica del passaggio dei parametri per const Reference anziché per Value . Mi è stato insegnato / appreso che gli argomenti di funzioni non banali (cioè la maggior parte dei tipi non primitivi) dovrebbero essere preferibilmente passati per riferimento const - parecchi libri che ho letto raccomandano questo come "best practice".

Tuttavia non posso fare a meno di chiedermi: i compilatori moderni e le nuove funzionalità del linguaggio possono fare miracoli, quindi le conoscenze che ho imparato potrebbero benissimo essere obsolete e non mi sono mai preoccupato di profilare se ci sono differenze di prestazioni tra

void fooByValue(SomeDataStruct data);   

e

void fooByReference(const SomeDataStruct& data);

La pratica che ho imparato - passando riferimenti costanti (di default per tipi non banali) - ottimizzazione prematura?


1
Vedi anche: F.call nelle Linee guida di base C ++ per una discussione di varie strategie di passaggio dei parametri.
amon,


1
@DocBrown La risposta accettata a questa domanda si riferisce al principio del minimo stupore , che può essere applicato anche qui (ovvero l'uso di riferimenti const è standard del settore, ecc. Ecc.). Detto questo, non sono d'accordo sul fatto che la domanda sia un duplicato: la domanda a cui ti riferisci chiede se è una cattiva pratica (generalmente) fare affidamento sull'ottimizzazione del compilatore. Questa domanda pone l'inverso: il passaggio dei riferimenti const è un'ottimizzazione (prematura)?
CharonX,

@CharonX: se uno può fare affidamento sull'ottimizzazione del compilatore qui, la risposta alla tua domanda è chiaramente "sì, l'ottimizzazione manuale non è necessaria, è prematura". Se non si può fare affidamento su di esso (forse perché non si sa in anticipo quali compilatori verranno mai utilizzati per il codice), la risposta è "per oggetti più grandi, probabilmente non è prematuro". Quindi, anche se queste due domande non sono letteralmente uguali, IMHO sembra essere abbastanza simile da collegarle insieme come duplicati.
Doc Brown,

1
@DocBrown: Quindi, prima di poterlo dichiarare un duplicato, indica dove nella domanda dice che il compilatore sarà autorizzato e in grado di "ottimizzarlo".
Deduplicatore

Risposte:


49

L '"ottimizzazione precoce" non riguarda l'utilizzo anticipato delle ottimizzazioni . Si tratta di ottimizzare prima che il problema sia compreso, prima di capire il runtime e spesso rendere il codice meno leggibile e meno gestibile per risultati dubbi.

L'uso di "const &" invece di passare un oggetto in base al valore è un'ottimizzazione ben compresa, con effetti ben noti sul runtime, praticamente senza sforzo e senza effetti negativi sulla leggibilità e sulla manutenibilità. In realtà migliora entrambi, perché mi dice che una chiamata non modificherà l'oggetto passato. Quindi aggiungere "const &" proprio quando si scrive il codice NON è PREMATURA.


2
Concordo sulla parte "praticamente senza sforzo" della tua risposta. Ma l'ottimizzazione prematura è innanzitutto l'ottimizzazione prima che sia un notevole impatto misurato sulle prestazioni. E non penso che la maggior parte dei programmatori C ++ (che include me stesso) effettui misurazioni prima dell'uso const&, quindi penso che la domanda sia abbastanza ragionevole.
Doc Brown,

1
Misuri prima di ottimizzare per sapere se ne valgono la pena. Con const & lo sforzo totale sta scrivendo sette caratteri e ha altri vantaggi. Quando non si intende modificare la variabile in transito, è vantaggioso anche se non c'è miglioramento della velocità.
gnasher729,

3
Non sono un esperto in C, quindi una domanda :. const& foodice che la funzione non modificherà foo, quindi il chiamante è al sicuro. Ma un valore copiato dice che nessun altro thread può cambiare foo, quindi la chiamata è sicura. Giusto? Quindi, in un'app multi-thread, la risposta dipende dalla correttezza, non dall'ottimizzazione.
user949300

1
@DocBrown potresti eventualmente cancellare la motivazione dello sviluppatore che ha messo la const &? Se lo facesse solo per le prestazioni senza considerare il resto, potrebbe essere considerato come ottimizzazione prematura. Ora, se lo mette perché sa che sarà un parametro const, allora sta solo auto-documentando il suo codice e dando l'opportunità al compilatore di ottimizzare, il che è meglio.
Walfrat,

1
@ user949300: Poche funzioni consentono ai loro argomenti di essere modificati contemporaneamente o mediante callback e lo dicono esplicitamente.
Deduplicatore

16

TL; DR: passare per riferimento const è ancora una buona idea in C ++, tutto sommato. Non un'ottimizzazione prematura.

TL; DR2: la maggior parte degli adagi non ha senso, finché non lo fanno.


Scopo

Questa risposta cerca solo di estendere un po ' l'elemento collegato nelle Linee guida di base C ++ (menzionato per la prima volta nel commento di Amon).

Questa risposta non cerca di affrontare il problema di come pensare e applicare correttamente i vari adagi che erano ampiamente diffusi all'interno dei circoli dei programmatori, in particolare il problema della riconciliazione tra conclusioni o prove contrastanti.


applicabilità

Questa risposta si applica solo alle chiamate di funzione (ambiti nidificati non rimovibili sullo stesso thread).

(Nota a margine .) Quando le cose passabili possono sfuggire all'ambito (ovvero avere una durata che potenzialmente supera l'ambito esterno), diventa più importante soddisfare la necessità dell'applicazione di una gestione della durata dell'oggetto prima di ogni altra cosa. Di solito, questo richiede l'uso di riferimenti che sono anche in grado di gestire la vita, come i puntatori intelligenti. Un'alternativa potrebbe essere l'utilizzo di un manager. Si noti che, lambda è una sorta di ambito staccabile; Le acquisizioni lambda si comportano come se avessero un oggetto. Pertanto, fai attenzione con le acquisizioni lambda. Fai anche attenzione a come passa la lambda stessa, per copia o per riferimento.


Quando passare per valore

Per valori scalari (primitive standard che rientrano in un registro macchina e hanno valore semantico) per i quali non è necessario comunicare per mutabilità (riferimento condiviso), passare per valore.

Per le situazioni in cui la chiamata richiede una clonazione di un oggetto o di un aggregato, passare per valore, in cui la copia della chiamata soddisfa la necessità di un oggetto clonato.


Quando passare per riferimento, ecc.

per tutte le altre situazioni, passare da puntatori, riferimenti, puntatori intelligenti, maniglie (vedi: linguaggio del manico), ecc. Ogni volta che si segue questo consiglio, applicare il principio di correttezza const come al solito.

Le cose (aggregati, oggetti, matrici, strutture di dati) che hanno un ingombro della memoria sufficientemente ampio dovrebbero sempre essere progettate per facilitare il riferimento pass-by, per motivi di prestazioni. Questo consiglio si applica sicuramente quando si tratta di centinaia di byte o più. Questo consiglio è limite quando è di decine di byte.


Paradigmi insoliti

Esistono paradigmi di programmazione per scopi speciali che sono ricchi di intenzioni. Ad esempio, elaborazione di stringhe, serializzazione, comunicazione di rete, isolamento, wrapping di librerie di terze parti, comunicazione tra processi di memoria condivisa, ecc. In queste aree di applicazione o paradigmi di programmazione, i dati vengono copiati da strutture a strutture, o talvolta riconfezionati in array di byte.


In che modo le specifiche della lingua influiscono su questa risposta, prima di considerare l'ottimizzazione.

Sub-TL; DR La propagazione di un riferimento non deve invocare alcun codice; il passaggio per riferimento const soddisfa questo criterio. Tuttavia, tutte le altre lingue soddisfano questo criterio senza sforzo.

(Si consiglia ai programmatori principianti C ++ di saltare completamente questa sezione.)

(L'inizio di questa sezione è in parte ispirato dalla risposta di gnasher729. Tuttavia, si raggiunge una conclusione diversa.)

C ++ consente costruttori di copie e operatori di assegnazione definiti dall'utente.

(Questa è (è stata) una scelta audace che è (è stata) sia sorprendente che deplorevole. È sicuramente una divergenza dalla norma accettabile di oggi nella progettazione del linguaggio.)

Anche se il programmatore C ++ non ne definisce uno, il compilatore C ++ deve generare tali metodi in base ai principi del linguaggio e quindi determinare se è necessario eseguire codice aggiuntivo diverso da memcpy. Ad esempio, un class/ structche contiene un std::vectormembro deve avere un costruttore di copia e un operatore di assegnazione non banali.

In altre lingue, i costruttori di copie e la clonazione di oggetti sono scoraggiati (tranne dove assolutamente necessario e / o significativo per la semantica dell'applicazione), poiché gli oggetti hanno una semantica di riferimento, in base alla progettazione del linguaggio. Queste lingue avranno in genere un meccanismo di garbage collection basato sulla raggiungibilità anziché sulla proprietà basata sull'ambito o sul conteggio dei riferimenti.

Quando un riferimento o un puntatore (incluso il riferimento const) viene passato in C ++ (o C), al programmatore viene assicurato che non verrà eseguito alcun codice speciale (funzioni definite dall'utente o generate dal compilatore), tranne la propagazione del valore dell'indirizzo (riferimento o puntatore). Questa è una chiarezza di comportamento che i programmatori C ++ trovano a loro agio.

Tuttavia, lo sfondo è che il linguaggio C ++ è inutilmente complicato, tale che questa chiarezza di comportamento è come un'oasi (un habitat sopravvissibile) da qualche parte intorno a una zona di ricaduta nucleare.

Per aggiungere più benedizioni (o insulti), C ++ introduce riferimenti universali (valori r) al fine di facilitare gli operatori di movimento definiti dall'utente (costruttori di movimenti e operatori di assegnazione di movimenti) con buone prestazioni. Ciò avvantaggia un caso d'uso estremamente rilevante (lo spostamento (trasferimento) di oggetti da un'istanza all'altra), riducendo la necessità di copia e clonazione profonda. Tuttavia, in altre lingue, è illogico parlare di tale spostamento di oggetti.


(Sezione fuori tema) Una sezione dedicata a un articolo "Vuoi velocità? Passa per valore!" scritto nel 2009 circa.

L'articolo è stato scritto nel 2009 e spiega la giustificazione del progetto per il valore r in C ++. Questo articolo presenta un valido argomento contrario alla mia conclusione nella sezione precedente. Tuttavia, l'esempio di codice dell'articolo e la dichiarazione di prestazione è stata a lungo confutata.

Sub-TL; DR Il design della semantica di valore r in C ++ consente Sort, ad esempio, una semantica sorprendentemente elegante sul lato utente di una funzione. Questo elegante è impossibile da modellare (imitare) in altre lingue.

Una funzione di ordinamento viene applicata a un'intera struttura di dati. Come accennato in precedenza, sarebbe lento se fosse coinvolta molta copia. Come ottimizzazione delle prestazioni (che è praticamente rilevante), una funzione di ordinamento è progettata per essere distruttiva in parecchi linguaggi diversi dal C ++. Distruttivo significa che la struttura dei dati di destinazione viene modificata per raggiungere l'obiettivo di ordinamento.

In C ++, l'utente può scegliere di chiamare una delle due implementazioni: una distruttiva con prestazioni migliori o una normale che non modifica l'input. (Il modello è omesso per brevità.)

/*caller specifically passes in input argument destructively*/
std::vector<T> my_sort(std::vector<T>&& input)
{
    std::vector<T> result(std::move(input)); /* destructive move */
    std::sort(result.begin(), result.end()); /* in-place sorting */
    return result; /* return-value optimization (RVO) */
}

/*caller specifically passes in read-only argument*/ 
std::vector<T> my_sort(const std::vector<T>& input)
{
    /* reuse destructive implementation by letting it work on a clone. */
    /* Several things involved; e.g. expiring temporaries as r-value */
    /* return-value optimization, etc. */
    return my_sort(std::vector<T>(input));  
}

/*caller can select which to call, by selecting r-value*/
std::vector<T> v1 = {...};
std::vector<T> v2 = my_sort(v1); /*non-destructive*/
std::vector<T> v3 = my_sort(std::move(v1)); /*v1 is gutted*/    

Oltre all'ordinamento, questa eleganza è utile anche nell'implementazione dell'algoritmo di ricerca mediana distruttiva in un array (inizialmente non ordinato), mediante partizionamento ricorsivo.

Tuttavia, si noti che la maggior parte delle lingue applica un ordinamento bilanciato dell'albero di ricerca binario all'ordinamento, invece di applicare un algoritmo di ordinamento distruttivo agli array. Pertanto, la rilevanza pratica di questa tecnica non è così elevata come sembra.


In che modo l'ottimizzazione del compilatore influisce su questa risposta

Quando l'inline (e anche l'ottimizzazione dell'intero programma / ottimizzazione del tempo di collegamento) viene applicata su più livelli di chiamate di funzione, il compilatore è in grado di vedere (a volte in modo esaustivo) il flusso di dati. Quando ciò accade, il compilatore può applicare molte ottimizzazioni, alcune delle quali possono eliminare la creazione di interi oggetti in memoria. In genere, quando si applica questa situazione, non importa se i parametri vengono passati per valore o per riferimento const, poiché il compilatore può analizzare in modo esauriente.

Tuttavia, se la funzione di livello inferiore chiama qualcosa che va oltre l'analisi (ad es. Qualcosa in una libreria diversa dalla compilation o un grafico di chiamata che è semplicemente troppo complicato), allora il compilatore deve ottimizzare in modo difensivo.

Oggetti più grandi di un valore di registro macchina potrebbero essere copiati da istruzioni esplicite di caricamento / archiviazione della memoria o da una chiamata alla memcpyfunzione venerabile . Su alcune piattaforme, il compilatore genera istruzioni SIMD per spostarsi tra due posizioni di memoria, ciascuna istruzione sposta decine di byte (16 o 32).


Discussione sulla questione della verbosità o del disordine visivo

I programmatori C ++ sono abituati a questo, cioè finché un programmatore non odia il C ++, il sovraccarico di scrivere o leggere il riferimento const nel codice sorgente non è orribile.

Le analisi costi-benefici potrebbero essere state fatte molte volte in precedenza. Non so se ce ne siano di quelli scientifici che dovrebbero essere citati. Immagino che la maggior parte delle analisi non siano scientifiche o non riproducibili.

Ecco cosa immagino (senza prove o riferimenti credibili) ...

  • Sì, influisce sulle prestazioni del software scritto in questa lingua.
  • Se i compilatori sono in grado di comprendere lo scopo del codice, potrebbe essere potenzialmente abbastanza intelligente da automatizzarlo
  • Sfortunatamente, nei linguaggi che favoriscono la mutabilità (rispetto alla purezza funzionale), il compilatore classificherebbe la maggior parte delle cose come mutate, quindi la deduzione automatizzata della costanza rifiuterebbe la maggior parte delle cose come non costanti
  • Il sovraccarico mentale dipende dalle persone; le persone che ritengono che ciò sia un alto sovraccarico mentale avrebbero rifiutato il C ++ come linguaggio di programmazione praticabile.

Questa è una delle situazioni in cui vorrei poter accettare due risposte invece di dover scegliere solo una ... sigh
CharonX

8

Nel documento di DonaldKnuth "StructuredProgrammingWithGoToStatements", ha scritto: "I programmatori sprecano enormi quantità di tempo pensando o preoccupandosi della velocità delle parti non critiche dei loro programmi e questi tentativi di efficienza hanno effettivamente un forte impatto negativo quando si considerano il debug e la manutenzione Dovremmo dimenticare le piccole efficienze, diciamo circa il 97% delle volte: l'ottimizzazione prematura è la radice di tutti i mali. Eppure non dovremmo rinunciare alle nostre opportunità in quel 3% critico. " - Ottimizzazione precoce

Ciò non consiglia ai programmatori di utilizzare le tecniche più lente disponibili. Si tratta di concentrarsi sulla chiarezza durante la scrittura di programmi. Spesso chiarezza ed efficienza sono un compromesso: se devi sceglierne solo una, scegli chiarezza. Ma se riesci a raggiungere entrambi facilmente, non è necessario paralizzare la chiarezza (come segnalare che qualcosa è una costante) solo per evitare l'efficienza.


3
"se devi sceglierne solo uno, scegli la chiarezza." Il secondo dovrebbe essere preferito invece, poiché potresti essere costretto a scegliere l'altro.
Deduplicatore

@Deduplicator Grazie. Nel contesto del PO, tuttavia, il programmatore ha la libertà di scegliere.
Lawrence,

La tua risposta è un po 'più generale di quella però ...
Deduplicatore,

@Deduplicator Ah, ma il contesto della mia risposta è (anche) scelto dal programmatore . Se la scelta fosse stata forzata sul programmatore, non saresti stato "tu" a scegliere :). Ho preso in considerazione la modifica che hai suggerito e non mi opporterei alla modifica della mia risposta di conseguenza, ma preferisco la formulazione esistente per la sua chiarezza.
Lawrence,

7

Passare per ([const] [rvalue] reference) | (valore) dovrebbe riguardare l'intento e le promesse fatte dall'interfaccia. Non ha nulla a che fare con le prestazioni.

Regola empirica di Richy:

void foo(X x);          // I intend to own the x you gave me, whether by copy, move or direct initialisation on the call stack.     

void foo(X&& x);        // I intend to steal x from you. Do not use it other than to re-assign to it after calling me.

void foo(X const& x);   // I guarantee not to change your x

void foo(X& x);         // I may modify your x and I will leave it in a defined state

3

Teoricamente, la risposta dovrebbe essere sì. E, in effetti, è sì qualche volta - in realtà, passare per riferimento const invece di passare semplicemente un valore può essere una pessimizzazione, anche nei casi in cui il valore passato è troppo grande per rientrare in un singolo registro (o la maggior parte delle altre euristiche che le persone cercano di usare per determinare quando passare per valore o meno). Anni fa, David Abrahams ha scritto un articolo intitolato "Vuoi velocità? Passa per valore!" coprendo alcuni di questi casi. Non è più facile da trovare, ma se riesci a scavare una copia vale la pena leggere (IMO).

Nel caso specifico del passaggio per riferimento const, tuttavia, direi che il linguaggio è così ben definito che la situazione è più o meno invertita: a meno che tu non sappia che il tipo sarà char/ short/ int/ long, le persone si aspettano di vederlo passare per const riferimento per impostazione predefinita, quindi probabilmente è meglio andare avanti con quello a meno che tu non abbia un motivo abbastanza specifico per fare diversamente.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.