18

I am writing a scientific program in Python and C with some complex physical simulation algorithms. After implementing algorithm, I found that there are a lot of possible optimizations to improve performance. Common ones are precalculating values, getting calculations out of cycle, replacing simple matrix algorithms with more complex and other. But there arises a problem. Unoptimized algorithm is much slower, but its logic and connection with theory look much clearer and readable. Also, it's harder to extend and modify optimized algorithm.

So, the question is - what techniques should I use to keep readability while improving performance? Now I am trying to keep both fast and clear branches and develop them in parallel, but maybe there are better methods?

user517893
  • 471
  • 4
  • 14
  • 1
    A very interesting and useful question. I fear that there are no clear answers though. You have to sacrifice *something* to get your performance boosts. Readability is just one of them. – Noufal Ibrahim Sep 04 '11 at 18:00
  • 1
    There is a good reason Knuth named his book "The *Art* of Computer Programming"... :) – hugomg Sep 04 '11 at 22:52

4 Answers4

15

Just as a general remark (I'm not too familiar with Python): I would suggest you make sure that you can easily exchange the slow parts of the 'reference implementation' with the 'optimized' parts (e.g., use something like the Strategy pattern).

This will allow you to cross-validate the results of the more sophisticated algorithms (to ensure you did not mess up the results), and will keep the overall structure of the simulation algorithm clear (separation of concerns). You can place the optimized algorithms into separate source files / folders / packages and document them separately, in as much detail as necessary.

Apart from this, try to avoid the usual traps: don't do premature optimization (check if it is actually worth it, e.g. with a profiler), and don't re-invent the wheel (look for available libraries).

Roland Ewald
  • 4,630
  • 3
  • 35
  • 49
  • 1
    Thank you, that makes sense. Probably I should try to split algorithms in reasonably many parts and to try to implement parts in different ways with same input/output but different implementations. Concepts you wrote about look worthful to study them more closely. – user517893 Sep 04 '11 at 18:10
  • Nice to hear you found this helpful. Splitting the algorithm sounds good... (divide & conquer! :-) Hiding the implementations behind specific interfaces will also allow you to re-use your unit tests etc. – Roland Ewald Sep 04 '11 at 18:16
  • There is also a reason why this is important to me. As I mentioned, I write in Python, but use C to implement most intensive parts (probably, the most effective optimization). So, C is important for performance and Python - for implementing new algorithms and for interface. It makes dividing strategy useful for easily mixing Python and C to get balance between their advantages. – user517893 Sep 04 '11 at 18:34
3

Yours is a very good question that arises in almost every piece of code, however simple or complex, that's written by any programmer who wants to call himself a pro.

I try to remember and keep in mind that a reader newly come to my code has pretty much the same crude view of the problem and the same straightforward (maybe brute force) approach that I originally had. Then, as I get a deeper understanding of the problem and paths to the solution become clearer, I try to write comments that reflect that better understanding. I sometimes succeed and those comments help readers and, especially, they help me when I come back to the code six weeks later. My style is to write plenty of comments anyway and, when I don't (because: a sudden insight gets me excited; I want to see it run; my brain is fried), I almost always greatly regret it later.

It would be great if I could maintain two parallel code streams: the naïve way and the more sophisticated optimized way. But I have never succeeded in that.

To me, the bottom line is that if I can write clear, complete, succinct, accurate and up-to-date comments, that's about the best I can do.

Just one more thing that you know already: optimization usually doesn't mean shoehorning a ton of code onto one source line, perhaps by calling a function whose argument is another function whose argument is another function whose argument is yet another function. I know that some do this to avoid storing a function's value temporarily. But it does very little (usually nothing) to speed up the code and it's a bitch to follow. No news to you, I know.

Pete Wilson
  • 8,610
  • 6
  • 39
  • 51
  • Thank you for sharing your experience. Yes, writing enough of accurate comments is pretty important and I've already met problems when dealing with old code without enough of those. And yes, that "one-liner" optimization will give vast increase of complexity with no or little benefit, so that's pretty right :-) – user517893 Sep 04 '11 at 18:54
  • 1
    +1 What I've found is if I'm doing something that really needs explaining, I need to do what I can to *teach* the reader what's going on. It's an effort. It's not always successful. The reader may not have enough background to be able to follow it. – Mike Dunlavey Sep 05 '11 at 01:04
  • 1
    @Mike Dunlavey -- yes, good point: we are responsible for *teaching* the reader. Something I thought of after reading your comment: if the reader doesn't understand, then (leaving aside background insufficiency) there's a finite chance that *I* don't understand it :-) – Pete Wilson Sep 05 '11 at 01:20
  • I'm kind of in the middle between classic software engineers and an honest-to-gosh mathematician. I try to understand what he's doing, but it's up-hill. At the same time I do everything from differential equations to compilers to UI code, and the engineers don't even try to understand that. – Mike Dunlavey Sep 05 '11 at 02:30
3

It is common to assume you must give up readability to get performance.

That's not necessarily so.

You need to find out What exactly is it spending much time doing, and why?

Notice, I didn't say you need to do any measuring.

Here's an example of what I mean.

Chances are very good that you can do some simple changes to avoid waste motion, but don't fix anything until the program itself has told you what to fix.

Community
  • 1
  • 1
Mike Dunlavey
  • 40,059
  • 14
  • 91
  • 135
  • agreed. corollary is that there probable *aren't* that many places to make (significant) performance improvements. most places simply don't matter compared to the few that do. – andrew cooke Sep 04 '11 at 22:56
  • @andrew: I don't have much experience in python, but I've [done a lot of performance tuning](http://stackoverflow.com/questions/926266/performance-optimization-strategies-of-last-resort/927773#927773). I agree, the first few problems may be very localized. Then, if you're lucky and can redesign the program, you may be able to go another round. My experience with math-heavy code is the second round isn't there. You basically have to dig on-line for the best algorigthm and look for low-level optimizations. But yeah, most of the code is innocent. – Mike Dunlavey Sep 04 '11 at 23:13
1
def whatYouShouldDo(servings, integration_method=oven):
    """
        Make chicken soup
    """
    # Comments:
    # They are important. With some syntax highlighting, the comments are
    # the first thing a new programmer will look for. Therefore, they should
    # motivate your algorithm, outline it, and break it up into stages.
    # You can MAKE IT FEEL AS IF YOU ARE READING TEXT, interspersing code
    # amongst the text.
    #
    # Algorithm overview:
    # To make chicken soup, we will acquire chicken broth and some noodles.
    # Preprocessing ingredients is done to optimize cooking time. Then we 
    # will output in SOUP format via stdout.
    #
    # BEGIN ALGORITHM
    #
    # Preprocessing:
    # 1. Thaw chicken broth.
    broth = chickenstore.deserialize()

    # 2. Mix with noodles
    if not noodles in cache:
        with begin_transaction(locals=poulty) as t:
            cache[noodles] = t.buy(noodles)  # get from local store
    noodles = cache[noodles]

    # 3. Perform 4th-order Runge-Kutta numerical integration
    import kitchensink import *  # FIXME: poor form, better to from kitchensink import pan at beginning
    result = boilerplate.RK4(broth `semidirect_product` noodles)

    # 4. Serve hot
    log.debug('attempting to serve')
    return result
    log.debug('server successful')

also see http://en.wikipedia.org/wiki/Literate_programming#Example

I've also heard that this is what http://en.wikipedia.org/wiki/Aspect-oriented_programming attempts to help with, though I haven't really looked into it. (It just seems to be a fancy way of saying "put your optimizations and your debugs and your other junk outside of the function you're writing".)

ninjagecko
  • 88,546
  • 24
  • 137
  • 145
  • Aspect Oriented is not fast, it has overloads, also in C I don't know any library to help OP, if it was c++ it was reasonable. – Saeed Amiri Sep 05 '11 at 06:53