3

So I was recently given a coding assignment from a large financial firm, and I thought of two ways to solve the problem. One of the ways involved 1 outer for loop and 1 inner for loop. In this case, the code would be relatively easy to understand, and I saw this method as a more "obvious" solution.

However, I thought of another way that used 4 out for loops consecutively. I'm no expert in Big O, but my understanding is that, asymptotically speaking, 4 outer for loops is O(n) and 1 outer and 1 inner for loop is O(n^2). However, the 4 outer for loops method was definitely a bit harder to understand and less obvious.

I was wondering from a coding assignment question, which method is seen as better? And then in a broader perspective, which one is better? Since I'm still sort of new to the idea of professional coding (I did a lot of casual coding when I was younger) I know there's an idea of balance between readability and efficiency. I was wondering what the balance is in this case.

Thanks!

Tyler M
  • 89

6 Answers6

19

If this wasn't an assignment, you could relatively easily guess which one of the solutions is better here.

  • In most cases, it would be the first one.

  • In some rare cases (and the fact that you're talking about the financial sector means that you shouldn't ignore this possibility), performance would be key (either because the piece of code will be run hundreds of times per second on thousands of machines, or because saving milliseconds would mean that traders will have a competitive advantage). But:

Since this is an assignment, do both.

The goal of an assignment is to see how you will react faced to a given problem. You may:

  • Find the simple solution and miss the optimized one,

  • Find the optimized solution and miss the simple one,

  • Spot both solutions and understand the benefits and drawbacks of each, including how much your second alternative is faster than the first one, why is it more difficult to understand, what can potentially be done to make it easier to understand without sacrificing its performance and what are the drawbacks of the potential change.

You cannot guess (otherwise you wouldn't be asking this question in the first place). Therefore, to be safe, just show that you found both solutions and are ready to suggest the one or the other after you get more information about the actual needs in terms of performance vs. maintainability.

3

It depends.

Sometimes a company has clear preferences for the one or the other; sometimes the development team has preferences.

If you are free to decide, think about the relevance of the performance in this area:

  • If this is a critical part, and performance issues would affect the user satisfaction or usability of the application overall, go for the faster and more complex solution (but document your thoughts and the algorithm in the code).
  • If this is not a critical part, use the easier to understand (and maintain) version (if it bothers you, add a comment that you could have done it faster, but didn't, for readability).

Either way, make sure that your prediction is real (=> measure!). Nothing is worse than a complicated solution that is slower.

Aganju
  • 1,473
3

From a classroom perspective, I'd agree (with the majority of answers/comments) that easier-to-understand is better. Most of the time, that'll probably be the correct answer to that classroom question. But many problems you eventually encounter doing (quoting your phrase) "professional coding" may have no such easy solution whatsoever. So what do you do then?

Ultimately, your easier-to-understand versus more-efficient is a false dichotomy. Good inline documentation is always the sine qua non that allows maintainers to come to grips with tens-of-thousands of lines of code they'd never seen before, and where the original developers of that code are long-since gone.

One aspect of good inline documentation is a large header comment block preceding each function, describing and discussing its purpose, calling sequence, side effects, algorithms, etc. For example, from my little lib of projection functions,

/* ==========================================================================
 * 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() --- */

And the code is itself extensively commented, frequently referring back to the above algorithm synopsis/explanation. That hopefully lets some future maintainers more easily figure out the decomposition of the somewhat-dense code into functional blocks. But there was never any "easier-to-read versus more-efficient" design choice possible. And there frequently never is.

1

The consensis is to go for readable code over optimized code, because for most code you are going to spend more time reading the code then the time it's actually being executed.

If you want to optimize, first see if it's needed: use a profiler to find the areas that need it and then optimize only the routines that need it.

However, having a good feel for big O complexity should be a thing any programmer should get. Because choosing the right algorithm will save you a lot of time in the future. There is this nice book: "Algorithms" and in my opinion every software developer should read it. It's a big difference I see between self taught and academically schooled programmers, this basic algorithm knowledge which self-taught programmers often lack.

So my question to you here: which algorithm are you using?

Pieter B
  • 13,310
0

Donald Knuth stated something like

Premature optimization is the root of all evil.

Go for the simplest most self evident techniques until things are working.

Then test and optimize the bottlenecks.

dkretz
  • 179
0

If you take the design approach of Developing everything as an API, the approach I recommend is this (Written in C++/Qt):

  1. Identify the return value you require to generate from your algorithm. In my rather arbitrary example, we need the number of hashes at the beginning of a string.

  2. Create a member function for that return value. It might look something like:

    // A descriptive function name is preferred over comments
    int MyClass::countFrontRecurringHashes() 
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         ... 
    }
    
  3. All subsequent code should rely exclusively upon this function's return value. No member variables should be altered within this code, thus allowing you to change the algorithm without having to worry about how other code is interacted with.

  4. On the first pass, write the code for readability if it saves you time. Mark it for improvement if its obvious:

    int MyClass::countFrontRecurringHashes()
    {
         /* Validate Parameters */
         if (m_MyString.isNull()) {
              /* Throw Error */
         }
    
         /* Algorithm */
         /// Todo: Performance
         return QRegularExpression("^[#]*").match(m_MyString).capturedLength();
    }
    
  5. When your code is up and running, you will be able to identify if improving performance is a higher priority than further development elsewhere. If it is, you can then go in and put in a change like so:

    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. Alternatively, if you are working on a team, this is also a good place to stick a new member. Tell him to search for todo's and fixme's in the code, although caution is advised, as often times, a fixme or a todo eventually becomes a feature other code might rely upon. That is why proper encapsulation and an API development strategy is important.

Anon
  • 3,639