【问题标题】:tempfile.TemporaryFile vs. StringIOtempfile.TemporaryFile 与 StringIO
【发布时间】:2016-05-18 15:17:46
【问题描述】:

我写了一个小基准,比较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

【问题讨论】:

  • GROANMODE=1 你实际上并没有运行临时文件测试,它应该是timeit('bench_temporaryfile()'(带有调用函数的括号)。
  • @tdelaney:不错的收获。应该注意到那里的时间有多可疑,但我只是假设我的古老系统没有有效地处理临时文件。 :-) 我将其纳入我的答案(并将其作为主要问题,因为它是)。
  • 谢谢@tdelaney 和 ShadowRanger。

标签: python stringio cstringio


【解决方案1】:

您最大的问题:Per tdelaney,您实际上从未运行过TemporaryFile 测试;您省略了timeit sn-p 中的括号(并且仅针对该测试,其他实际运行)。因此,您正在计时查找名称 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 而不是io.StringIO 并统一写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 可能只是无论如何都是RAMdisk)。

有些事情告诉我你可能有一个设置,其中TemporaryFile 基本上是一个从不进入文件系统的普通内存缓冲区,而我的最终可能会在持久存储上结束(如果只是短期的话);内存中发生的事情是可以预测的,但是当您涉及文件系统时(TemporaryFile 可以,取决于操作系统、内核设置等),系统之间的行为会有很大差异。

【讨论】:

  • 你能给出你的测试代码吗?想看看我在做什么不同/错误。
  • @pcdummy:我怀疑你的测试是错误的,但我猜我们对TemporaryFile 有非常不同的幕后行为;您可能使用的是 Linux 内核 3.11 或更高版本(O_TMPFILE 可用,并且做了一些有意义的事情来最小化实际的磁盘 I/O);我使用的是没有该功能的旧内核,所以我在/tmp 中创建了一个真实文件。我的代码与您的代码几乎相同(我刚刚放弃了在交互式提示下定义的BufferedRandomSpooledTemporaryFile 的测试)并使用来自ipython%timeit 魔法进行了测试,例如%timeit -r5 bench_temporaryfile().
  • 查看open manpage 并阅读O_TMPFILE 上的信息,这可能会在我们的操作系统之间产生巨大的性能差异。
  • 我在 3.2.0-4-amd64 (Debian wheezy) 上测试的 tempfile 也快得多。 /tmp 上没有挂载 tmpfs。
  • @pcdummy:您可能想尝试手动执行相同的任务as the built-in tempfile.TemporaryFile,看看它是否真的最终支持O_TMPFILE 或做一些特别的事情。我怀疑我的系统既旧又做傻事(过时的 RHEL 版本,几乎整个系统都安装在 NFS 上)。关键是,至少在某些系统上,TemporaryFile 最终会改变成本,因为它最终可能会使用慢速磁盘,而不是内存缓冲区。
猜你喜欢
  • 2015-08-03
  • 1970-01-01
  • 2023-04-11
  • 2010-11-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-17
相关资源
最近更新 更多