views:

310

answers:

3

I use the following code to log a map, it is fast when it only contains zeroes, but as soon as there is actual data in the map it becomes unbearably slow... Is there any way to do this faster?

log_file = open('testfile', 'w')
for i, x in ((i, start + i * interval) for i in range(length)):
    log_file.write('%-5d %8.3f %13g %13g %13g %13g %13g %13g\n' % (i, x,
        map[0][i], map[1][i], map[2][i], map[3][i], map[4][i], map[5][i]))
+2  A: 

First I checked % against backquoting. % is faster. THen I checked % (tuple) against 'string'.format(). An initial bug made me think it was faster. But no. % is faster.

So, you are already doing your massive pile of float-to-string conversions the fastest way you can do it in Python.

The Demo code below is ugly demo code. Please don't lecture me on xrange versus range or other pedantry. KThxBye.

My ad-hoc and highly unscientific testing indicates that (a) % (1.234,) operations on Python 2.5 on linux is faster than % (1.234,...) operation Python 2.6 on linux, for the test code below, with the proviso that the attempt to use 'string'.format() won't work on python versions before 2.6. And so on.

# this code should never be used in production.
# should work on linux and windows now.

import random
import timeit
import os
import tempfile


start = 0
interval = 0.1

amap = [] # list of lists
tmap = [] # list of tuples

def r():
    return random.random()*500

for i in xrange(0,10000):
        amap.append ( [r(),r(),r(),r(),r(),r()] )

for i in xrange(0,10000):
        tmap.append ( (r(),r(),r(),r(),r(),r()) )




def testme_percent():
    log_file = tempfile.TemporaryFile()
    try:
        for qmap in amap:
            s = '%g %g %g %g %g %g \n' % (qmap[0], qmap[1], qmap[2], qmap[3], qmap[4], qmap[5]) 
            log_file.write( s)
    finally:
        log_file.close();

def testme_tuple_percent():
    log_file = tempfile.TemporaryFile()
    try:    
        for qtup in tmap:
            s = '%g %g %g %g %g %g \n' % qtup
            log_file.write( s );
    finally:
        log_file.close();

def testme_backquotes_rule_yeah_baby():
    log_file = tempfile.TemporaryFile()
    try:
        for qmap in amap:
            s = `qmap`+'\n'
            log_file.write( s );
    finally:
        log_file.close();        

def testme_the_new_way_to_format():
    log_file = tempfile.TemporaryFile()
    try:
        for qmap in amap:
            s = '{0} {1} {2} {3} {4} {5} \n'.format(qmap[0], qmap[1], qmap[2], qmap[3], qmap[4], qmap[5]) 
            log_file.write( s );
    finally:
        log_file.close();

# python 2.5 helper
default_number = 50 
def _xtimeit(stmt="pass",  timer=timeit.default_timer,
           number=default_number):
    """quick and dirty"""
    if stmt<>"pass":
        stmtcall = stmt+"()"
        ssetup = "from __main__ import "+stmt
    else:
        stmtcall = stmt
        ssetup = "pass"
    t = timeit.Timer(stmtcall,setup=ssetup)
    try:
      return t.timeit(number)
    except:
      t.print_exc()


# no formatting operation in testme2

print "now timing variations on a theme"

#times = []
#for i in range(0,10):

n0 = _xtimeit( "pass",number=50)
print "pass = ",n0

n1 = _xtimeit( "testme_percent",number=50);
print "old style % formatting=",n1

n2 = _xtimeit( "testme_tuple_percent",number=50);
print "old style % formatting with tuples=",n2

n3 = _xtimeit( "testme_backquotes_rule_yeah_baby",number=50);
print "backquotes=",n3

n4 = _xtimeit( "testme_the_new_way_to_format",number=50);
print "new str.format conversion=",n4


#        times.append( n);




print "done"    

I think you could optimize your code by building your TUPLES of floats somewhere else, wherever you built that map, in the first place, build your tuple list, and then applying the fmt_string % tuple this way:

for tup in mytups:
    log_file.write( fmt_str % tup )

I was able to shave the 8.7 seconds down to 8.5 seconds by dropping the making-a-tuple part out of the for loop. Which ain't much. The big boy there is floating point formatting, which I believe is always going to be expensive.

Alternative:

Have you considered NOT writing such huge logs as text, and instead, saving them using the fastest "persistence" method available, and then writing a short utility to dump them to text, when needed? Some people use NumPy with very large numeric data sets, and it does not seem they would use a line-by-line dump to store their stuff. See:

http://thsant.blogspot.com/2007/11/saving-numpy-arrays-which-is-fastest.html

Warren P
This approach lumps everything about what's going on together to time, which isn't the most effective way to figure out how time varies with a change. You typically want to isolate just the operations you're looking at.
Mike Graham
The `tempfile` module provides a reliable way to make temporary files without introducing unnecessary interference with existing files or platform dependency. Also, `if os.path.exists(...)` introduces an unnecessary race condition over just calling `os.remove` and catching the error and is generally worse form because of it. Also typically bad form is relying on `close` to be called manually; if you actually want to make sure a file gets closed, use a context manager (`with open(afilename1, 'w') as log_file:`). The current code will not close the file if there is an exception raised.
Mike Graham
Using backticks to call `repr` is deprecated. It is hard to read and for some people hard to type. All three of your solutions do different things, but this one is especially different from the others.
Mike Graham
This is just my hackery in five seconds. Nobody should use this code. In fact it won't work on Unix, at all. :-)
Warren P
Incidentally, I wanted a total value for "how long does it take to make such a file" (about 8-10 seconds) and then "take away the floating point formatting", as well as trying strings instead of floats. This effectively isolated the floating point conversion while maintaining some kind of overall context.
Warren P
`(qmap[0], qmap[1], qmap[2], qmap[3], qmap[4], qmap[5])` seems like a poor way to write `tuple(qmap)` to me (or just to start with tuples). Similarly, `.format(qmap[0], qmap[1], qmap[2], qmap[3], qmap[4], qmap[5])` seems like a worse way to write `.format(*qmap)`. Writing out loops by hand rather than programatically is usually a sign you're doing something wrong. In this particular case, the lookups involved are a little different and will unfortunately affect the timing. In OP's case the lookups were completely different and are what *actually* let to the slowdown, not the string formatting.
Mike Graham
I would probably replace `smap = []; for i in range(0, 10000): smap.append(['a', 'x', 'y', 'e', '1', '2', '3'])` with code like `smap = [['a', 'x', 'y', 'e', '1', '2', '3'] for i in xrange(10000)]`, which utilises list comprehensions for the `foo = []: for...foo.append...` boilerplate, doesn't build the unnecessary `range` list, and uses the arguments of (`x`)`range` as expected for the case of performing an operation n times.
Mike Graham
Mike, this is not production code, it is a minimal way of timing the original question's assertions in the context of writing a file. Six comments, Mike. Lighten up.
Warren P
Posting "Nobody should use this code" code to a site like this, from where people constantly copy code, is usually a precarious idea. You never know if someone will know better.
Mike Graham
You can go ahead and write a 200 line production ready sample. All I'm doing is comparing several different times, some of which use % and some of which do not. All involve a transition from data that is not in string type, to data which is in string type. If someone wishes to copy the code off here, they are welcome to. And they can do so with no warranty, like everything else. It's not your problem, or mine, if demo code is DEMO CODE.
Warren P
Your rationale regarding how to time the various options goes against the typical standards used in benchmarking and performance testing and—really—experimental science in general. You don't try to look at the big picture to be precise about one detail; you try to isolate this detail as much as possible. In this case you mash everything together so much the results you get incorrectly conflate the times taken by two tasks, one of which you claim to be timing and the other of which also varies through your options and was the original culprit in the bottleneck.
Mike Graham
@Warren P, I am not suggesting you write longer code. Most of my suggestions would have made your code shorter (but hopefully as clear or clearer). I am suggesting you not post code that you would yourself call unusably bad and I am suggesting you post code that uses techniques that can speak to the conclusions you say you reach, which this code does not.
Mike Graham
I am not reaching conclusions, merely pointing out that removing % did not significantly alter the time of the file write. Would you like to time the % without timing file writes? Because that's cool. But that's not what I was trying to do.
Warren P
Further interesting results: Python 2.5 is faster than Python 2.6.And Python 3.1 is faster than both of them at the now-deprecated (in the docs) '%' operator.
Warren P
+3  A: 

I suggest you run your code using the cProfile module and postprocess the results as described on http://docs.python.org/library/profile.html . This will let you know exactly how much time is spent in the call to str.__mod__ for the string formatting and how much is spent doing other things, like writing the file and doing the __getitem__ lookups for map[0][i] and such.

Mike Graham
A: 

Without wishing to wade into the optimize-this-code morass, I would have written the code more like this:

log_file = open('testfile', 'w')
x = start
map_iter = zip(range(length), map[0], map[1], map[2], map[3], map[4], map[5])
fmt = '%-5d %8.3f %13g %13g %13g %13g %13g %13g\n'
for i, m0, m1, m2, m3, m4, m5 in mapiter:
    s = fmt % (i, x, m0, m1, m2, m3, m4, m5)
    log_file.write(s)
    x += interval

But I will weigh in with the recommendation that you not name variables after python builtins, like map.

hughdbrown