【问题标题】:Does Python load in function arguments into registers or does it keep them on the stack?Python是将函数参数加载到寄存器中还是将它们保留在堆栈中?
【发布时间】:2017-10-10 14:49:24
【问题描述】:

所以我正在编写一个函数,它接受一个元组作为参数,并对其进行一系列处理。看起来是这样的:

   def swap(self, location):
    if (location[0] < 0 or location[1] < 0 or
        location[0] >= self.r or location[1] >= self.c):
        return False

    self.board[0][0] = self.board[location[0]][location[1]]
    self.board[location[0]][location[1]] = 0
    self.empty = (location[0],location[1])

我正在尝试使我的代码尽可能高效,因此由于我没有修改 location 的值,因此将变量加载到寄存器 (loc0 = location[0]; loc1 = location[1]) 以加快计算速度(零-循环读取)还是location在作为函数参数传入时已经被Python编译器加载到寄存器中?

编辑:我硬着头皮进行了一些测试。以下是此函数运行 1000 万次重复输入的结果(以秒为单位):"up", "down", "left", "right"(分别)

 Code as is:
   run#1: 19.39
   run#2: 17.18
   run#3: 16.85
   run#4: 16.90
   run#5: 16.74
   run#6: 16.76
   run#7: 16.94

 Code after defining location[0] and location[1] in the beginning of the function:
   run#1: 14.83
   run#2: 14.79
   run#3: 14.88
   run#4: 15.033
   run#5: 14.77
   run#6: 14.94
   run#7: 14.67

这意味着性能平均提高了 16%。对我来说绝对不是微不足道的。当然,这并不科学,因为我需要在更多环境中进行更多测试,输入更多,但对于我的简单用例来说已经足够了!

Macbook Pro (Early 2015) 上使用 Python 2.7 测量的时间,Broadwell i5-5257U CPU(2c4t 最大 turbo 3.1GHz,持续 2.7GHz,3MB 三级缓存)。

IDE 是:PyCharm Edu 3.5.1 JRE:1.8.0_112-release-408-b6 x86_64 JVM:OpenJDK 64 位服务器虚拟机。

不幸的是,这是针对根据代码速度评分的课程。

【问题讨论】:

  • 如果你这样做loc0 = location[0],你是在设置一个变量,而不是一个“寄存器”。
  • 您使用的是哪个 python JIT 编译器?如果您使用的是解释器,那么任何 Python 变量都不太可能存在于不同表达式之间的寄存器中。在 C 中,使用局部变量的一个好处是可能存在别名。 (例如,如果编译器认为修改 self.board[0][0] 可能也修改了 location[0],则将其读入本地将使编译器将其保存在寄存器中。) IDK 如果这是 Python 中的事情。 (我在这里是因为assembly 标签。)
  • @BrenBarn 是的,但是编译器通过为它们分配寄存器来优化经常使用的已定义变量。
  • @QuantumHoneybees:什么编译器? Python 不编译任何东西(无论如何也不编译为机器代码)。
  • 它可能更快,因为它避免了 [] 运算符和其他开销(请参阅您接受的 Bren 的答案),而不是因为它实际上将值保存在寄存器中。除非您是 JIT 编译,否则不值得专门使用一个寄存器来保存 Python 变量的值。为了使这更合理,解释器必须用 asm 编写,或者在 GNU C 中使用long last_used_variable asm("rdx") 或其他东西。这毫无意义(我是作为一个经常查看 C 编译器 asm 输出的经验丰富的 asm 程序员/优化器说的。)

标签: python python-2.7 performance assembly


【解决方案1】:

如果您使用的是解释器,那么任何 Python 变量都不太可能存在于不同表达式之间的寄存器中。你可以看看 Python 源代码是如何编译成字节码的。

Python 字节码(存储在解释器之外的文件中)是基于堆栈的 (http://security.coverity.com/blog/2014/Nov/understanding-python-bytecode.html)。然后将此字节码解释或 JIT 编译为本机机器码。 常规 python 只解释,因此将 python 变量跨多个语句保存在机器寄存器中是不合理的

用 C 语言编写的解释器可能会将字节码堆栈的顶部保存在解释循环内的局部变量中,而 C 编译器可能会将该 C 变量保存在寄存器中。因此,重复使用相同的 Python 变量可能最终不会有太多的存储/重新加载往返。

请注意,Broadwell CPU 上的存储转发延迟大约为 4 或 5 个时钟周期,远不及往返 DRAM 的数百个周期。存储/重新加载甚至不必等待存储退出并提交到 L1D 缓存;它直接从存储缓冲区转发。相关:http://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/http://agner.org/optimize/,以及 标签 wiki 中的其他链接)。对于 L1D 缓存命中,加载使用延迟也只有 5 个时钟周期(从地址准备好到数据准备好的延迟。您可以通过链表(在 asm 中)的指针追踪来测量它。)有足够的解释器开销(总它运行以确定下一步要做什么的指令数量)这可能甚至不是瓶颈。


将特定的 Python 变量保存在寄存器中对于解释器来说根本不合理。即使您在 asm 中编写了解释器,根本问题是寄存器不可寻址。 x86 add r14d, eax 指令必须将两个寄存器都硬编码到指令的机器代码中。 (其他所有 ISA 的工作方式都相同:寄存器编号是指令机器代码的一部分,没有基于任何数据的间接寻址)。即使解释器完成工作以找出它需要“将 reg-var #3 添加到 reg-var #2”(即将字节码堆栈操作解码回寄存器变量以获得它解释的内部表示),它也会必须使用与任何其他寄存器组合不同的功能。

给定一个整数,获取第 N 个寄存器值的唯一方法是跳转到使用该寄存器的指令,或者将所有寄存器存储到内存中并对结果数组进行索引。 (或者可能是某种无分支比较和屏蔽的东西)。

无论如何,尝试对此做任何具体的事情都无利可图,这就是为什么人们只用 C 编写解释器并让 C 编译器(希望)做好优化实际运行的机器代码的工作。

或者您编写一个 JIT 编译器,就像 Sun 为 Java(HotSpot VM)所做的那样。 IDK 如果有 Python 的话。见Does the Python 3 interpreter have a JIT feature?

JIT 编译器实际上将 Python 代码转换为机器代码,其中寄存器状态主要保存 Python 变量而不是解释器数据。同样,如果没有 JIT 编译器(或提前编译器),“将变量保存在寄存器中”就不是一回事了。


它可能更快,因为它避免了 [] 运算符和其他开销(请参阅 Bren 的回答,您已接受)


脚注:一些 ISA 具有内存映射寄存器。例如AVR(8 位 RISC 微控制器),其中芯片还具有内置 SRAM,其中包含包括寄存器在内的低范围内存地址。因此,您可以进行索引加载并获取寄存器内容,但您也可以在没有保存架构寄存器内容的内存上完成。

【讨论】:

    【解决方案2】:

    Python VM使用堆栈来执行其字节码,并且此堆栈完全独立硬件堆栈。您可以使用dis 反汇编您的代码,看看您的更改如何影响生成的字节码。

    【讨论】:

    • CPython 可能是这样,但更准确的说法是根本没有指定这种行为。
    【解决方案3】:

    把这两个变量存起来会快一点:

    loc0 = location[0]
    loc1 = location[1]
    

    因为将只有两个look-up 而不是四个。

    顺便说一句,如果你想使用python,你不应该关心这个低级别的性能。

    【讨论】:

    • 不幸的是,这是针对根据代码速度评分的课程。可笑,我知道,但不幸的是我无能为力。
    【解决方案4】:

    这些细节不是 Python 指定行为的一部分。正如 Ignacio 的回答所说,CPython 以一种方式做到这一点,但语言本身并不能保证这一点。 Python 对其功能的描述与寄存器等低级概念相去甚远,而且大多数时候担心 Python 的功能如何映射到这些细节上并没有什么用处。 Python 是一种高级语言,其行为是根据高级抽象定义的,类似于 API。

    无论如何,在 Python 代码中执行 loc0 = language[0] 之类的操作与设置寄存器无关。它只是创建指向现有 Python 对象的新 Python 名称。

    也就是说,存在性能差异,因为如果您在任何地方使用 location[0],实际查找将(或至少可能——理论上智能 Python 实现可以优化这一点)每次表达式都会一次又一次地发生location[0] 被评估。但是,如果您先使用loc0 = location[0],然后在任何地方使用loc0,您就会知道查找只会发生一次。在典型情况下(例如,location 是 Python 列表或字典,您不会在紧密循环中运行此代码无数次)这种差异会很小。

    【讨论】:

    • 在大多数/某些解释器/JIT 编译器中分配给局部变量是否有一些成本?在 C 语言中,int tmp = a[i]; b[i] = tmp; 的 asm 与 b[i] = a[i]; 相同(假设以后不使用 tmp,并且您在编译时当然启用了优化)。如果有成本,如果您甚至两次使用该值,分配给本地人可能是一个perf win,您是说?即使一次是性能损失? (平均而言,我的意思是,在具有多次迭代的中小型循环中。我相信您可以构建不正确的案例。)
    • @PeterCordes:嗯,将生成额外的字节码来完成分配。看到 Python 实现分配给本地的成本不可忽略,我会感到震惊。我不知道 JIT 编译器做什么;理论上他们可以优化一些任务,但我不确定他们在这方面有多聪明。从理论上讲,如果您多次使用该值,则将其分配给本地将是一个完美的胜利,但如果您需要在计算期间修改该值,则它会更加复杂。不过,在许多情况下,性能优势可能微乎其微。
    • 您可以使用 CPU 性能计数器测量非常小的性能差异,例如对于小的 asm 循环 (although even a 2% slowdown can be hard to explain from looking at the asm...) 很容易检测到 100 分之一。通过仔细测量,您可以获得优于 10k 微基准的信噪比。我已经明白了,如果你真的这么关心性能,python 通常是错误的语言:P
    • @PeterCordes:是的,但我的意思是,在实际应用程序中,一个更改的性能差异将在所有其他可能影响性能但不受您控制的事物中丢失。如果你真的在做任何事情,就会有内存分配、垃圾收集、磁盘读取等;与那些东西相比,分配一个局部变量的成本将是微不足道的。
    • @BrenBarn,我自己运行了这两种实现的代码。完整的结果在我的帖子的编辑中,但通过在本地声明 location[0] 和 location[1] 基本上有近 18% 的改进!
    猜你喜欢
    • 2010-11-01
    • 1970-01-01
    • 2016-03-06
    • 2018-02-25
    • 2016-03-15
    • 2016-05-07
    • 1970-01-01
    • 2020-01-10
    • 2021-06-19
    相关资源
    最近更新 更多