È una cattiva pratica scrivere codice che si basa sulle ottimizzazioni del compilatore?


99

Ho imparato un po 'di C ++ e spesso devo restituire oggetti di grandi dimensioni da funzioni create all'interno della funzione. So che c'è il passaggio per riferimento, restituisce un puntatore e restituisce soluzioni di un tipo di riferimento, ma ho anche letto che i compilatori C ++ (e lo standard C ++) consentono l'ottimizzazione del valore di ritorno, che evita di copiare questi oggetti di grandi dimensioni attraverso la memoria, quindi risparmiando tempo e memoria di tutto ciò.

Ora, ritengo che la sintassi sia molto più chiara quando l'oggetto viene esplicitamente restituito in base al valore e il compilatore impiegherà generalmente l'RVO e renderà il processo più efficiente. È una cattiva pratica fare affidamento su questa ottimizzazione? Rende il codice più chiaro e più leggibile per l'utente, il che è estremamente importante, ma dovrei essere prudente nel ritenere che il compilatore colga l'opportunità RVO?

Si tratta di una micro-ottimizzazione o qualcosa che dovrei tenere a mente durante la progettazione del mio codice?


7
Per rispondere alla tua modifica, si tratta di una micro ottimizzazione perché anche se provassi a confrontare ciò che guadagni in nanosecondi, lo vedresti a malapena. Per il resto, sono troppo marcio in C ++ per fornirti una risposta rigorosa sul perché non funzionerebbe. Uno di questi è probabile che ci siano casi in cui è necessaria un'allocazione dinamica e quindi utilizzare nuovi / puntatori / riferimenti.
Walfrat,

4
@Walfrat anche se gli oggetti sono abbastanza grandi, nell'ordine dei megabyte? Le mie matrici possono diventare enormi a causa della natura dei problemi che sto risolvendo.
Matt,

6
@ Matt non lo farei. Riferimenti / puntatori esistono proprio per questo. Le ottimizzazioni del compilatore dovrebbero essere al di là di ciò che i programmatori dovrebbero prendere in considerazione quando costruiscono un programma, anche se sì, spesso i due mondi si sovrappongono.
Neil,

5
@Matt A meno che tu non stia facendo qualcosa di estremamente specifico che suppone di richiedere agli sviluppatori un'esperienza 10+ ish in kernel C, le interazioni hardware basse non dovrebbero servirti. Se pensi di appartenere a qualcosa di molto specifico, modifica il tuo post e aggiungi una descrizione accurata di ciò che la tua applicazione dovrebbe fare (in tempo reale? Calcolo matematico pesante? ...)
Walfrat

37
Nel caso particolare del C ++ (N) RVO, sì, basarsi su questa ottimizzazione è perfettamente valido. Questo perché lo standard C ++ 17 impone espressamente che ciò accada, nelle situazioni in cui i compilatori moderni lo stavano già facendo.
Caleth,

Risposte:


130

Impiega il principio del minimo stupore .

Sei tu e solo tu che userai questo codice, e sei sicuro che lo stesso tu in 3 anni non sarai sorpreso da quello che fai?

Quindi vai avanti.

In tutti gli altri casi, utilizzare il modo standard; altrimenti, tu e i tuoi colleghi vi imbatterete in bug difficili da trovare.

Ad esempio, il mio collega si lamentava del fatto che il mio codice causasse errori. Si è scoperto, aveva disattivato la valutazione booleana di corto circuito nelle impostazioni del suo compilatore. L'ho quasi schiaffeggiato.


88
@Neil, questo è il mio punto, tutti si affidano alla valutazione del corto circuito. E non dovresti pensarci due volte, dovrebbe essere acceso. È uno standard defacto. Sì, puoi cambiarlo, ma non dovresti.
Pieter B,

49
"Ho cambiato il modo in cui funziona la lingua e il tuo sporco codice marcio si è rotto! Arghh!" Wow. Schiaffeggiare sarebbe appropriato, invia il tuo collega all'allenamento Zen, ce n'è molto lì.

109
@PieterB Sono abbastanza sicuro che le specifiche del linguaggio C e C ++ garantiscono una valutazione del corto circuito. Quindi non è solo uno standard di fatto, è lo standard. Senza di essa, non stai nemmeno più usando C / C ++, ma qualcosa di sospettosamente simile: P
marcelm,

47
Solo per riferimento, il modo standard qui è di ritornare per valore.
DeadMG

28
@ dan04 sì, era a Delfi. Ragazzi, non fatevi sorprendere nell'esempio che riguarda il punto che ho sollevato. Non fare cose sorprendenti che nessun altro fa.
Pieter B,

81

Per questo caso particolare, sicuramente torna semplicemente per valore.

  • RVO e NRVO sono ottimizzazioni ben note e solide che dovrebbero davvero essere fatte da qualsiasi compilatore decente, anche in modalità C ++ 03.

  • La semantica di spostamento assicura che gli oggetti vengano spostati dalle funzioni se (N) RVO non ha luogo. È utile solo se il tuo oggetto utilizza internamente dati dinamici (come std::vectorfa), ma dovrebbe essere così se è così grande: traboccare lo stack è un rischio con grandi oggetti automatici.

  • C ++ 17 applica RVO. Quindi non preoccuparti, non sparirà su di te e finirà per affermarsi completamente una volta che i compilatori sono aggiornati.

E alla fine, forzare un'allocazione dinamica aggiuntiva per restituire un puntatore o forzare il tipo di risultato in modo che sia predefinito costruibile solo in modo da poterlo passare poiché un parametro di output sono entrambe soluzioni brutte e non idiomatiche a un problema che probabilmente non riuscirai mai avere.

Basta scrivere codice che abbia un senso e ringraziare gli autori del compilatore per aver ottimizzato correttamente il codice che ha un senso.


9
Solo per divertimento, vedi come Borland Turbo C ++ 3.0 del 1990 gestisce l'RVO . Spoiler: fondamentalmente funziona bene.
nwp,

9
La chiave qui non è l'ottimizzazione casuale specifica del compilatore o una "caratteristica non documentata", ma qualcosa che, sebbene tecnicamente facoltativo in diverse versioni dello standard C ++, è stato fortemente spinto dall'industria e praticamente tutti i principali compilatori lo hanno fatto per un tempo molto lungo.

7
Questa ottimizzazione non è abbastanza robusta come si potrebbe desiderare. Sì, è piuttosto affidabile nei casi più ovvi, ma cercando ad esempio il bugzilla di gcc, ci sono molti casi a malapena meno ovvi in ​​cui è mancato.
Marc Glisse,

62

Ora, ritengo che la sintassi sia molto più chiara quando l'oggetto viene esplicitamente restituito in base al valore e il compilatore impiegherà generalmente l'RVO e renderà il processo più efficiente. È una cattiva pratica fare affidamento su questa ottimizzazione? Rende il codice più chiaro e più leggibile per l'utente, il che è estremamente importante, ma dovrei essere prudente nel ritenere che il compilatore colga l'opportunità RVO?

Questa non è una micro ottimizzazione poco conosciuta, carina, di cui leggi in qualche piccolo blog poco trafficato e poi ti senti intelligente e superiore sull'uso.

Dopo C ++ 11, RVO è il modo standard per scrivere questo codice di codice. È comune, previsto, insegnato, menzionato nei colloqui, menzionato nei blog, menzionato nello standard, verrà segnalato come bug del compilatore se non implementato. In C ++ 17, il linguaggio fa un ulteriore passo avanti e obbliga a copiare le elezioni in determinati scenari.

Dovresti assolutamente fare affidamento su questa ottimizzazione.

Inoltre, il ritorno per valore porta semplicemente a un codice enormemente più facile da leggere e gestire rispetto al codice restituito per riferimento. La semantica del valore è una cosa potente, che potrebbe portare a maggiori opportunità di ottimizzazione.


3
Grazie, questo ha molto senso ed è coerente con il "principio del minimo stupore" di cui sopra. Renderebbe il codice molto chiaro e comprensibile e renderebbe più difficile confondere gli shenanigans con i puntatori.
Matt,

3
@Matt Parte del motivo per cui ho votato a favore di questa risposta è che menziona "valore semantico". Man mano che acquisisci maggiore esperienza in C ++ (e nella programmazione in generale), troverai situazioni occasionali in cui la semantica del valore non può essere utilizzata per determinati oggetti perché sono mutabili e le loro modifiche devono essere rese visibili ad altro codice che utilizza lo stesso oggetto (un esempio di "mutabilità condivisa"). Quando si verificano queste situazioni, gli oggetti interessati dovranno essere condivisi tramite puntatori (intelligenti).
rwong,

16

La correttezza del codice che scrivi non dovrebbe mai dipendere da un'ottimizzazione. Dovrebbe produrre il risultato corretto quando eseguito sulla "macchina virtuale" C ++ che usano nelle specifiche.

Tuttavia, ciò di cui parli è più una questione di efficienza. Il tuo codice funziona meglio se ottimizzato con un compilatore di ottimizzazione RVO. Va bene, per tutti i motivi indicati nelle altre risposte.

Tuttavia, se hai bisogno di questa ottimizzazione (come se il costruttore della copia causasse effettivamente il fallimento del codice), ora sei ai capricci del compilatore.

Penso che il miglior esempio di questo nella mia pratica sia l'ottimizzazione delle chiamate di coda:

   int sillyAdd(int a, int b)
   {
      if (b == 0)
          return a;
      return sillyAdd(a + 1, b - 1);
   }

È un esempio sciocco, ma mostra una coda, in cui una funzione viene chiamata ricorsivamente proprio alla fine di una funzione. La macchina virtuale C ++ mostrerà che questo codice funziona correttamente, anche se potrei causare un po 'di confusione sul motivo per cui mi sono preso il disturbo di scrivere una routine di aggiunta in primo luogo. Tuttavia, nelle implementazioni pratiche di C ++, abbiamo uno stack e ha uno spazio limitato. Se eseguita pedanticamente, questa funzione dovrebbe spingere almeno i b + 1frame dello stack nello stack mentre fa la sua aggiunta. Se voglio calcolare sillyAdd(5, 7), questo non è un grosso problema. Se voglio calcolare sillyAdd(0, 1000000000), potrei essere davvero nei guai a causare uno StackOverflow (e non il tipo buono ).

Tuttavia, possiamo vedere che una volta raggiunta l'ultima linea di ritorno, abbiamo davvero finito con tutto nel frame dello stack corrente. Non abbiamo davvero bisogno di tenerlo in giro. L'ottimizzazione delle chiamate di coda consente di "riutilizzare" il frame dello stack esistente per la funzione successiva. In questo modo, abbiamo bisogno solo di 1 frame dello stack, anziché b+1. (Dobbiamo ancora fare tutte quelle stupide aggiunte e sottrazioni, ma non occupano più spazio.) In effetti, l'ottimizzazione trasforma il codice in:

   int sillyAdd(int a, int b)
   {
      begin:
      if (b == 0)
          return a;
      // return sillyAdd(a + 1, b - 1);
      a = a + 1;
      b = b - 1;
      goto begin;  
   }

In alcune lingue, l'ottimizzazione delle chiamate di coda è esplicitamente richiesta dalla specifica. C ++ non è uno di quelli. Non posso fare affidamento sui compilatori C ++ per riconoscere questa opportunità di ottimizzazione delle chiamate di coda, a meno che non vada caso per caso. Con la mia versione di Visual Studio, la versione di rilascio esegue l'ottimizzazione delle chiamate di coda, ma la versione di debug no (in base alla progettazione).

Quindi sarebbe male per me dipendere dalla capacità di calcolare sillyAdd(0, 1000000000).


2
Questo è un caso interessante, ma non credo che tu possa generalizzarlo alla regola nel tuo primo paragrafo. Supponiamo di avere un programma per un dispositivo di piccole dimensioni, che verrà caricato se e solo se utilizzo le ottimizzazioni per la riduzione delle dimensioni del compilatore: è sbagliato farlo? sembra piuttosto pedante dire che la mia unica scelta valida è quella di riscriverla in assembler, specialmente se quella riscrittura fa le stesse cose dell'ottimizzatore per risolvere il problema.
sdenham,

5
@sdenham Suppongo che ci sia una piccola stanza nella discussione. Se non stai più scrivendo per "C ++", ma piuttosto per "compilatore WindRiver C ++ versione 3.4.1", allora posso vedere la logica lì. Tuttavia, come regola generale, se stai scrivendo qualcosa che non funziona correttamente secondo le specifiche, ti trovi in ​​uno scenario molto diverso. So che la libreria Boost ha un codice del genere, ma lo mettono sempre in #ifdefblocchi e hanno una soluzione conforme agli standard disponibile.
Cort Ammon,

4
è un errore di battitura nel secondo blocco di codice in cui si dice b = b + 1?
Stib

2
Potresti voler spiegare cosa intendi per "macchina virtuale C ++", poiché non è un termine usato in nessun documento standard. Penso che tu stia parlando del modello di esecuzione del C ++, ma non del tutto certo - e il tuo termine è ingannevolmente simile a una "macchina virtuale bytecode" che si riferisce a qualcosa di completamente diverso.
Toby Speight,

1
@supercat Scala ha anche una sintassi esplicita di ricorsione della coda. Il C ++ è la sua bestia, ma penso che la ricorsione della coda sia unidiomatica per i linguaggi non funzionali e obbligatoria per i linguaggi funzionali, lasciando un piccolo set di lingue dove è ragionevole avere una sintassi esplicita di ricorsione della coda. Tradurre letteralmente la ricorsione della coda in loop e la mutazione esplicita è semplicemente un'opzione migliore per molte lingue.
prosfilaes,

8

In pratica, i programmi C ++ prevedono alcune ottimizzazioni del compilatore.

Guarda in particolare le intestazioni standard delle implementazioni dei container standard . Con GCC , è possibile richiedere il modulo preelaborato ( g++ -C -E) e la rappresentazione interna GIMPLE ( g++ -fdump-tree-gimpleo Gimple SSA con -fdump-tree-ssa) della maggior parte dei file di origine (unità di traduzione tecnicamente) utilizzando i contenitori. Sarai sorpreso dalla quantità di ottimizzazione che viene eseguita (con g++ -O2). Quindi gli implementatori di container si basano sulle ottimizzazioni (e la maggior parte delle volte, l'implementatore di una libreria standard C ++ sa quale ottimizzazione sarebbe accaduta e avrebbe scritto l'implementazione del container tenendo presente quelli; a volte scriveva anche il passaggio di ottimizzazione nel compilatore per gestire le funzionalità richieste dalla libreria C ++ standard).

In pratica, sono le ottimizzazioni del compilatore che rendono abbastanza efficienti C ++ e i suoi contenitori standard. Quindi puoi contare su di loro.

E anche per il caso RVO menzionato nella tua domanda.

Lo standard C ++ è stato progettato congiuntamente (in particolare sperimentando ottimizzazioni abbastanza buone e proponendo nuove funzionalità) per funzionare bene con le possibili ottimizzazioni.

Ad esempio, considera il seguente programma:

#include <algorithm>
#include <vector>

extern "C" bool all_positive(const std::vector<int>& v) {
  return std::all_of(v.begin(), v.end(), [](int x){return x >0;});
}

compilalo con g++ -O3 -fverbose-asm -S. Scoprirai che la funzione generata non esegue alcuna CALListruzione macchina. Quindi la maggior parte dei passaggi in C ++ (costruzione di una chiusura lambda, sua applicazione ripetuta, ottenimento di begine enditeratori, ecc ...) sono stati ottimizzati. Il codice macchina contiene solo un ciclo (che non appare esplicitamente nel codice sorgente). Senza tali ottimizzazioni, C ++ 11 non avrà successo.

addenda

(dicembre aggiunto 31 st 2017)

Vedi CppCon 2017: Matt Godbolt “Cosa ha fatto di recente il mio compilatore per me? Discorso sul coperchio del compilatore ” .


4

Ogni volta che usi un compilatore, la comprensione è che produrrà codice macchina o byte per te. Non garantisce nulla su come sia quel codice generato, tranne che implementerà il codice sorgente in base alle specifiche della lingua. Si noti che questa garanzia è la stessa indipendentemente dal livello di ottimizzazione utilizzato e quindi, in generale, non vi è motivo di considerare un output più "giusto" dell'altro.

Inoltre, in quei casi, come RVO, dove è specificato nella lingua, sembrerebbe inutile fare di tutto per evitare di usarlo, soprattutto se semplifica il codice sorgente.

Viene fatto molto sforzo per fare in modo che i compilatori producano un output efficiente e chiaramente l'intenzione è quella di usare quelle capacità.

Potrebbero esserci motivi per utilizzare codice non ottimizzato (ad esempio per il debug), ma il caso menzionato in questa domanda non sembra essere uno (e se il tuo codice fallisce solo quando ottimizzato, e non è una conseguenza di qualche peculiarità del dispositivo su cui lo stai eseguendo, quindi c'è un bug da qualche parte ed è improbabile che sia nel compilatore.)


3

Penso che altri abbiano coperto bene l'angolo specifico di C ++ e RVO. Ecco una risposta più generale:

Quando si tratta di correttezza, non si dovrebbe fare affidamento sulle ottimizzazioni del compilatore o sul comportamento specifico del compilatore in generale. Fortunatamente, sembra che tu non stia facendo questo.

Quando si tratta di prestazioni, è necessario fare affidamento sul comportamento specifico del compilatore in generale e sulle ottimizzazioni del compilatore in particolare. Un compilatore conforme allo standard è libero di compilare il codice nel modo che desidera, purché il codice compilato si comporti in base alle specifiche del linguaggio. E non sono a conoscenza di alcuna specifica per un linguaggio tradizionale che specifica la velocità di ciascuna operazione.


1

Le ottimizzazioni del compilatore dovrebbero influire solo sulle prestazioni, non sui risultati. Affidarsi alle ottimizzazioni del compilatore per soddisfare i requisiti non funzionali non è solo ragionevole, ma è spesso il motivo per cui un compilatore viene scelto su un altro.

I flag che determinano il modo in cui vengono eseguite determinate operazioni (ad esempio le condizioni di indice o di overflow) sono spesso raggruppati con ottimizzazioni del compilatore, ma non dovrebbero esserlo. Effettuano esplicitamente i risultati dei calcoli.

Se un'ottimizzazione del compilatore provoca risultati diversi, si tratta di un bug, un bug nel compilatore. Affidarsi a un bug nel compilatore è a lungo termine un errore: cosa succede quando viene corretto?

L'uso di flag del compilatore che cambiano il modo in cui funzionano i calcoli dovrebbe essere ben documentato, ma usato secondo necessità.


Sfortunatamente, molta documentazione del compilatore fa un cattivo lavoro nel specificare ciò che è o non è garantito in varie modalità. Inoltre, gli autori di compilatori "moderni" sembrano ignari delle combinazioni di garanzie che i programmatori fanno e non hanno bisogno. Se un programma funzionerebbe bene se x*y>zrestituisce arbitrariamente 0 o 1 in caso di overflow, a condizione che non abbia altri effetti collaterali , è necessario che un programmatore debba o evitare traboccamenti a tutti i costi o forzare il compilatore a valutare l'espressione in un modo particolare inutili ottimizzazioni per
svantaggio

... il compilatore potrebbe comportarsi a suo piacimento come se x*ypromuovesse i suoi operandi a un tipo arbitrario più lungo (consentendo così forme di sollevamento e riduzione della forza che avrebbero cambiato il comportamento di alcuni casi di overflow). Molti compilatori, tuttavia, richiedono che i programmatori prevengano l'overflow a tutti i costi o forzino i compilatori a troncare tutti i valori intermedi in caso di overflow.
supercat,

1

No.

Questo è quello che faccio sempre. Se devo accedere a un blocco arbitrario a 16 bit in memoria, lo faccio

void *ptr = get_pointer();
uint16_t u16;
memcpy(&u16, ptr, sizeof(u16)); // ntohs omitted for simplicity

... e fare affidamento sul compilatore che fa tutto il possibile per ottimizzare quel pezzo di codice. Il codice funziona su ARM, i386, AMD64 e praticamente su ogni singola architettura in circolazione. In teoria, un compilatore non ottimizzante potrebbe effettivamente chiamare memcpy, risultando in prestazioni totalmente scadenti, ma questo non è un problema per me, poiché utilizzo le ottimizzazioni del compilatore.

Considera l'alternativa:

void *ptr = get_pointer();
uint16_t *u16ptr = ptr;
uint16_t u16;
u16 = *u16ptr;  // ntohs omitted for simplicity

Questo codice alternativo non funziona su macchine che richiedono un corretto allineamento, se get_pointer()restituisce un puntatore non allineato. Inoltre, in alternativa potrebbero esserci problemi di aliasing.

La differenza tra -O2 e -O0 quando si utilizza il memcpytrucco è notevole: 3,2 Gbps di prestazioni di checksum IP rispetto a 67 Gbps di prestazioni di checksum IP. Oltre un ordine di differenza di grandezza!

A volte potrebbe essere necessario aiutare il compilatore. Quindi, ad esempio, invece di fare affidamento sul compilatore per srotolare i loop, puoi farlo da solo. O implementando il famoso dispositivo Duff o in un modo più pulito.

Lo svantaggio di fare affidamento sulle ottimizzazioni del compilatore è che se si esegue gdb per eseguire il debug del codice, è possibile scoprire che molte cose sono state ottimizzate. Quindi, potrebbe essere necessario ricompilare con -O0, il che significa che le prestazioni risulteranno totalmente durante il debug. Penso che questo sia uno svantaggio che vale la pena prendere, considerando i vantaggi dell'ottimizzazione dei compilatori.

Qualunque cosa tu faccia, assicurati che la tua strada non sia in realtà un comportamento indefinito. Certamente l'accesso a qualche blocco casuale di memoria come numero intero a 16 bit è un comportamento indefinito a causa di problemi di alias e allineamento.


0

Tutti i tentativi di un codice efficiente scritto in tutto tranne che nell'assembly si basano molto, molto pesantemente sull'ottimizzazione del compilatore, a partire dall'allocazione del registro più basilare ed efficiente per evitare fuoriuscite di stack superflue in tutto il luogo e almeno ragionevolmente buona, se non eccellente, selezione delle istruzioni. Altrimenti saremmo tornati agli anni '80 dove dovevamo mettere registersuggerimenti in tutto il luogo e usare il numero minimo di variabili in una funzione per aiutare i compilatori C arcaici o anche prima quando gotoera un'utile ottimizzazione delle ramificazioni.

Se non avessimo la sensazione di poter fare affidamento sulla capacità del nostro ottimizzatore di ottimizzare il nostro codice, dovremmo comunque codificare i percorsi di esecuzione critici per le prestazioni in assembly.

È davvero una questione di quanto ritieni affidabile che possa essere fatta l'ottimizzazione che è meglio risolta profilando e esaminando le capacità dei compilatori che hai e possibilmente anche smontando se c'è un hotspot che non riesci a capire dove sembra il compilatore non sono riusciti a fare un'ovvia ottimizzazione.

RVO è qualcosa che esiste da secoli e, almeno escludendo casi molto complessi, è qualcosa che i compilatori si stanno applicando in modo affidabile per anni. Sicuramente non vale la pena aggirare un problema che non esiste.

Err dalla parte del fare affidamento sull'ottimizzatore, non temendolo

Al contrario, direi err dal lato di fare troppo affidamento sulle ottimizzazioni del compilatore piuttosto che troppo poco, e questo suggerimento viene da un ragazzo che lavora in campi molto critici in termini di prestazioni in cui l'efficienza, la manutenibilità e la qualità percepita tra i clienti sono tutta una sfocatura gigante. Preferirei che ti affidassi troppo con confidenza al tuo ottimizzatore e trovassi alcuni casi oscuri in cui hai fatto troppo affidamento piuttosto che affidarti troppo poco e solo a codificare continuamente paure superstiziose per il resto della tua vita. Questo almeno ti farà raggiungere un profiler e indagare correttamente se le cose non si eseguono rapidamente come dovrebbero e acquisire preziose conoscenze, non superstizioni, lungo la strada.

Stai facendo bene ad appoggiarti all'ottimizzatore. Continuate così. Non diventare come quel ragazzo che inizia esplicitamente a richiedere di incorporare ogni funzione chiamata in un ciclo prima ancora di profilare da una paura fuorviante delle carenze dell'ottimizzatore.

profiling

La profilazione è davvero la rotonda ma la risposta definitiva alla tua domanda. Il problema che i principianti desiderosi di scrivere codice efficiente con cui spesso lottano non è cosa ottimizzare, ma cosa non ottimizzare perché sviluppano tutti i tipi di intuizioni sbagliate sulle inefficienze che, sebbene umanamente intuitive, sono errate dal punto di vista computazionale. Lo sviluppo di esperienza con un profiler inizierà davvero a darti un giusto apprezzamento non solo delle capacità di ottimizzazione dei compilatori su cui puoi affidarti con fiducia, ma anche delle capacità (oltre alle limitazioni) del tuo hardware. C'è probabilmente ancora più valore nella profilazione nell'apprendimento di ciò che non valeva la pena ottimizzare rispetto all'apprendimento di ciò che era.


-1

Il software può essere scritto in C ++ su piattaforme molto diverse e per molti scopi diversi.

Dipende completamente dallo scopo del software. Dovrebbe essere facile da mantenere, espandere, rattoppare, refactor ecc. o sono altre cose più importanti, come prestazioni, costi o compatibilità con alcuni hardware specifici o il tempo necessario per lo sviluppo.


-2

Penso che la noiosa risposta sia: "dipende".

È una cattiva pratica scrivere codice che si basa su un'ottimizzazione del compilatore che è probabile che sia disattivata e in cui la vulnerabilità non è documentata e in cui il codice in questione non è testato dall'unità in modo che se si rompe lo sapresti ? Probabilmente.

È una cattiva pratica scrivere codice che si basa su un'ottimizzazione del compilatore che non è probabile che sia disattivata , che sia documentata e sia testata dall'unità ? Forse no.


-6

A meno che non ci sia altro che non ci stai dicendo, questa è una cattiva pratica, ma non per il motivo che suggerisci.

Forse diversamente dalle altre lingue che hai usato prima, restituendo il valore di un oggetto in C ++ si ottiene una copia dell'oggetto. Se poi modifichi l'oggetto, stai modificando un altro oggetto . Cioè, se ho Obj a; a.x=1;e Obj b = a;, allora lo faccio b.x += 2; b.f();, allora è a.xuguale a 1, non a 3.

Quindi no, l'uso di un oggetto come valore anziché come riferimento o puntatore non fornisce la stessa funzionalità e potresti riscontrare bug nel tuo software.

Forse lo sai e non influisce negativamente sul tuo caso d'uso specifico. Tuttavia, in base al testo della tua domanda, sembra che potresti non essere consapevole della distinzione; parole come "creare un oggetto nella funzione".

"Crea un oggetto nella funzione" suona come new Obj;dove "restituisce l'oggetto per valore" suona comeObj a; return a;

Obj a;e Obj* a = new Obj;sono cose molto, molto diverse; il primo può provocare il danneggiamento della memoria se non utilizzato e compreso correttamente, mentre il secondo può causare perdite di memoria se non utilizzato e compreso correttamente.


8
L'ottimizzazione del valore di ritorno (RVO) è una semantica ben definita in cui il compilatore costruisce un oggetto restituito a un livello superiore sul frame dello stack, evitando in particolare copie di oggetti non necessarie. Si tratta di un comportamento ben definito che è stato supportato molto prima che fosse richiesto in C ++ 17. Anche 10-15 anni fa, tutti i principali compilatori hanno supportato questa funzione e lo hanno fatto in modo coerente.

@Snowman Non sto parlando della gestione della memoria fisica a basso livello e non ho discusso di gonfiore o velocità della memoria. Come ho mostrato specificamente nella mia risposta, sto parlando dei dati logici. Logicamente , fornire il valore di un oggetto ne sta creando una copia, indipendentemente da come viene implementato il compilatore o quale assembly viene utilizzato dietro le quinte. Le cose di basso livello dietro le quinte sono una cosa, e la struttura logica e il comportamento del linguaggio sono un'altra; sono collegati, ma non sono la stessa cosa - entrambi dovrebbero essere compresi.
Aaron,

6
la tua risposta dice "restituendo il valore di un oggetto in C ++ si ottiene una copia dell'oggetto" che è completamente falso nel contesto di RVO - l'oggetto viene costruito direttamente nella posizione chiamante e non viene mai fatta alcuna copia. È possibile verificarlo eliminando il costruttore di copie e restituendo l'oggetto che viene costruito returnnell'istruzione che è un requisito per RVO. Inoltre, vai avanti a parlare di parole chiave newe puntatori, che non è ciò di cui parla RVO. Credo che o non capisca la domanda, o RVO, o forse entrambi.

-7

Pieter B ha assolutamente ragione nel raccomandare il minimo stupore.

Per rispondere alla tua domanda specifica, ciò che questo (molto probabilmente) significa in C ++ è che dovresti restituire un std::unique_ptroggetto costruito.

Il motivo è che questo è più chiaro per uno sviluppatore C ++ su ciò che sta succedendo.

Sebbene il tuo approccio molto probabilmente funzionerebbe, stai effettivamente segnalando che l'oggetto è un tipo di piccolo valore quando, in realtà, non lo è. Inoltre, stai gettando via ogni possibilità di astrazione dell'interfaccia. Questo può essere OK per i tuoi scopi attuali ma è spesso molto utile quando hai a che fare con le matrici.

Apprezzo che se vieni da altre lingue, inizialmente tutti i sigilli possono essere fonte di confusione. Ma fai attenzione a non dare per scontato che, non utilizzandole, rendi il tuo codice più chiaro. In pratica, è probabile che sia vero il contrario.


Paese che vai, usanze che trovi.

14
Questa non è una buona risposta per i tipi che non eseguono loro stessi allocazioni dinamiche. Il fatto che l'OP ritenga che la cosa naturale nel suo caso d'uso sia restituire in base al valore indica che i suoi oggetti hanno una durata di memorizzazione automatica sul lato chiamante. Per oggetti semplici, non troppo grandi, anche un'implementazione ingenua del valore di ritorno della copia sarà ordini di grandezza più veloci di un'allocazione dinamica. (Se, d'altra parte, la funzione restituisce un contenitore, la restituzione di un parametro_intero può anche essere vantaggiosa rispetto a un ingenuo ritorno del compilatore in base al valore.)
Peter A. Schneider,

9
@Matt Nel caso in cui non ti rendessi conto che questa non è la migliore pratica. Fare inutilmente allocazioni di memoria e forzare la semantica del puntatore sugli utenti è male.
nwp,

5
Prima di tutto, quando si utilizzano i puntatori intelligenti, si dovrebbe restituire std::make_unique, non std::unique_ptrdirettamente. In secondo luogo, RVO non è un'ottimizzazione esoterica specifica del fornitore: è integrata nello standard. Anche quando non lo era, era ampiamente supportato e si aspettava un comportamento. Non ha senso restituire a std::unique_ptrquando in primo luogo non è necessario un puntatore.

4
@Snowman: non esiste "quando non lo era". Sebbene solo di recente sia diventato obbligatorio , ogni standard C ++ ha mai riconosciuto [N] RVO e ha creato alloggi per abilitarlo (ad esempio, al compilatore è sempre stata concessa l'autorizzazione esplicita per omettere l'uso del costruttore di copie sul valore restituito, anche se ha effetti collaterali visibili).
Jerry Coffin,
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.