Steven Caron, OBJ Files, Sexy Bits and Waste

March 1st, 2008 by Patrick Boucher - Viewed 24494 times -




What do all these things have in common?

The weird wirings in my brain. That’s what.

Back to the beginning

At the end of last October Steven Caron sent me a plugin he wrote that allows an XSI user to drag and drop .obj files into the interface and have them import automatically according to settings in a custom preference. I was a bit swamped at the time putting up the infrastructure for a new VFX department at my new workplace. And then I totally forgot about it… Sorry Steven.

This past week Steven politely reminded me of my omission so I ran back to my email archives and installed it. Neat piece of work.

I didn’t want to just post up the tool as I don’t really see this blog being about tool distribution but about how said tools work and the neat tricks they use. With that philosophy in mind I cracked open Steven’s tool to dig out some of its sexier bits and maybe demystify them for the audience.

To my surprise there weren’t any sexy bits. Please don’t get me wrong, the tool works superbly, how much more intuitive can you get than drag and drop. The tool is also extremely well written, concise and straightforward. There just aren’t any weird tricks or convoluted syntax or things that would generally have you scratching your head.

Then I noticed this in his version trapping code (yes folks, the drag and drop event is new in XSI 6.5):

27
28
29
30
31
32
33
34
35
if int( tXSIVersion[0] ) < 6:
    xsiPrint( xsi.Version() + " doesn't support the 'siOnDragAndDrop' event!" )
    xsi.UnloadPlugin( in_reg.name, True )
    return False
else:
    if int( tXSIVersion[1] ) < 5:
        xsiPrint( xsi.Version() + " doesn't support the 'siOnDragAndDrop' event!" )
        xsi.UnloadPlugin( in_reg.name, True )
        return False

About the waste part

Bare with me…

Last week I was reading a really neat article in Wired by Chris Anderson entitled “Free! Why $0.00 Is the Future of Business”. I invite you to read it, especially the section beginning on page two: Waste and Waste Again.

…So if in the seventies you had to be really tight with your algorithms and optimizations because cpu cycles were so scarce, today I guess we can really waste them. Just the other day I setup an 8 core MacPro with BootCamp, XP x64 and XSI. Whatever I did to the machine, I swear, I could hear it yawn because it was bored.

Which brings me back to Steven’s code. Could it have been written like so:

27
28
29
30
if int( tXSIVersion[0] ) < 6 or int( tXSIVersion[1] ) < 5:
    xsiPrint( xsi.Version() + " doesn't support the 'siOnDragAndDrop' event!" )
    xsi.UnloadPlugin( in_reg.name, True )
    return False

Is it more or less readable by a programmer?
It probably executes in a few cycles less but is it worth it?
Python is interpreted so if the parser goes through this version quicker, is it worty of any mention?
Is it more maintainable?

This example is really simple but in a bigger project, or with more complex cases, should we really be worrying if today transistors and cpu cycles are so cheap as to not even matter anymore

When I'm stuck on a piece of code or when I hit that time of the afternoon where most of my energy is diverted to digesting, I'll often reread my code and tighten it, remove redundancy, put in functions instead of copying a few lines in two locations, consolidate if statements, etc... One of my colleagues used to tell me that "Premature optimization is the root of all evil."

Honestly, if I look back at a good proportion of the code I have written, it either executes in a blink of an eye on today's computers or it sits there waiting 90% of the time for user interaction. Should we, as technical XSI users, XSI scripters or TDs, even worry about optimization from a performance standpoint or a maintainability standpoint? What can be considered a good optimization and what should be considered a bad one?

And now that you're really wondering what I'm rambling about, I'll shut up and let you download Steven's addon.

Have a good weekend.

20 Responses to “Steven Caron, OBJ Files, Sexy Bits and Waste”

  1. Steven Caron says:

    thanks for posting this patrick! i appreciate the peer review of my code also, i dont get that luxury with my personal projects.

  2. Yay! The blog is back :-)

    Actually, I hate to be the one to point it out, but both code segments will fail for any version that has a minor version less then 5. That includes 7.0, 7.1, etc. Whoops!

    I’d probably test for it like this:

    if int( tXSIVersion[0] )

  3. Gah. The html didn’t like the less than sign.

    It should have looked like this..

    1
    
    if int( tXSIVersion[0] ) < 6 or (int( tXSIVersion[0] ) = 6 and int( tXSIVersion[1] ) < 5):
  4. To address the main point of the article, I think the revised form is definitely a better style. When I read the first code snippet, I had to read it again to see if there was a difference between the bodies of the two ‘if’ statements, which of course there isn’t. So for me, it’s more about readability than anything.

    I wouldn’t even put it into the category of premature optimisation. The following I would consider to be premature optimisation, and it’s not something I’d code on a first pass, but would introduce during factorising. It improves readability, plus it avoids a list lookup, albeit at the cost of having two temporary variables in memory.

    1
    2
    3
    
    majorver = int( tXSIVersion[0] )
    minorver = int( tXSIVersion[1] )
    if  majorver < 6 or (majorver = 6 and minorver < 5):

    For me, whether TDs take time to optimise and factorise their code or not depends on what the code is for. If it’s just a quick script to batch process something, then no, it’s probably not worth it. If it’s a SCOP, then yes, performance is everything.

    More crucially, if a TD is writing code for a pipeline, then clarity is incredibly important. TDs come and go, yet their code often remains. If it hasn’t been left in a maintainable state then it can lead to misunderstandings and new bugs. Worse still, a new recruit may decide to rewrite it, because it’s less effort than trying to understand what it does.

    That’s why, for me, I think readability is key. If something needs to be optimised to the point where the code does not express it’s purpose, then comments are essential to instruct a newcomer as to what is happening.

  5. Andy:

    Touché on comment #2.

    And you bring up nice points on comment #3. I can only beg for forgiveness when I think of the person that might today be playing around in code I’ve left behind… ;)

  6. Hehe. Yep, we’re all guilty of that :-)

  7. Steven Caron says:

    i have made a jscript version for the non python adopters…

    http://www.steven-caron.com/downloads/tools/objDnD_js.xsiaddon

    i have the plugins hosted at my site, but patrick is welcome to host them here too.

  8. I consider performance/optimization and maintainability to be two very distinct aspects of programming.

    I approach performance/optimization mainly from a business perspective. Since it’s the user that’s going to run my script, the experience for the user has to be as pleasant as possible. As far as speed is concerned, that means as fast as possible. Any excuse is good for someone to go out for another cigarette, if you see what I mean (in my case, it’s surfing the internet for a much longer time than the actual execution, as I won’t stop reading in the middle of a paragraph or in the middle of writing a post on a forum). In a sentence, the longer an artist waits, the more expensive it is for the studio. There are already countless other reasons to wait, do not make matter worse if you can avoid it! I think any reasonable step a coder can take to reduce the execution to a minimum is good. There is no point in wasting cpu cycles just because we can!

    In my case I’m hardly ever too premature. Very often my code will go through many changes just to better organize it, and sometimes these changes will take me several hours (or even several days) to implement. Sometimes I realize I wrote a function that is called only once, so I put it in the main code, sometimes I realize that I have a wacky combination of if/else, so I change it to a dictionary look (sort of like a switch), etc. It is something that is done at all stages of the development, not just at the end. I optimize as I see fit.

    As for maintainability, I agree with Andy. I don’t see optimization and maintainability being incompatible. I go to great lengths to comment my code. Most functions and classes have a doc string, with arguments and return value described. I write the doc strings at the end of the development, because functions are subject to many changes. Annoying to re-write the doc string every time. Above lines that seem to do weird things, I put multiline comments.

    In the case of highly sensitive code, like pipeline framework, I might even write a proper documentation, detailing each class, method, attribute, as well as a walk-through of the architecture. Of course this has to be done late in the development, for the same reason as doc strings. Maintaining documentation can be a lot of work. Again this documentation is not just for others, but to me as well.

    In the case of the example above, I’d write it this way:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    # Form version number that includes only the major and minor version numbers
    sVersion = '.'.join( xsi.version().split('.')[0:2] )
     
    # Convert this number to float, round it to 2 digits to be sure
    fVersion = round( float(sVersion), 2 )
     
    if not fVersion &gt;= 6.5:
    	xsi.logmessage( '%s does not support the "siOnDragAndDrop" event!' % (sVersion,) )
    	xsi.UnloadPlugin( in_reg.name, True )
    	return False

    So all functions all called only once. Only one comparison operation.

  9. Ok, first off I think we’ll all agree I’m being extremely stupid testing this but I had to.

    I was seriously having my doubts as to the speed of Bernard’s approach, and if it wasn’t quicker, IMHO, Andy’s code is much more readable. My doubts lied in three things:

    - The join just doesn’t exist in the other versions.
    - I figure a float conversion must be slower than an int conversion – gotta check for that period.
    - And lastly the round that doesn’t exits in the other versions.

    I included Steven’s version as well as mine in the tests even if we all agree that they don’t even work for letting through a ’7.0′ version string incorrectly.

    Here are the results I got for 10000000 iterations of our code bits. Yeah, ten million, that’s the stupid part of all this!

    Testing with version string '6.0'
    ---------- o -----------
           steven 1.64961721897
          patrick 1.62311317921
             andy 1.76714420319
    andyNoTempVar 1.1488224268
          bernard 2.41005961895
    
    
    Testing with version string '7.0'
    ---------- o -----------
           steven 1.6822024107
          patrick 1.66664714813
             andy 1.73656346798
    andyNoTempVar 1.17056546211
          bernard 2.48615708351
    
    
    Testing with version string '6.5'
    ---------- o -----------
           steven 1.64540979862
          patrick 1.63079109192
             andy 1.73360862732
    andyNoTempVar 1.15994520187
          bernard 2.59466853142

    The andyNoTempVar is Andy’s code without the two temporary variables. Duh. And as it turns out, it is the clear winner.

  10. This is what I used to test. If anyone finds problems with this, please point them out.
    Yes, I’m timing a range loop, a function call and variable passing but they are the same in all tests so they kind of cancel out. The time differences we notice are due to the contents of the various functions.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    
    import time
    avgIter = 10
    iter = 1000000
     
    def timeit(version, funcs):
        results = dict(zip([f.__name__ for f in funcs], [0.0] * len(funcs)))
     
        for avgi in range(avgIter):
            for func in funcs:
                start = time.time()
                for i in range(iter):
                    func(version)
                duration = time.time() - start
                results[func.__name__] += duration
                print '%s - %s: %f' % (func.__name__, version, duration)
        print '----- o -----'
        for k, v in results.iteritems():
            print k, v/avgIter
     
    def steven(version):
        tXSIVersion = version.split('.')
        if int( tXSIVersion[0] ) < 6:
            pass
        else:
            if int( tXSIVersion[1] ) < 5:
                pass
     
    def patrick(version):
        tXSIVersion = version.split('.')
        if int( tXSIVersion[0] ) < 6 or int( tXSIVersion[1] ) < 5:
            pass
     
    def andy(version):
        tXSIVersion = version.split('.')
        majorver = int( tXSIVersion[0] )
        minorver = int( tXSIVersion[1] )
        if majorver < 6 or (majorver == 6 and minorver < 5):
            pass
     
    def andyNoTempVar(version):
        tXSIVersion = version.split('.')
        if tXSIVersion[0] < 6 or (tXSIVersion[0] == 6 and tXSIVersion[1] < 5):
            pass
     
    def bernard(version):
        # Form version number that includes only the major and minor version numbers
        sVersion = '.'.join( version.split('.')[0:2] )
        # Convert this number to float, round it to 2 digits to be sure
        fVersion = round( float(sVersion), 2 )
     
        if not fVersion >= 6.5:
            pass
     
    if __name__ == '__main__':
        funcs = [steven, patrick, andy, andyNoTempVar, bernard]
        for v in ['6.0', '7.0', '6.5']:
            timeit(v, funcs)
  11. Nice! Thanks a lot for this!

  12. Oops.

    The andyNoTempVar function should have read:

    1
    2
    3
    4
    
    def andyNoTempVar(version):
        tXSIVersion = version.split('.')
        if int(tXSIVersion[0]) < 6 or (int(tXSIVersion[0]) == 6 and int(tXSIVersion[1]) < 5):
            pass

    I’d forgotten the explicit int conversions. It winds up being slower than the temp variable version in function andy. So, the temp var version, for having one less conversion creeps ahead in the standings of two tests. New numbers, again with ten million runs:

    Testing with version string '6.0'
    ---------- o -----------
           steven 1.6405577898
          patrick 1.68582975864
             andy 1.7813429594
    andyNoTempVar 2.06077568531
          bernard 2.4824331522
    
    
    Testing with version string '7.0'
    ---------- o -----------
           steven 1.62016811371
          patrick 1.56284923553
             andy 1.61941108704
    andyNoTempVar 1.53076636791
          bernard 2.48117063046
    
    
    Testing with version string '6.5'
    ---------- o -----------
           steven 1.61195776463
          patrick 1.66966564655
             andy 1.77969810963
    andyNoTempVar 2.00582532883
          bernard 2.63731787205
  13. I have to say that I like Bernards method for the elegance of the procedure. I think it is a little harder to read and understand at first glance, but that’s why he’s been good enough to put nice big comments there :-D

    Thanks for doing the testing Patrick.

  14. Steven Caron says:

    sweet… i was feeling all dumb about my if/else if setup and it runs with acceptable speed.

    i do find bernard’s harder to read and i dont think i would opt for his code because code maintenance is often shared (your comments go a long way and make up for it) between developers and when i leave the code for a few months and come back i dont want to be scratching my head because i didn’t comment :)

    fun stuff! true purpose of xsiblog!

    s

  15. Hey guys, check this out.

    I thought that all we were interested in is the version to be 6.5 or more. That’s it. The number of digits after the decimal doesn’t matter.

    So I thought that I could simply take the 3 first characters, convert them to float, and test against that. If the XSI version is ever going to be above 9, it doesn’t matter, because at 10 we know for sure we’re beyond 6.5.

    So wrote this function. No split, no join, no round, no conversion to int, only one comparison.

    1
    2
    3
    4
    
    def bernardFloat(version):
        fVersion = float( version[:3] )
        if not fVersion >= 6.5:
            pass

    (let me know if I’ve overlooked something!)

    And then I ran Patrick’s test code (although I only did 1,000,000 iterations, as my computer is slower than his). My results:

    Testing with version string '6.0'
    ----- o -----
    steven          0.359000039101
    patrick         0.35649998188
    andy            0.375
    andyNoTempVar   0.453299975395
    bernard         0.487199997902
    bernardList     0.462599992752
    bernardFloat    0.214199995995
    
    Testing with version string '6.5'
    ----- o -----
    patrick         0.357999968529
    steven          0.356000041962
    andy            0.364100003242
    andyNoTempVar   0.359200000763
    bernard         0.484500002861
    bernardList     0.453199982643
    bernardFloat    0.21400001049
    
    Testing with version string '7.0'
    ----- o -----
    steven          0.354699969292
    patrick         0.357800030708
    andy            0.373399996758
    andyNoTempVar   0.453199982643
    bernard         0.521800017357
    bernardList     0.465700006485
    bernardFloat    0.25

    You’ll also notice this entry: bernardList. I wanted to see how things go if I converted the number to integers via a list comprehension:

    1
    2
    3
    4
    
    def bernardList(version):
        aVersion = [int(sNumber) for sNumber in version.split('.')]
        if (aVersion[0] < 6) or (aVersion[0] == 6 and aVersion[1] < 5):
            pass

    I love this stuff!

    Bernard

  16. Me again,

    I reworked the function a little bit. In bernardFloat(), I change the not … >= to a simple <.
    I also added bernardFloat2(), which does the same thing except creating a temp variable.

    1
    2
    3
    4
    5
    6
    7
    8
    
    def bernardFloat(version):
        fVersion = float( version[:3] )
        if fVersion < 6.5:
            pass
     
    def bernardFloat2(version):
        if float(version[:3]) < 6.5:
            pass

    New results:

    Testing with version string '6.0'
    ----- o -----
    steven          0.359300041199
    patrick         0.36099998951
    andy            0.378199982643
    andyNoTempVar   0.454500007629
    bernard         0.490700006485
    bernardList     0.468799996376
    bernardFloat    0.212400007248
    bernardFloat2   0.209499979019
    
    Testing with version string '6.5'
    ----- o -----
    steven          0.361100029945
    patrick         0.359200000763
    andy            0.378199982643
    andyNoTempVar   0.459399986267
    bernard         0.525099992752
    bernardList     0.467100024223
    bernardFloat    0.25
    bernardFloat2   0.245299983025
    
    Testing with version string '7.0'
    ----- o -----
    steven          0.362500047684
    patrick         0.360899996758
    andy            0.365499973297
    andyNoTempVar   0.363999962807
    bernard         0.489200019836
    bernardList     0.453099989891
    bernardFloat    0.214200091362
    bernardFloat2   0.207699918747

    So skipping the temp variable is even faster!

  17. Ciaran Moloney says:

    Thanks, folks!
    As a novice scripter, I found this all to be very informative.

  18. Stumbled onto this article describing Python optimization this morning.
    Thought it was appropriate.

    http://www.python.org/doc/essays/list2str.html

  19. Very interesting link that (for nerds like me anyway). Thanks :-)

  20. Rob Chapman says:

    ok so this is definitely a Softimage Bad Ass comments section. I stopped listening at ‘Python if int blah’ unfortunately, but maybe give me another 4 years? ran Ciaran’s python prt2icecache.py from a Naiad export today so am allowed to comment? :)