È meglio scrivere un algoritmo o codice efficiente che sia più facile da capire?

3

Quindi mi è stato recentemente assegnato un incarico di programmazione da una grande società finanziaria e ho pensato a due modi per risolvere il problema. Uno dei modi coinvolti 1 loop esterno e 1 ciclo interno. In questo caso, il codice sarebbe relativamente facile da comprendere e ho visto questo metodo come una soluzione più "ovvia".

Tuttavia, ho pensato ad un altro modo che usava 4 out per loop consecutivamente. Non sono esperto in Big O, ma la mia comprensione è che, asintoticamente parlando, 4 outer for loops è O (n) e 1 outer e 1 inner for loop è O (n ^ 2). Tuttavia, il metodo 4 outer for loops era decisamente più difficile da capire e meno ovvio.

Mi stavo chiedendo da una domanda di assegnazione del codice, quale metodo è considerato migliore? E poi in una prospettiva più ampia, quale è meglio? Dato che sono ancora un po 'nuovo all'idea della codifica professionale (ho fatto un sacco di codice casuale quando ero più giovane), so che c'è un'idea di equilibrio tra leggibilità ed efficienza. Mi chiedevo quale sia il bilanciamento in questo caso.

Grazie!

    
posta Tyler M 17.09.2018 - 00:10
fonte

6 risposte

19

Se non si tratta di un compito, puoi facilmente intuire quale delle soluzioni è migliore qui.

  • Nella maggior parte dei casi, sarebbe il primo.

  • In alcuni rari casi (e il fatto che stai parlando del settore finanziario significa che non dovresti ignorare questa possibilità), le prestazioni sarebbero fondamentali (o perché il pezzo di codice verrà eseguito centinaia di volte al secondo su migliaia di macchine, o perché il salvataggio di millisecondi significherebbe che i trader avranno un vantaggio competitivo). Ma:

Poiché si tratta di un compito, fallo entrambi.

L'obiettivo di un incarico è vedere come reagirai di fronte a un determinato problema. Puoi:

  • Trova la soluzione semplice e perdi quella ottimizzata,

  • Trova la soluzione ottimizzata e perdi quella semplice,

  • Individua entrambe le soluzioni e comprendi i vantaggi e gli svantaggi di ciascuna, compresa la quantità della seconda alternativa più veloce della prima, perché è più difficile da capire, cosa può essere fatto per rendere più facile la comprensione senza sacrificare le sue prestazioni e quali sono gli svantaggi del potenziale cambiamento.

Non puoi indovinare (altrimenti non faresti questa domanda in primo luogo). Pertanto, per sicurezza, basta mostrare che hai trovato entrambe le soluzioni e sei pronto a suggerire l'una o l'altra dopo aver ottenuto maggiori informazioni sui bisogni effettivi in termini di prestazioni e manutenibilità.

    
risposta data 17.09.2018 - 00:28
fonte
3

Dipende.

A volte un'azienda ha preferenze chiare per l'una o per l'altra; a volte il team di sviluppo ha delle preferenze.

Se sei libero di decidere, pensa alla pertinenza della performance in quest'area:

  • Se si tratta di una parte critica e i problemi di prestazioni influenzano la soddisfazione dell'utente o l'usabilità dell'applicazione nel suo complesso, vai alla soluzione più rapida e complessa (ma documenta i tuoi pensieri e l'algoritmo nel codice).
  • Se questa è non una parte critica, usa la versione più facile da capire (e mantieni) (se ti infastidisce, aggiungi un commento che avresti potuto fare più velocemente, ma non lo fece, per leggibilità).

In ogni caso, assicurati che la tua previsione sia reale (= > misura!). Niente è peggio di una soluzione complicata che è più lenta.

    
risposta data 17.09.2018 - 00:27
fonte
3

Dal punto di vista della classe, sono d'accordo (con la maggior parte delle risposte / commenti) che è più facile capire meglio. Il più delle volte, questa sarà probabilmente la risposta corretta a quella domanda in classe. Ma molti problemi che alla fine incontrerai facendo (citando la tua frase) "codifica professionale" potrebbero non avere una soluzione così semplice. Allora cosa fai allora?

In definitiva, la tua più facile comprensione contro più efficiente è una falsa dicotomia. La buona documentazione in linea è sempre il sine qua non che consente ai maintainer di fare i conti con decine di migliaia di righe di codice che non avevano mai visto prima e dove gli sviluppatori originali di quel codice sono lunghi -sono andato.

Un aspetto della buona documentazione in linea è un grande blocco di commenti di intestazione che precede ciascuna funzione, che descrive e discute il suo scopo, la sequenza chiamante, gli effetti collaterali, gli algoritmi, ecc. Ad esempio, dalla mia piccola lib di funzioni di proiezione,

/* ==========================================================================
 * Function:    rotatetoz ( l, r, axis, nlines )
 * Purpose:     xlate, rotate l, returning r as though
 *              axis were along the z-axis with axis.pt1 at origin
 * --------------------------------------------------------------------------
 * Arguments:   l (I)           input LINE * to 3d-lines to be xlated, rotated
 *                              along with axis
 *              r (O)           output LINE * to xlated, rotated 3d-lines
 *              axis (I)        LINE specifying rotation axis,
 *                              with pt1 the tail, and pt2 the head,
 *                              such that pt1 ends up at (0,0,0), and
 *                              pt2 at (0,0,z=r) with r the length of axis
 *              nlines (I)      int containing #lines to be rotated
 * Returns:     (double)        r, the length of axis, or -1.0 for any error
 * --------------------------------------------------------------------------
 * Notes:     o axis is (obviously) not changed, with r xlated, rotated as
 *              though axis pt1 was at the origin, and pt2 was at (0,0,z=r)
 *            o to accomplish this, first xlate pt1 of axis to (0,0,0),
 *              and then...
 *                    z.             As per usual spherical polar coords,
 *                    |  .                 x = r*cos(phi)*cos(theta)
 *                    |    .               y = r*cos(phi)*sin(theta)
 *                    |      .             z = r*sin(phi), where
 *                    |        o(x,y,z)    r^2 = x^2 + y^2 + z^2
 *                    |90-phi. |for r-axis
 *                    |    .   |     Then, to rotate axis to positive z-axis,
 *                    |  .     |           o rotate about z-axis by -theta
 *                    |.       |             to bring axis to x,z-plane
 *                    +--------|---y       o rotate about y-axis by -(90-phi)
 *                   /   .     |  /          to bring axis coincident with z
 *                  /theta .   | /   Where we calculate
 *                 /         . |/          o sin(phi)   = z/r
 *                x------------+           o sin(theta) = y/(r*cos(phi)) or
 *                                           cos(theta) = x/(r*cos(phi))
 *
 * ======================================================================= */
/* --- entry point --- */
double  rotatetoz ( LINE *l, LINE *r, LINE axis, int nlines ) {
  /* ---
   * lots of code;
   * ---------------- */
  } /* --- end-of-function rotatetoz() --- */

E il codice stesso è ampiamente commentato, spesso riferendosi alla sintesi / spiegazione dell'algoritmo di cui sopra. Si spera che alcuni futuri manutentori trovino più facilmente la decomposizione del codice un po 'denso in blocchi funzionali. Ma non è mai stata possibile una scelta progettuale "più facile da leggere e più efficiente". E spesso non lo è mai.

    
risposta data 17.09.2018 - 16:22
fonte
1

Il consensis è un codice leggibile su un codice ottimizzato, perché per la maggior parte del codice si impiegherà più tempo a leggere il codice e quindi a quando viene effettivamente eseguito.

Se vuoi ottimizzare, prima vedi se è necessario: usa un profiler per trovare le aree che ne hanno bisogno e poi ottimizza solo le routine che ne hanno bisogno.

Tuttavia, avere un buon feeling con la grande complessità O dovrebbe essere una cosa che ogni programmatore dovrebbe avere. Perché scegliere l'algoritmo giusto ti farà risparmiare molto tempo in futuro. C'è questo bel libro: "Algoritmi" e secondo me ogni sviluppatore di software dovrebbe leggerlo. È una grande differenza che vedo tra i programmatori autodidatti e quelli con una formazione accademica, questa conoscenza di base dell'algoritmo che spesso mancano ai programmatori autodidatti.

Quindi la mia domanda qui: quale algoritmo stai usando?

    
risposta data 17.09.2018 - 08:43
fonte
0

Donald Knuth ha dichiarato qualcosa di simile

L'ottimizzazione prematura è la radice di tutto il male.

Cerca le tecniche più semplici e più evidenti fino a quando le cose non funzionano.

Quindi prova e ottimizza i colli di bottiglia.

    
risposta data 19.09.2018 - 02:08
fonte
0

Se segui l'approccio alla progettazione di Developing everything as an API , l'approccio che raccomando è questo (Scritto in C ++ / Qt):

  1. Identifica il valore di ritorno che desideri generare dal tuo algoritmo. Nel mio esempio piuttosto arbitrario, abbiamo bisogno del numero di hash all'inizio di una stringa.

  2. Crea una funzione membro per quel valore di ritorno. Potrebbe sembrare qualcosa di simile:

    // A descriptive function name is preferred over comments
    int MyClass::countFrontRecurringHashes() 
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         ... 
    }
    
  3. Tutto il codice successivo dovrebbe basarsi esclusivamente sul valore di ritorno di questa funzione. Non è necessario modificare le variabili membro all'interno di questo codice, consentendo così di modificare l'algoritmo senza doversi preoccupare di come viene gestito l'altro codice.

  4. Al primo passaggio, scrivi il codice per la leggibilità se ti fa risparmiare tempo. Contrassegnalo per miglioramenti se è ovvio:

    int MyClass::countFrontRecurringHashes()
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         /// Todo: Performance
         return QRegularExpression("^[#]*").match(m_MyString).capturedLength();
    }
    
  5. Quando il tuo codice è attivo e funzionante, sarai in grado di identificare se migliorare le prestazioni è una priorità più alta rispetto a un ulteriore sviluppo altrove. Se lo è, puoi quindi entrare e inserire una modifica in questo modo:

    int MyClass::countFrontRecurringHashes()
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         int i, l = m_MyString.length();
         for(i = 0; i < l && m_MyString[i] == '#'; ++i);
         return i;
         // This code is more complex, abstract, and difficult to read, 
         // However, it is about 100 times more efficient.
    }
    
  6. In alternativa, se stai lavorando in una squadra, questo è anche un buon posto per attaccare un nuovo membro. Digli di cercare todo e fixme nel codice, sebbene si consiglia cautela, poiché spesso, un fixme o un todo diventano infine una funzionalità su cui potrebbe fare affidamento un altro codice. Ecco perché è importante l'incapsulamento corretto e una strategia di sviluppo dell'API.

risposta data 03.12.2018 - 20:53
fonte