pythonperformancetemporary-filesstringiocstringio

Real file objects slower than StringIO and cStringIO?


StringIO has the following notes in its code:

Notes:
- Using a real file is often faster (but less convenient).
- There's also a much faster implementation in C, called cStringIO, but
  it's not subclassable.

The "real file is often faster" line seemed really odd to me: how could writing to disk beat writing to memory? I tried profiling these different cases and got results that contradict these docs, as well as the answer to this question. This other question does explain why cStringIO is slower under some circumstances, though I'm not doing any concatenating here. The test writes a given amount of data to a file, then seeks to the beginning and reads it back out. On the "new" tests, I created a new object each time, and on the "same" ones I truncate and reuse the same object for each repetition to rule out that source of overhead. That overhead mattered for using tempfiles with small data sizes but not large ones.

Code is here.

Using 1000 passes with size 1.0KiB
New StringIO:   0.0026 0.0025 0.0034
Same StringIO:  0.0026 0.0023 0.0030
New cStringIO:  0.0009 0.0010 0.0008
Same cStringIO: 0.0009 0.0009 0.0009
New tempfile:   0.0679 0.0554 0.0542
Same tempfile:  0.0069 0.0064 0.0070
==============================================================
Using 1000 passes with size 100.0KiB
New StringIO:   0.0093 0.0099 0.0108
Same StringIO:  0.0109 0.0090 0.0086
New cStringIO:  0.0130 0.0139 0.0120
Same cStringIO: 0.0118 0.0115 0.0124
New tempfile:   0.1006 0.0905 0.0899
Same tempfile:  0.0573 0.0526 0.0523
==============================================================
Using 1000 passes with size 1.0MiB
New StringIO:   0.0727 0.0700 0.0717
Same StringIO:  0.0740 0.0735 0.0712
New cStringIO:  0.1484 0.1399 0.1470
Same cStringIO: 0.1493 0.1393 0.1465
New tempfile:   0.6576 0.6750 0.6821
Same tempfile:  0.5951 0.5870 0.5678
==============================================================
Using 1000 passes with size 10.0MiB
New StringIO:   1.0965 1.1129 1.1079
Same StringIO:  1.1206 1.2979 1.1932
New cStringIO:  2.2532 2.2162 2.2482
Same cStringIO: 2.2624 2.2225 2.2377
New tempfile:   6.8350 6.7924 6.8481
Same tempfile:  6.8424 7.8114 7.8404
==============================================================

The two StringIO implementations were pretty comparable, though cStringIO slowed down significantly for large data sizes. But the tempfile.TemporaryFile always took 3 times as long as the slowest StringIO.


Solution

  • It all depends on what "often" means. StringIO is implemented by keeping your writes in a list and then joining the list to a string on read. Your test case - a series of writes followed by a read - is its best scenario. If I tweak the test case to do 50 random writes/reads in the file, then cStringIO tends to win with the file system in second place.

    The comment seems to reflect a system programmer's bias to let the c libraries plus operating system do file system things because its hard to guess in a general sense what performs best under all conditions.

    def write_and_read_test_data(flo):
        fsize = len(closure['test_data'])
        flo.write(closure['test_data'])
        for _ in range(50):
            flo.seek(random.randint(0, fsize-1))
            flo.write('x')
            flo.read(1)
        flo.seek(0)
        closure['output'] = flo.read()
    

    The 10meg test case took longer than my attention span...

    Using 1000 passes with size 1.0KiB
    New StringIO:   0.9551 0.9467 0.9366
    Same StringIO:  0.9252 0.9228 0.9207
    New cStringIO:  0.3274 0.3280 0.3251
    Same cStringIO: 0.3182 0.3231 0.3280
    New tempfile:   1.1833 1.1853 1.1650
    Same tempfile:  0.9563 0.9414 0.9504
    ==============================================================
    Using 1000 passes with size 100.0KiB
    New StringIO:   5.6253 5.6589 5.6025
    Same StringIO:  5.5799 5.5608 5.5589
    New cStringIO:  0.4157 0.4133 0.4140
    Same cStringIO: 0.4078 0.4076 0.4088
    New tempfile:   2.0420 2.0391 2.0408
    Same tempfile:  1.5722 1.5749 1.5693
    ==============================================================
    Using 1000 passes with size 1.0MiB
    New StringIO:   105.2350 106.3904 107.5411
    Same StringIO:  108.3744 109.4510 105.6012
    New cStringIO:  2.4698 2.4781 2.4165
    Same cStringIO: 2.4699 2.4600 2.4451
    New tempfile:   6.6086 6.5783 6.5916
    Same tempfile:  6.1420 6.1614 6.1366