File: timeIO.py

"""
test various read and write file I/O modes for speed
in the version of Python that is running this script;
runs most common and valid read/write coding patterns;
tests ascii and binary, but not wide-char unicode files;
printed results can be parsed later for comparisons
"""


######################################################################
# generic timer
######################################################################

import time

def timeOnce(func, *args):
    start = time.clock()
    func(*args)                                 # ignore any return value
    return time.clock() - start

def timerAvg(func, *args):
    warmcache = timeOnce(func, *args)
    reps   = 3
    runavg = 0                                  # take average of 3 runs
    for i in range(reps):
        runavg += timeOnce(func, *args)
    return runavg / reps                        # CHANGED: take low = "best"


# The following may be a bit better, but isn't directly comparable

def timerBest(func, *args):
    warmcache = timeOnce(func, *args)           # make sure disk caches active
    reps = 3
    runs = []                                   # take min of N runs
    for i in range(reps):
        runs.append(timeOnce(func, *args))
    return min(runs)

timer = timerBest                               # CHANGED


######################################################################
# file read tests
######################################################################

#=====================================================================
# all of the following are probably valid use cases for 2.6 and 3.0,
# though lines/text and blocks/binary combos seem more typical in 3.0
# (programs will pick str xor bytes for text or binary data in 3.0); 
# truly binary files can only be read in binary mode in 3.0, because 
# they cannot be decoded into characters in text mode, and it makes 
# no sense to read truly binary files by lines: they have no lineends;
# the allAtOnce modes may fail for pathologically large files;
#
# 3.0 has str + bytes; 2.6 has just str, plus binary files
# open mode default = 'r' = 'rt' in 3.0, and 'r' in 2.6
# (both mean text mode input when the mode argument is omitted)
#=====================================================================


blocksize = 1024 * 32


def read_byLines_textMode(filename):          
    for line in open(filename):               # 2.6 text mode returns str, does not decode (use codecs.open)
        pass                                  # 3.0 text mode returns str, after decoding content

def read_byLines_binaryMode(filename):        # less common in 3.0?
    for line in open(filename, 'rb'):         # 2.6 binary mode returns str,   does not decode
        pass                                  # 3.0 binary mode returns bytes, does not decode


def read_byBlocks_textMode(filename, size=blocksize):
    f = open(filename)
    while True:                                # less common in 3.0?
        block = f.read(size)
        if not block: break

def read_byBlocks_binaryMode(filename, size=blocksize):
    f = open(filename, 'rb')
    while True:
        block = f.read(size)
        if not block: break


def read_allAtOnce_textMode(filename):         # not for very large files
    text = open(filename).read()              

def read_allAtOnce_binaryMode(filename):       # not for very large files
    text = open(filename, 'rb').read()


######################################################################
# file write tests
######################################################################

#=====================================================================
# all the following work, but tests "write_byLines_binaryMode" and
# "write_byBlocks_textMode" are probably invalid use cases for 3.0,
# where programs are more likely to pick str xor bytes for text
# or binary data, and not convert to str or bytes just to write in 
# text or binary mode; portability issues: 3.0's encoding arg required 
# by 3.0's bytes() converter is not allowed in 2.6's bytes(), and 
# 2.6's str.decode() creates a unicode object which adds some cost;
#
# hoist set-up ops out to avoid charging to test funcs
# 'xx' / b'xx' are str / bytes in 3.0, both are str in 2.6
# 'xx' == b'xx' and bytes(x) == str(X) in 2.6
# 2.6: str is a seq of bytes, unicode a distinct type
# 3.0: str is seq of Unicode chars, bytes is seq of ints
#=====================================================================


oneMeg   = 1024 * 1024
halfMeg  = oneMeg // 2               # use truncating division in both 2.6 and 3.0
repsList = list(range(halfMeg))      # force list in both 2.6 and 3.0

aLine    = '*' * 49 + '\n'           # 25M in file ((50+\r?) * ((1024 * 1024) / 2))
aBlock   = b'1\x0234\x05' * 10       # 25M in file ((5 * 10) * (1M / 2))
aFileStr = aLine  * halfMeg          # 25M characters
aFileBin = aBlock * halfMeg          # 25M bytes

print ('\nOutput data sizes: %s %s %s %s %s' % 
      (len(repsList), len(aLine), len(aBlock), len(aFileStr), len(aFileBin)) )


def write_byLines_textMode(filename):       # writing by blocks in text mode is similar
    file = open(filename, 'w')              # 3.0 text mode takes str, encodes content, xlates newlines 
    for i in repsList:                      # 2.6 text mode takes str, xlates newlines
        file.write(aLine)                   # 3.0 text mode takes open() flag to control lineends
    file.close()                            

def write_byLines_binaryMode(filename):     # less common in 3.0?
    file = open(filename, 'wb')             # 3.0 binary mode takes bytes, does not decode or xlate
    for i in repsList:                      # 2.6 binary mode takes str,   does not xlate newlines
        file.write(aLine.encode())          # encode() makes bytes in 3.0, same str in 2.6
    file.close()                            


def write_byBlocks_textMode(filename):      # less common in 3.0?
    file = open(filename, 'w')              # decode() makes str in 3.0, unicode in 2.6    
    for i in repsList:                      
        file.write(aBlock.decode())         
    file.close()

def write_byBlocks_binaryMode(filename):    # writing by lines in binary mode is similar
    file = open(filename, 'wb')             
    for i in repsList:                      
        file.write(aBlock)                  
    file.close()                
                                            
                                            
def write_allAtOnce_textMode(filename):     # not for very large files 
    open(filename, 'w').write(aFileStr)

def write_allAtOnce_binaryMode(filename):   # not for very large files
    open(filename, 'wb').write(aFileBin) 


######################################################################
# run, collect test data for Python running me
######################################################################

def timePython():
    import sys, os
    outputfile = 'timeIO.out'                      # hard-code: I create this
    textfile, binaryfile = sys.argv[1:3]           # input files vary, command line 

    tests = {textfile:   (read_byLines_textMode,   
                          read_byLines_binaryMode,     # less common in 3.0?
                          read_byBlocks_textMode,      # less common in 3.0?
                          read_byBlocks_binaryMode,
                          read_allAtOnce_textMode,     # not for very large files
                          read_allAtOnce_binaryMode),  # not for very large files

             binaryfile: (read_byBlocks_binaryMode,    # other read modes not valid,
                          read_allAtOnce_binaryMode),  # for truly binary data files

             outputfile: (write_byLines_textMode,
                          write_byLines_binaryMode,    # less common in 3.0?
                          write_byBlocks_textMode,     # less common in 3.0?
                          write_byBlocks_binaryMode,
                          write_allAtOnce_textMode,    # not for very large files
                          write_allAtOnce_binaryMode)  # not for very large files
             }

    for filename in (textfile, binaryfile, outputfile):
        filesize = os.path.getsize(filename) if os.path.exists(filename) else '0'      # CHANGED
        version  = sys.version.split()[0]
        print('\n[Python {0}: {1}, {2} bytes]'.format(version, filename, filesize))

        for func in tests[filename]:
            try:
                testtime = timer(func, filename)
            except:
                print('%-26s => %s, %s' % (func.__name__, '*fail*', sys.exc_info()[0]))
            else:
                # int/int=float+remainder in 3.0, but not 2.6
                filemegs = float(filesize) / oneMeg
                testid   = '%-26s (%s=%.2fM)' % (func.__name__, filename, filemegs)
                print('%-46s  => %f' % (testid, testtime))

if __name__ == '__main__': 
    timePython()   # the version running me



[Home page] Books Code Blog Python Author Train Find ©M.Lutz