-1

Earlier today I was working on a simple program from a textbook.

Although I solved the question with confidence and a clear robust solution, the solution wasn't perfect as it had steps which were redundant.

The more I optimized and tried to make it perfect:

The code became more:
- Unreadable
- Buggy

I became:
- Less confident of the solution
- More frustrated

Is there a proper way to deal with optimizations so you don't lose your mind and time, or should one stop optimizing if the solution is unclean.

closed as too broad by gnat, user22815, enderland, Doc Brown, MetaFight Feb 13 '17 at 15:54

Please edit the question to limit it to a specific problem with enough detail to identify an adequate answer. Avoid asking multiple distinct questions at once. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 3
    Automated tests sure would help! – svidgen Feb 12 '17 at 22:34
  • 2
    You should decide what you're optimising for. If you want performance (and if you're sure you need it) then sacrificing readability is normal. I would recommend, however, to optimise for Readability because human time is far more expensive than CPU time or RAM. Don't fall in the premature performance optimisation trap. And, for goodness sake, if you need to optimise for performance, use a profiler! – MetaFight Feb 12 '17 at 23:20
  • I suspect you're optimizing by eyeball. Every programmer makes the mistake of thinking if something looks complicated or repetitive it must be inefficient. Maybe it is complicated or repetitive, but that doesn't tell you it's inefficient. It only tells you it may be somewhat hard to follow. To find possible speedups, do this. – Mike Dunlavey Feb 14 '17 at 14:38
1

Don't try for perfection. Recognize that there's a balance that must be struck between "good" code and "perfect" code.

Let's say the code works well, is readable, maintainable, easy to work with, but has a small handful of things that might not be 100% the way you'd like. If you try to refactor those few things but end up digging yourself into a rabbit hole, then the code would have been better off if you had just left it in it's "good" working state, right?

Actually, such cases are usually a sign that a better architecture at the outset would have been better, but at this point it may not make much sense to go back and "fix it all up."

For every single project you work on, you'll walk away with a few "I could have done X better" thoughts - and that's good. You learn from them and apply what you've learned in your next project. It's a never-ending cycle of programming. No project is ever "perfect", and recognizing that "hitting the balance" is what you really need to be doing is the key to your sanity. Learn from it, be happy with a job well done (even if not perfect), and try again next time.

For this particular project, it sounds like it might be good to take a step back and say "if I had to do it all again, what might have I done differently, specifically to prevent this problem area from being more buggy and contributing to less readable code when I try to fix it?" If you can figure that out, it sure sounds like it'd be the good takeaway.

The problem comes when people fail to be retrospective about these things. Good experience is built like this, but if a person isn't willing to reflect on what could have been done better, they're never going to remember that and better their skills for next time.

2

The only best practice that I know of for reducing bugs while optimizing code (or for any kind of refactoring, for that matter) is proper testing. (Which includes extensive testing.)

So, first you get your code to work, then you have all your tests in place which prove that it works correctly, (or first you do your tests, then you write your code, if you are of the TDD persuation,) and then you start tweaking things. And with every tweak, you re-run your tests. This way, if your tweak breaks something, you catch it.

Of course, as you are tweaking things, you might get ideas about more tests to specifically test for things that your tweak may break. When such an idea comes to mind, stop what you are doing, shelve your tweaking changes, add that test, make sure everything works, and then unshelve your tweaking changes and proceed.

  • What worries me is that, if I have testing with proper case coverage, this gives leeway to write code that is poorly written knowing that the testing will catch anything. Right now I'm trying to write code that I am highly certain works, and then have a safety net of test cases. – Janarth K Feb 14 '17 at 16:05
  • @JanarthK well, of course you decide how you write your own code, but be advised that the worldwide trend, among the vast majority of professional software developers, is with testing. Even for many of us who started programming before testing was even a thing, once we started using it, we started loving it so much, that we are never looking back. Testing is not regarded as an additional safety measure; testing is regarded as the fundamental guarantee of correctness. – Mike Nakis Feb 14 '17 at 16:13
2

Avoid Pre-optimization

Pre-optimization occurs when you assume a piece of code is inefficient on resources, and thus focus on fixing it, without even checking if it's a problem.

You can waste quite a bit of time on this, and it could potentially harm your design decisions. If you have any worries, profile your application.

Avoid Mico-optimization

Mico-optimization is an attempt to save resources by trying trying to strip things down to their bare bones.

An example would be declaring local variables. If we had a collision detection method, it may look something like:

boolean isCollidingWith(Entity entity) {
    return this.x < entity.x && this.x + this.width > entity.x || this.x > entity.x && entity.x + entity.width > this.x || ...;
}

A developer may want to clean this up by declaring some local boolean variables:

boolean isCollidingWith(Entity entity) {
    boolean collideRight = this.x < entity.x && this.x + this.width > entity.x;
    boolean collideLeft = this.x > entity.x && entity.x + entity.width > this.x;

    return collideRight || collideLeft || ...;
}

If one were to say "you don't need those variables, they just waste space", that would be considered mico-optimization. Although a little space may be saved, it's negligable, and attempting to save that space may harm readability.

Suit your program's needs/requirements

You are required to write a program that calculates PI. Whoever writes the most powerful PI calculator wins a prize.

In a scenario like this, you may prefer resource efficiency over readability, conserving as much as possible. You plan on deleting this program afterwards, so you won't need to read this code ever again.

Are you going to invest hours into making this project scalable? Probably not.

You own a popular social networking site.

The rules have changed. You have millions of users constantly requesting content, while also attempting to fix any bugs currently being exploited. On top of this, you have ideas of your own you want to implement.

Performance is important, but there are many other, potentially more important, factors at hand. Readability is a must, due to your need for a team and the need to revisit code.

Make sure you understand the needs of your situation.

Learn how to properly test

Testing can be a long and painful process if you don't know how to do it right. You could spend hours fighting with tests, just to realize you didn't test a unit properly.

Unit testing is a mainstream form of testing. It focuses on checking the integrity of a single unit of code. Integration testing would be the next step: testing as a whole.

Mocking allows you to recreate situations that don't usually occur on command, such as seeing if a robot wakes up when an alarm goes off (you could mock the alarm and give it a specific "current time", rather than waiting).

Without exposing yourself to these concepts, you'll have a hard time ensuring your software's integrity.

  • This is a good thorough answer! The only thing I could have said extra, is that sometimes it is fun and good practice to see what you can optimise. Optimising code isn't something to be ashamed of, but it is wasted time if it's not required and wasting time is real trouble. – Erdrik Ironrose Feb 13 '17 at 9:38
-2

It's a trade-off. You should optimize when there is a likely or clear potential benefit from optimizing the code, and stop optimizing when you no longer estimate that benefit to be worth your time.

The cost, as you mentioned, can be frustration, more time finding and fixing obscure bugs, creating more thorough tests, clearly documenting obscure refactorings, giving up and retreating to a previous working version, and etc.

The benefits can be a little as just from learning how to write (or not write!) "tight" efficient code for later situations where it is required (tiny embedded IoT, or zillion of costly AWS instances, or deep space network uploads, mobile user's battery life, and etc.)

In other situations, removing duplicated code might even cause disadvantages (tutorial code readability, weird processor cache/pipeline hazards, and etc.)

Not the answer you're looking for? Browse other questions tagged or ask your own question.