___ qstnhdr ___ È una cattiva pratica scrivere codice che si basa sulle ottimizzazioni del compilatore? ______ qstntxt ___
Ho imparato alcuni C ++ e spesso devo restituire oggetti di grandi dimensioni dalle funzioni create all'interno della funzione. So che c'è il passaggio per riferimento, restituire un puntatore e restituire un tipo di soluzioni di riferimento, ma ho anche letto che i compilatori C ++ (e lo standard C ++) consentono l'ottimizzazione del valore di ritorno, che evita la copia di questi oggetti di grandi dimensioni attraverso la memoria, salvare il tempo e la memoria di tutto ciò.
Ora, ritengo che la sintassi sia molto più chiara quando l'oggetto viene esplicitamente restituito dal valore e il compilatore generalmente impiegherà il RVO e renderà il processo più efficiente. È una cattiva pratica affidarsi a questa ottimizzazione? Rende il codice più chiaro e più leggibile per l'utente, il che è estremamente importante, ma dovrei essere cauto nell'assumere che il compilatore catturerà l'opportunità RVO?
Questa è una micro-ottimizzazione, o qualcosa che dovrei tenere a mente durante la progettazione del mio codice?
______ azszpr359030 ___
Impiega il principio del minimo stupore .
Sei tu e solo tu che usi questo codice e sei sicuro che lo stesso in 3 anni non sarà sorpreso da quello che fai?
Quindi vai avanti.
In tutti gli altri casi, usa il modo standard; altrimenti, tu ei tuoi colleghi incontrerete dei bug difficili da trovare.
Ad esempio, il mio collega si è lamentato del fatto che il mio codice causasse errori. Si è scoperto che aveva disattivato la valutazione booleana di cortocircuito nelle sue impostazioni del compilatore. L'ho quasi schiaffeggiato.
______ azszpr359048 ___
In questo caso particolare, restituisci di valore.
-
RVO e NRVO sono ottimizzazioni note e robuste che dovrebbero essere fatte da qualsiasi compilatore decente, anche in modalità C ++ 03.
-
Spostare la semantica assicura che gli oggetti vengano spostati fuori dalle funzioni se (N) il RVO non ha avuto luogo. Ciò è utile solo se il tuo oggetto usa internamente i dati dinamici (come %code% fa), ma dovrebbe essere proprio così se è che grande - il sovraccarico dello stack è un rischio con grandi oggetti automatici.
-
C ++ 17 applica RVO. Quindi non ti preoccupare, non sparirà su di te e finirà di stabilirsi completamente una volta che i compilatori saranno aggiornati.
E alla fine, forzare un'allocazione dinamica aggiuntiva per restituire un puntatore, o forzare il tipo di risultato a essere costruttibile di default solo così puoi passarlo come parametro di output sono sia soluzioni brutte che non idiomatiche a un problema probabilmente non lo sarà mai.
Basta scrivere un codice che abbia senso e ringraziare gli autori del compilatore per ottimizzare correttamente il codice che ha senso.
______ azszpr359053 ___
%bl0ck_qu0te%
Questa non è una piccola micro-ottimizzazione poco conosciuta, cullata, di cui si legge in un blog piccolo e trafficato, e poi si sente intelligente e superiore all'utilizzo.
Dopo C ++ 11, RVO è il modo standard per scrivere questo codice di codice. È comune, previsto, insegnato, menzionato nei discorsi, menzionato nei blog, menzionato nello standard, verrà segnalato come un errore del compilatore se non implementato. In C ++ 17, la lingua fa un passo avanti e impone copia elision in alcuni scenari.
Devi assolutamente fare affidamento su questa ottimizzazione.
Inoltre, return-by-value porta semplicemente a un codice molto 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.
______ azszpr359063 ___
La correttezza del codice che scrivi dovrebbe mai dipendere da un'ottimizzazione. Dovrebbe produrre il risultato corretto quando viene eseguito sulla "macchina virtuale" C ++ che usano nelle specifiche.
Tuttavia, quello 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 richiede questa ottimizzazione (ad esempio se il costruttore di copia effettivamente causasse il fallimento del codice), ora sei ai capricci del compilatore.
Penso che il miglior esempio di ciò nella mia pratica sia l'ottimizzazione della coda di chiamata:
%pre%
È un esempio stupido, ma mostra una chiamata coda, in cui una funzione è chiamata ricorsivamente proprio alla fine di una funzione. La macchina virtuale C ++ mostrerà che questo codice funziona correttamente, anche se posso causare un po 'di confusione su perché mi sono preoccupato di scrivere una tale routine di aggiunta in primo luogo. Tuttavia, nelle implementazioni pratiche di C ++, abbiamo uno stack e lo spazio è limitato. Se fatto pedanticamente, questa funzione dovrebbe spingere almeno %code% stack frames nello stack mentre fa la sua aggiunta. Se voglio calcolare %code% , questo non è un grosso problema. Se voglio calcolare %code% , potrei avere problemi a causare uno StackOverflow (e non il buon tipo ).
Tuttavia, possiamo vedere che una volta raggiunta l'ultima riga di ritorno, abbiamo davvero finito con tutto ciò che è presente nello stack frame corrente. Non abbiamo davvero bisogno di tenerlo in giro. L'ottimizzazione della chiamata di coda consente di "riutilizzare" lo stack frame esistente per la funzione successiva. In questo modo, abbiamo solo bisogno di 1 frame stack, piuttosto che %code% . (Dobbiamo ancora fare tutte quelle aggiunte e sottrazioni sciocche, ma non occupano più spazio.) In effetti, l'ottimizzazione trasforma il codice in:
%pre%
In alcune lingue, l'ottimizzazione delle chiamate tail è esplicitamente richiesta dalle specifiche. C ++ è non uno di quelli. Non posso fare affidamento sui compilatori C ++ per riconoscere questa opportunità di ottimizzazione delle chiamate tail, a meno che non vada caso per caso. Con la mia versione di Visual Studio, la versione di rilascio fa l'ottimizzazione della coda, ma la versione di debug non lo fa (design).
Quindi sarebbe male per me dipendere su essere in grado di calcolare %code% .
______ azszpr359096 ___
In pratica i programmi C ++ si aspettano alcune ottimizzazioni del compilatore.
Guarda in particolare le intestazioni standard delle implementazioni containers standard. Con GCC , puoi chiedere il formato preelaborato ( %code% ) e la rappresentazione interna di GIMPLE ( %code% o Gimple SSA con %code% ) della maggior parte dei file sorgente (tecnicamente le unità di traduzione) utilizzando i contenitori. Rimarrai sorpreso dalla quantità di ottimizzazione che viene eseguita (con %code% ). Quindi gli implementatori dei contenitori si affidano alle ottimizzazioni (e la maggior parte delle volte, l'implementatore di una libreria standard C ++ sa cosa sarebbe l'ottimizzazione e scriverà l'implementazione del contenitore con questi in mente, a volte scriverebbe 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 C ++ e i suoi contenitori standard abbastanza efficienti. Quindi puoi fare affidamento su di loro.
E allo stesso modo per il caso RVO menzionato nella tua domanda.
Lo standard C ++ è stato progettato congiuntamente (in particolare sperimentando ottimizzazioni sufficientemente buone proponendo nuove funzionalità) per funzionare bene con le possibili ottimizzazioni.
Ad esempio, considera il seguente programma:
%pre%
compila con %code% . Scoprirai che la funzione generata non esegue nessuna istruzione macchina %code% . Quindi la maggior parte dei passi in C ++ (costruzione di una chiusura lambda, la sua applicazione ripetuta, ottenendo %code% e %code% iteratori, 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
(aggiunto il 31 dicembre st 2017)
Vedi CppCon 2017: Matt Godbolt "Che cosa ha fatto il mio compilatore per me ultimamente? Sbloccare il coperchio del compilatore " parlare.
______ azszpr359068 ___
Ogni volta che usi un compilatore, la comprensione è che produrrà per te codice macchina o byte. Non garantisce nulla su ciò che è il codice generato, tranne che implementerà il codice sorgente in base alle specifiche della lingua. Nota che questa garanzia è la stessa indipendentemente dal livello di ottimizzazione usato, e quindi, in generale, non c'è motivo di considerare un output più "giusto" dell'altro.
Inoltre, in quei casi, come RVO, dove è specificato nella lingua, sembrerebbe inutile andare fuori strada per evitare di usarlo, specialmente se rende il codice sorgente più semplice.
Viene fatto un grande sforzo per far sì che i compilatori producano risultati efficienti e chiaramente l'intento è quello di utilizzare tali funzionalità.
Ci possono essere dei motivi per usare 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 se ottimizzato, e non è una conseguenza di alcuni peculiarità del dispositivo su cui si sta eseguendo, quindi c'è un bug da qualche parte, ed è improbabile che sia nel compilatore.)
______ azszpr359193 ___
Penso che gli altri riguardassero bene l'angolo specifico di C ++ e RVO. Ecco una risposta più generale:
Per quanto riguarda la correttezza, non dovresti fare affidamento sulle ottimizzazioni del compilatore o sul comportamento specifico del compilatore in generale. Fortunatamente, non sembra che tu stia facendo questo.
Quando si tratta di prestazioni, devi 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 in qualsiasi modo, purché il codice compilato si comporti secondo le specifiche della lingua. E non sono a conoscenza di alcuna specifica per un linguaggio mainstream che specifica la velocità di ogni operazione.
___ ______ azszpr361578 ___
Tutti i tentativi di codice efficiente scritti in tutto tranne l'assembly si basano molto, molto sull'ottimizzazione del compilatore, a partire dalla più semplice allocazione del registro efficiente per evitare fuoriuscite di stack superflue dappertutto e almeno ragionevolmente buone, se non eccellenti, selezione delle istruzioni. Altrimenti torneremmo agli anni '80 in cui dovevamo mettere %code% suggerimenti ovunque e usare il numero minimo di variabili in una funzione per aiutare i compilatori C arcaici o anche prima quando %code% era un'utiliz- zazione di ramificazione utile.
Se non avessimo la sensazione di poter contare sulla capacità del nostro ottimizzatore di ottimizzare il nostro codice, staremmo ancora codificando i percorsi di esecuzione critici per le prestazioni in assembly.
È davvero una questione di quanto affidabile sia l'ottimizzazione che si può fare, individuando il profilo e analizzando le funzionalità dei compilatori che si hanno e magari anche smontando se c'è un hotspot che non si riesce a capire dove il compilatore sembra non essere riuscito a fare un'ovvia ottimizzazione.
L'RVO è qualcosa che esiste da secoli e, almeno escludendo casi molto complessi, è qualcosa che i compilatori si sono applicati in modo affidabile da secoli. Non vale sicuramente la pena lavorare su un problema che non esiste.
Err sul lato di fare affidamento sull'ottimizzatore, non temerlo
Al contrario, direi errare sul lato di affidarmi troppo alle ottimizzazioni del compilatore che troppo poco, e questo suggerimento proviene da un ragazzo che lavora in settori molto critici per le prestazioni in cui efficienza, manutenibilità e qualità percepita tra i clienti c'è una sfocatura gigantesca. Preferirei che ti affidi troppo fiduciosamente al tuo ottimizzatore e trovi alcuni casi oscuri in cui ti sei affidato troppo al fatto che non ti affidavi troppo e che codifica sempre da paure superstiziose per il resto della tua vita. Avrai almeno la possibilità di raggiungere un profiler e di indagare correttamente se le cose non vengono eseguite nel modo più veloce possibile e di acquisire preziose conoscenze, non superstizioni, lungo il percorso.
Stai facendo bene ad appoggiarti all'ottimizzatore. Continuate così. Non diventare come quel ragazzo che inizia a richiedere esplicitamente di inline ogni funzione chiamata in un ciclo prima ancora di tracciare una paura sbagliata delle carenze dell'ottimizzatore.
Profiling
Il profiling è davvero la rotonda, ma la risposta definitiva alla tua domanda. Il problema che i principianti desiderosi di scrivere codice efficiente spesso non è quello che ottimizzare, è ciò che non deve ottimizzare perché sviluppano tutti i tipi di intuizioni fuorvianti su inefficienze che, se umanamente intuitive, sono computazionalmente sbagliate. Lo sviluppo dell'esperienza con un profiler inizierà davvero a darti un giusto apprezzamento non solo delle capacità di ottimizzazione dei tuoi compilatori su cui puoi tranquillamente appoggiarti, ma anche delle capacità (oltre che dei limiti) del tuo hardware. C'è forse ancora più valore nel profilarsi nell'apprendimento di ciò che non vale la pena ottimizzare rispetto all'apprendimento di ciò che è stato.
______ azszpr363212 ___
No.
Questo è quello che faccio sempre. Se devo accedere a un blocco arbitrario a 16 bit in memoria, lo faccio
%pre%
... 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 là fuori. In teoria, un compilatore non ottimizzante potrebbe effettivamente chiamare %code% , risultando in prestazioni totalmente negative, ma non è un problema per me, poiché utilizzo le ottimizzazioni del compilatore.
Considera l'alternativa:
%pre%
Questo codice alternativo non funziona su macchine che richiedono un allineamento corretto, se %code% restituisce un puntatore non allineato. Inoltre, potrebbero esserci problemi di aliasing in alternativa.
La differenza tra -O2 e -O0 quando si utilizza il trucco %code% è ottima: 3,2 Gbps di prestazioni del checksum IP rispetto a 67 Gbps di prestazioni del checksum IP. Oltre una differenza di ordine di grandezza!
A volte potrebbe essere necessario aiutare il compilatore. Ad esempio, invece di affidarsi al compilatore per srotolare i loop, puoi farlo da solo. O implementando il famoso dispositivo di Duff , o in modo più pulito.
Lo svantaggio di affidarsi alle ottimizzazioni del compilatore è che se esegui gdb per eseguire il debug del tuo codice, potresti scoprire che molto è stato ottimizzato. Quindi, potrebbe essere necessario ricompilare con -O0, il che significa che le prestazioni risulteranno totalmente inutili durante il debug. Penso che sia un difetto da considerare, 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 intero a 16 bit è un comportamento indefinito a causa di problemi di aliasing e allineamento.
______ azszpr359116 ___
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, rappezzare, refactoring et.c. o sono altre cose più importanti, come le prestazioni, i costi o la compatibilità con hardware specifico o il tempo necessario per lo sviluppo.
______ azszpr359123 ___
Penso che la noiosa risposta a questo è: 'dipende'.
È una cattiva pratica scrivere codice che si basa su un'ottimizzazione del compilatore che rischia di essere disattivato e dove la vulnerabilità non è documentata e dove il il codice in questione non è stato testato unitamente in modo che se si rompesse lo sapresti ? Probabilmente.
È una cattiva pratica scrivere codice che si basa su un'ottimizzazione del compilatore che non è probabile che sia disattivato , che è documentato e è testato dall'unità ? Forse no.
______ azszpr359057 ___
A meno che non ci sia altro che non ci stai dicendo, questa è una cattiva pratica, ma non per il motivo che suggerisci.
Forse a differenza di altri linguaggi che hai usato prima, restituire il valore di un oggetto in C ++ produce una copia dell'oggetto. Se modifichi l'oggetto, stai modificando un oggetto diverso . Cioè, se ho %code% e %code% , allora faccio %code% , quindi %code% equivale ancora a 1, non a 3.
Quindi no, usare un oggetto come valore invece che come riferimento o puntatore non fornisce la stessa funzionalità e potresti finire con errori nel tuo software.
Forse lo sai e non influisce negativamente sul tuo caso d'uso specifico. Tuttavia, in base alla formulazione della tua domanda, sembra che tu non sia consapevole della distinzione; parole come "creare un oggetto nella funzione".
"crea un oggetto nella funzione" suona come %code% dove "restituisci l'oggetto per valore" suona come %code%
%code% e %code% sono cose molto, molto diverse; il primo può causare il danneggiamento della memoria se non viene utilizzato e compreso correttamente, e quest'ultimo può causare perdite di memoria se non viene utilizzato e compreso correttamente.
______ azszpr359034 ___
Pieter B è assolutamente corretto nel raccomandare meno stupore.
Per rispondere alla tua domanda specifica, ciò che (molto probabilmente) significa in C ++ è che dovresti restituire una %code% all'oggetto costruito.
Il motivo è che questo è più chiaro per uno sviluppatore C ++ su cosa sta succedendo.
Sebbene il tuo approccio funzioni molto probabilmente, stai effettivamente segnalando che l'oggetto è un tipo di valore piccolo quando, in realtà, non lo è. Oltre a ciò, stai buttando via ogni possibilità di astrazione dell'interfaccia. Questo può essere OK per i tuoi scopi attuali, ma è spesso molto utile quando si tratta di matrici.
Apprezzo che se provieni da altre lingue, inizialmente tutti i sigilli possono essere fonte di confusione. Ma fai attenzione a non presumere che, non utilizzandoli, rendi più chiaro il tuo codice. In pratica, è probabile che sia vero il contrario.
___
risposta data
16.10.2017 - 00:22