【问题标题】:Why is my function faster than python's print function in IDLE?为什么我的函数在 IDLE 中比 python 的 print 函数快?
【发布时间】:2021-02-19 23:33:11
【问题描述】:

我不久前写了这个函数:

def faster_print(*args, sep=" ", end="\n", file=stdout):
    file.write(sep.join(map(str, args))+end)

我测试了它:

from sys import stdout
from time import perf_counter

def faster_print(*args, sep=" ", end="\n", file=stdout):
    file.write(sep.join(map(str, args))+end)

def time(function, *args, **kwargs):
    start = perf_counter()
    function(*args, **kwargs)
    return perf_counter()-start

def using_normal_print(number):
    for i in range(number):
        print("Hello world.", 5, 5.0, ..., str)

def using_faster_print(number):
    for i in range(number):
        faster_print("Hello world.", 5, 5.0, ..., str)

normal_time = time(using_normal_print, number=100)
faster_time = time(using_faster_print, number=100)

print("Normal print:", normal_time)
print("My print function", faster_time)

事实证明,它只是在 IDLE 中更快,而不是在 cmd 中。我知道 IDLE 会为 sys.stdoutsys.stdinsys.stderr 创建自己的对象,但我不明白为什么它只会减慢 python 内置的 print 函数。 This 回答说内置的 print 函数是用 c 编写的。由于我的函数需要从 python 字节码编译成机器码,这不应该让它更快吗?

我正在使用 Python 3.7.9 和 IDLE 版本 3.7.9

【问题讨论】:

  • datetime.now() 对于此目的不一定准确;您应该使用time.perf_counter 作为这个“具有最高可用分辨率的时钟来测量短持续时间”,或者您可以使用timeit 模块而不是滚动您自己的计时代码。
  • 不要使用 datetime.now() 进行基准测试。你应该改用time.process_time
  • 好的,我会尝试更新问题
  • 使用 timeit 进行 1k 次迭代时它们大致相同
  • @TeejayBruno 你在使用 IDLE 吗?什么版本?

标签: python python-3.x printing python-idle


【解决方案1】:

TheLizard,感谢您报告和修改您的实验。作为 IDLE 维护者,我很关心 IDLE 速度。我注意到打印到屏幕有时比在 Python 终端/控制台 REPL 中慢得多。在后者中,Python 在与屏幕窗口相同的进程中执行,screen.write 直接写入屏幕缓冲区。另一方面,IDLE 在单独的进程中执行用户代码。在该过程中,替换 sys.stdout 通过套接字将输出发送到 IDLE GUI 进程,然后调用 tkinter text.insert,后者调用 tcl/tk 函数来写入屏幕窃听器。但是直到现在,我都没有好好调查。

我在我的 Win 10 机器上运行了 3.10.0a5 中的代码。在 REPL 中,正常和快速打印需要 0.05 秒。在 IDLE 中,他们花了大约 1.1 和 0.3 秒。上面的开销解释了 6 (.3/.05) 的因数。但是额外的系数大约是 3.7 (1.1/.3)?

为了检验 kaya3 的第二个假设,我定义了 s = 'a'*75 并将您的打印参数替换为 s。在 REPL 中,时间仍然是 0.05 和 0.05。在 IDLE 中,它们约为 0.41 和 0.31。我的结论是有一点内部打印功能开销,但是 3.7 的大部分是额外的套接字到屏幕开销。当 print 写入缓冲区时,没有理由预先连接小字符串,因为多个 stdout.write 本质上是在连接,无论是屏幕缓冲区还是磁盘缓冲区。

为了进一步测试这一点,我将测试更改为编写 3 个 40 行的块。 REPL 时间保持不变。在 IDLE 中,它们的平均速度约为 0.058 和 0.05,与 REPL 中的速度差不多。

结论:我应该记录一下,如果一个人打印为在 IDLE 中定期运行而编写的代码并且关心速度,那么应该首先将想要显示的所有内容预先组装到一个字符串中,然后打印该字符串。 IDLE 为回溯执行此操作,这就是它们显示“一次全部”的原因。

【讨论】:

  • 感谢您的详细回答。因此,使 IDLE 更快的唯一方法是强制 cpython 在写入 stdout 之前加入传递给其 print 函数的参数。
  • 我建议关心的用户应该加入 args 和可能的行,并使用单个字符串参数调用 print。我现在正在 IDLE 文档中添加一段关于此的内容。我曾经用 500000 40 char 行填充 Shell 以查看滚动是否仍然可以正常工作(是的)。一次打印 1000 行会更快;-)。我考虑过替换内置print的可能性,但这会产生精确仿真的问题,即使仿真完美,也会使IDLE有所不同。
  • 我认为这不是一个好建议,但如果 IDLE 将所有标准输出写入缓冲区,几毫秒后写入 tkinter。使用<tkinter.Tk>.after 脚本应该不难实现。否则,如果要打印很多东西,每个人都应该加入 print args。
  • 运行用户代码的部分原因是第二个进程,以便用户 tkinter 代码和 IDLE 的 GUI tkinter 代码不会干扰。所以idlelib.run,它设置了执行进程和它与gui进程的连接,并没有运行tkinter/tk代码。
  • 连接是在一个单独的线程中处理的,所以我想在等待时让那个线程休眠,但我怀疑这也行不通,因为睡眠不能被中断并且 Windows 的最小睡眠是,我相信至少有 16 毫秒。