2017-10-10 153 views
1

所以我正在写一个函数,它接受一个元组作为参数,并为它做了一堆东西。这里是什么样子:Python是否将函数参数加载到寄存器中,还是将它们保存在堆栈中?

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%的平均值。我的情况绝对不是微不足道的。当然,这不是科学的,因为我需要在更多的环境中进行更多的测试,并提供更多的输入,但足以满足我的简单用例!

使用Python 2.7在Macbook Pro (Early 2015)上测量的时间,其中Broadwell i5-5257U CPU(2c4t max 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位服务器虚拟机。

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

+0

如果你做'loc0 = location [0]',你正在设置一个变量,而不是“寄存器”。 – BrenBarn

+2

你正在使用哪种python JIT编译器?如果你使用的是intepreter,那么任何Python变量都不会存在于不同表达式之间的寄存器中。在C中,使用局部变量的一个好处是有可能出现混叠。 (例如,如果编译器认为修改self.board [0] [0]也可能修改了位置[0],则将其读入本地会让编译器将其保存在寄存器中。)IDK(如果这是Python中的事情)。 (我在这里是因为[tag:assembly]标签) –

+0

@BrenBarn是的,但是编译器会优化定义的变量,这些变量通过为它们分配寄存器来频繁使用。 – QuantumHoneybees

回答

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)编写的JIT编译器。 IDK,如果有Python的话。见Does the Python 3 interpreter have a JIT feature?

JIT编译器确实将Python代码转换为机器代码,其中寄存器状态通常保存Python变量而不是解释器数据。再次,如果没有JIT编译器(或提前编译器),“将变量保存在寄存器中”不是问题。


这可能更快,因为它避免了[]运营商和其它开销(见布林的回答,你接受)


脚注:一对夫妇的ISA具有内存映射寄存器。例如AVR(8位RISC微控制器),其中该芯片还具有内置SRAM,其中包含包含寄存器的低范围内存地址。所以你可以做一个索引加载并获取寄存器内容,但是你也可以在没有保存架构寄存器内容的内存上做这件事。

1

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

+0

对于CPython可能如此,但更准确地说,此行为不是完全指定。 – BrenBarn

1

这将是一个快一点,如果你存储这些两个可变:

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

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

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

+1

不幸的是,这是针对基于代码速度等级的课程。荒谬的是,我知道但不是很多,我可以做不幸的事情。 – QuantumHoneybees

1

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

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

这就是说,有性能差异,因为如果使用location[0]无处不在,实际查找将(或至少可以 - 在理论上智能Python实现可以优化这个)每次一再发生的表达location[0]被评估。但是,如果您在任何地方都使用loc0 = location[0]然后使用loc0,则只会发生一次查找。在典型的情况下(例如,location是一个Python列表或字典,你不会在严格的循环中运行这段代码gazillions),这种差异很小。

+0

在大多数/某些解释器/ JIT编译器中分配给局部变量是否存在一些成本?在C中,你会得到'int tmp = a [i]; b [i] = tmp;'为'b [i] = a [i];'(假设'tmp'稍后不会使用,并且当然您可以使用优化来编译)。如果有成本,如果你甚至使用两次这个价值,那么你可能会赢得一次本地获胜,你说的是?即使曾经是一次性损失? (平均而言,我的意思是,在一个有许多迭代的小型到中型循环中,我相信你可以在不是这样的情况下构建案例。) –

+0

@PeterCordes:那么将会生成额外的字节码来完成这个任务。我很震惊地看到一个Python实现,其中分配给本地的成本不可忽略。我不知道JIT编译器做什么;理论上他们可以优化一些任务,但我不确定他们有多聪明。从理论上讲,如果您不止一次使用该值,那么分配给本地将是一次性能胜利,但如果您需要在计算过程中修改该值,则会更加复杂。尽管如此,性能优势在许多情况下可能无法察觉地很小。 – BrenBarn

+1

你可以用CPU性能计数器来测量很小的性能差异,就像100中的一部分很容易检测到小的asm循环(虽然即使是2%的减速也很难通过看asm ...来解释) https://stackoverflow.com/questions/44169342/can-x86s-mov-really-be-free-why-cant-i-reproduce-this-at-all/44193770#44193770))。经过仔细的测量,您可以获得比10k中的一个部分更好的信噪比(对于微基准)。我已经得到了一个观点,如果你真的关心perf,Python通常是错误的语言:P –

相关问题