tempfile.TemporaryFile与StringIO的区别

11
我写了一个小基准测试,比较了不同的字符串连接方法,用于ZOCache。从结果看,tempfile.TemporaryFile似乎比其他任何方法都要快:
$ python src/ZOCache/tmp_benchmark.py 
3.00407409668e-05 TemporaryFile
0.385630846024 SpooledTemporaryFile
0.299962997437 BufferedRandom
0.0849719047546 io.StringIO
0.113346099854 concat

我一直使用的基准代码:

#!/usr/bin/python
from __future__ import print_function
import io
import timeit
import tempfile


class Error(Exception):
    pass


def bench_temporaryfile():
    with tempfile.TemporaryFile(bufsize=10*1024*1024) as out:
        for i in range(0, 100):
            out.write(b"Value = ")
            out.write(bytes(i))
            out.write(b" ")

        # Get string.
        out.seek(0)
        contents = out.read()
        out.close()
        # Test first letter.
        if contents[0:5] != b"Value":
            raise Error


def bench_spooledtemporaryfile():
    with tempfile.SpooledTemporaryFile(max_size=10*1024*1024) as out:
        for i in range(0, 100):
            out.write(b"Value = ")
            out.write(bytes(i))
            out.write(b" ")

        # Get string.
        out.seek(0)
        contents = out.read()
        out.close()
        # Test first letter.
        if contents[0:5] != b"Value":
            raise Error


def bench_BufferedRandom():
    # 1. BufferedRandom
    with io.open('out.bin', mode='w+b') as fp:
        with io.BufferedRandom(fp, buffer_size=10*1024*1024) as out:
            for i in range(0, 100):
                out.write(b"Value = ")
                out.write(bytes(i))
                out.write(b" ")

            # Get string.
            out.seek(0)
            contents = out.read()
            # Test first letter.
            if contents[0:5] != b'Value':
                raise Error


def bench_stringIO():
    # 1. Use StringIO.
    out = io.StringIO()
    for i in range(0, 100):
        out.write(u"Value = ")
        out.write(unicode(i))
        out.write(u" ")

    # Get string.
    contents = out.getvalue()
    out.close()
    # Test first letter.
    if contents[0] != 'V':
        raise Error


def bench_concat():
    # 2. Use string appends.
    data = ""
    for i in range(0, 100):
        data += u"Value = "
        data += unicode(i)
        data += u" "
    # Test first letter.
    if data[0] != u'V':
        raise Error


if __name__ == '__main__':
    print(str(timeit.timeit('bench_temporaryfile()', setup="from __main__ import bench_temporaryfile", number=1000)) + " TemporaryFile")
    print(str(timeit.timeit('bench_spooledtemporaryfile()', setup="from __main__ import bench_spooledtemporaryfile", number=1000)) + " SpooledTemporaryFile")
    print(str(timeit.timeit('bench_BufferedRandom()', setup="from __main__ import bench_BufferedRandom", number=1000)) + " BufferedRandom")
    print(str(timeit.timeit("bench_stringIO()", setup="from __main__ import bench_stringIO", number=1000)) + " io.StringIO")
    print(str(timeit.timeit("bench_concat()", setup="from __main__ import bench_concat", number=1000)) + " concat")

编辑 Python3.4.3 + io.BytesIO

python3 ./src/ZOCache/tmp_benchmark.py 
2.689500024644076e-05 TemporaryFile
0.30429405899985795 SpooledTemporaryFile
0.348170792000019 BufferedRandom
0.0764778530001422 io.BytesIO
0.05162201000030109 concat

使用 io.BytesIO 的新源代码:

#!/usr/bin/python3
from __future__ import print_function
import io
import timeit
import tempfile


class Error(Exception):
    pass


def bench_temporaryfile():
    with tempfile.TemporaryFile() as out:
        for i in range(0, 100):
            out.write(b"Value = ")
            out.write(bytes(str(i), 'utf-8'))
            out.write(b" ")

        # Get string.
        out.seek(0)
        contents = out.read()
        out.close()
        # Test first letter.
        if contents[0:5] != b"Value":
            raise Error


def bench_spooledtemporaryfile():
    with tempfile.SpooledTemporaryFile(max_size=10*1024*1024) as out:
        for i in range(0, 100):
            out.write(b"Value = ")
            out.write(bytes(str(i), 'utf-8'))
            out.write(b" ")

        # Get string.
        out.seek(0)
        contents = out.read()
        out.close()
        # Test first letter.
        if contents[0:5] != b"Value":
            raise Error


def bench_BufferedRandom():
    # 1. BufferedRandom
    with io.open('out.bin', mode='w+b') as fp:
        with io.BufferedRandom(fp, buffer_size=10*1024*1024) as out:
            for i in range(0, 100):
                out.write(b"Value = ")
                out.write(bytes(i))
                out.write(b" ")

            # Get string.
            out.seek(0)
            contents = out.read()
            # Test first letter.
            if contents[0:5] != b'Value':
                raise Error


def bench_BytesIO():
    # 1. Use StringIO.
    out = io.BytesIO()
    for i in range(0, 100):
        out.write(b"Value = ")
        out.write(bytes(str(i), 'utf-8'))
        out.write(b" ")

    # Get string.
    contents = out.getvalue()
    out.close()
    # Test first letter.
    if contents[0:5] != b'Value':
        raise Error


def bench_concat():
    # 2. Use string appends.
    data = ""
    for i in range(0, 100):
        data += "Value = "
        data += str(i)
        data += " "
    # Test first letter.
    if data[0] != 'V':
        raise Error


if __name__ == '__main__':
    print(str(timeit.timeit('bench_temporaryfile()', setup="from __main__ import bench_temporaryfile", number=1000)) + " TemporaryFile")
    print(str(timeit.timeit('bench_spooledtemporaryfile()', setup="from __main__ import bench_spooledtemporaryfile", number=1000)) + " SpooledTemporaryFile")
    print(str(timeit.timeit('bench_BufferedRandom()', setup="from __main__ import bench_BufferedRandom", number=1000)) + " BufferedRandom")
    print(str(timeit.timeit("bench_BytesIO()", setup="from __main__ import bench_BytesIO", number=1000)) + " io.BytesIO")
    print(str(timeit.timeit("bench_concat()", setup="from __main__ import bench_concat", number=1000)) + " concat")

这对于每个平台都是真的吗?如果是,为什么呢?

编辑:使用固定基准(和固定代码)的结果:

0.2675984420002351 TemporaryFile
0.28104681999866443 SpooledTemporaryFile
0.3555715570000757 BufferedRandom
0.10379689100045653 io.BytesIO
0.05650951399911719 concat

1
“GROANMODE=1” 您实际上没有运行临时文件测试,应该是 timeit('bench_temporaryfile()'(带括号调用函数)。 - tdelaney
@tdelaney:好发现。应该注意到那里的时间有多么可疑,但我只是觉得我的古老系统没有有效地处理临时文件。 :-) 我把它纳入了我的答案中(并将其作为主要问题,因为它确实是)。 - ShadowRanger
感谢 @tdelaney 和 ShadowRanger。 - pcdummy
1个回答

12

你最大的问题是:根据tdelaney的说法,你实际上从未运行TemporaryFile测试;在timeit片段中,你省略了括号(仅对该测试如此,其他测试实际上都运行了)。因此,你计时的是查找名称bench_temporaryfile所需的时间,而不是实际调用它所需的时间。请更改为:

print(str(timeit.timeit('bench_temporaryfile', setup="from __main__ import bench_temporaryfile", number=1000)) + " TemporaryFile")

发送至:

print(str(timeit.timeit('bench_temporaryfile()', setup="from __main__ import bench_temporaryfile", number=1000)) + " TemporaryFile")

在调用时添加括号以进行修复。

还有一些其他问题:

io.StringIO与您的其他测试用例基本不同。具体来说,您正在进行二进制模式下读取和写入str,并避免换行符转换的所有其他类型测试。 io.StringIO使用Python 3风格的字符串(Python 2中的unicode),您的测试通过使用不同的文字并将其转换为unicode而不是bytes来证明这一点。这增加了很多编解码开销,以及使用更多内存(相同数据的unicode使用2-4倍于str的内存,这意味着有更多分配器开销,更多复制开销等)。

另一个主要区别是您为TemporaryFile设置了一个非常巨大的bufsize;只需要进行少量系统调用,并且大多数写操作仅附加到缓冲区中的连续内存。相比之下,io.StringIO会存储写入的各个值,并且当您使用getvalue()时才将它们连接在一起。

最后,您认为通过使用bytes构造函数可以实现向前兼容性,但实际上并没有;在Python 2中,bytesstr的别名,因此bytes(10)返回'10',但在Python 3中,bytes是完全不同的东西,将整数传递给它将返回一个大小为零且初始化为零的bytes 对象,bytes(10)返回b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

如果您想进行公平的测试用例,则至少应该切换到cStringIO.StringIOio.BytesIO,并统一编写bytes。通常,您不会自己显式设置TemporaryFile和类似文件的缓冲区大小,因此您可能要考虑删除它。

在我自己的Linux x64和Python 2.7.10上使用ipython的%timeit魔法进行测试时,排名如下:

  1. io.BytesIO每次循环约48μs
  2. io.StringIO每次循环约54μs(因此unicode开销并没有增加太多)
  3. cStringIO.StringIO每次循环约83μs
  4. TemporaryFile每次循环约2.8 ms(请注意单位;ms比μs长1000倍)

这是没有返回默认缓冲区大小的情况下(我保留了您测试中的显式bufsize)。我怀疑TemporaryFile的行为将有很大的变化(取决于操作系统以及如何处理临时文件;某些系统只会存储在内存中,而其他系统可能会存储在/tmp中,但当然,/tmp可能只是一个RAM磁盘)。

有些东西告诉我,您可能拥有一个设置,其中TemporaryFile基本上


你能否提供你的测试代码吗?我想看看我哪里做错了或不同了。 - pcdummy
1
@pcdummy:我怀疑你的测试并没有错,但我猜我们在“TemporaryFile”的底层行为上可能有很大的不同;你可能在Linux内核3.11或更高版本上(其中“O_TMPFILE”可用,并且可以采取有意义的措施来最小化实际磁盘I/O);而我使用的是没有这个功能的旧内核,在“/tmp”中创建了一个真正的文件。我的代码与你的几乎完全相同(我只是删除了“BufferedRandom”和“SpooledTemporaryFile”的测试),在交互式提示符下定义,并使用ipython的“%timeit”魔法进行测试,例如“%timeit -r5 bench_temporaryfile()”。 - ShadowRanger
请查看open手册,并阅读有关O_TMPFILE的信息,这可能会在我们的操作系统之间产生巨大的性能差异。 - ShadowRanger
我在3.2.0-4-amd64 (Debian wheezy)上进行了测试。在那里,tempfile也快得多。/tmp上没有tmpfs安装。 - pcdummy
1
@pcdummy:您可能想尝试手动执行与内置的tempfile.TemporaryFile相同的任务,并查看它是否实际支持O_TMPFILE或者做了一些特殊的事情。我怀疑我的系统既古老又愚蠢(过时的RHEL版本,几乎整个系统都挂载在NFS上)。重点是,在某些系统上,TemporaryFile最终会因为可能使用缓慢的磁盘而导致成本大幅增加,而不是使用内存缓冲区。 - ShadowRanger
@pcdummy:哦,正如 tdelaney 指出的那样,当测试 TemporaryFile 时,您忽略了括号,因此您实际上从未运行过任何东西。在 timeit 测试中添加调用的括号。 - ShadowRanger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接