Go比Python真的快那么多吗?

57

我认为我的实现可能有误,因为结果不合理。我有一个 Go 程序,用于计数到 1000000000:

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 1000000000; i++ {}
    fmt.Println("Done") 
}

这个任务可以在不到一秒钟内完成。另一方面,我有一个Python脚本:

x = 0
while x < 1000000000:
    x+=1
print 'Done'

它几分钟就能完成。

为什么 Go 版本如此之快?它们都在计数到 1000000000,或者是我遗漏了什么吗?

8个回答

92

十亿并不是一个很大的数字。只要是任何现代机器,如果能够使用本机类型进行工作,最多只需几秒钟即可完成。我编写了一个等效的C程序进行验证,查看汇编以确保它实际上正在执行加法,并计时(在我的机器上约为1.8秒)。

然而,Python并没有本地类型变量的概念(或者根本没有有意义的类型注释),因此在这种情况下需要做数百倍的工作。简而言之,你标题问题的答案是“是”。Go确实可以比Python快得多,即使没有任何像优化无副作用循环之类的编译器技巧。


76

pypy实际上在加快这个循环方面表现出色

def main():
    x = 0
    while x < 1000000000:
        x+=1

if __name__ == "__main__":
    s=time.time()
    main()
    print time.time() - s

$ python count.py 
44.221405983
$ pypy count.py 
1.03511095047

速度提升约97%!

对于3个没有"理解"的人,需要澄清的是Python语言本身并不慢。CPython实现是运行代码的相对简单的方式。Pypy是另一种语言实现,采用许多棘手的技巧(尤其是JIT),可以产生巨大差异。直接回答标题中的问题 - Go并不比Python快 "太多",而是比CPython快那么多。

话虽如此,这些代码示例并没有真正执行相同的操作。Python需要实例化1000000000个int对象。Go只是增加一个内存位置。


26

这种情况将会极大地支持本地编译的静态类型语言。本地编译的静态类型语言能够发出非常简单的循环,例如4-6个CPU操作码,利用简单的检查条件来终止。这个循环有效地避免了分支预测错误,可以有效地被认为是每个CPU周期执行一次增量(虽然这并不完全正确)。

Python的实现需要做相当多的工作,主要是由于动态类型引起的。 在Python中,为了将两个 int 相加,必须进行多个不同的调用(内部和外部)。在Python中,它必须调用 __add__(实际上是 i = i.__add__(1),但这种语法只适用于Python 3.x),然后检查传递的值的类型(以确保它是一个 int),然后添加整数值(从两个对象中提取它们),然后将新的整数值再次封装在一个新对象中。最后,它将新对象重新赋值给局部变量。相比之下,Go /本机版本很可能只通过副作用递增寄存器,这是相当多的工作,甚至没有解决循环本身的问题。

在这样一个微不足道的基准测试中,Java表现会更好,并且很可能与Go非常接近;JIT和计数器变量的静态类型可以确保这一点(它使用特殊的整数加JVM指令)。再次强调,Python没有这样的优势。现在,有一些实现,比如PyPy/RPython,它们运行静态类型检查阶段,在这里的表现应该比CPython好得多..


8
抱歉如果我没有表达清楚,我并不是想把这个作为基准。我只是好奇为什么 Python 版本会慢那么多。 - bab
-1:你最后的“本质上具有误导性”的评论似乎是一个没有理由或解释的平板断言。 - igouy
1
@igouy,我不明白为什么它是没有保证的(整个帖子都是一个辩解),但我将其删除,因为它没有添加任何新内容。 - user166390

10

在这里有两个因素起作用。首先,Go语言被编译成机器码并直接在CPU上运行,而Python被编译为字节码并针对(特别慢的)虚拟机执行。

第二个更重要的影响程序性能的因素是,这两个程序的语义实际上有很大的不同。Go版本创建了一个名为“x”的“框”,该框包含一个数字,并在每次通过程序时将其增加1。Python版本实际上必须在每个循环中创建一个新的“框”(int对象),并最终不得不将它们丢弃。我们可以通过稍微修改您的程序来演示这一点:

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 10; i++ {
        fmt.Printf("%d %p\n", i, &i)
    }
}

...并且:

x = 0;
while x < 10:
    x += 1
    print x, id(x)
这是因为Go由于其C语言的根源,使用变量名来引用“位置”,而Python则使用变量名来引用“事物”。由于整数在Python中被视为唯一不可变的实体,我们必须不断创建新的整数。Python应该比Go慢,但你选择了最坏的情况-在基准测试游戏中,我们发现Go平均快约25倍(最差的情况下快100倍)。
你可能已经读到过,如果你的Python程序运行太慢,可以通过将一些代码转换为C来提高速度。幸运的是,在这种情况下,有人已经为你完成了这项工作。如果你将空循环改写为使用xrange(),就像这样:
for x in xrange(1000000000):
    pass
print "Done."

如果你使用这种方法,你会发现它运行速度会快大约两倍。如果你发现循环计数器实际上是你程序的主要瓶颈,那么现在可能是时候去探索一种新的解决问题的方式了。



4

@troq

我有些迟到,但是我的答案是肯定和否定。正如@gnibbler所指出的那样,CPython在简单实现上较慢,但当你需要时,pypy可以进行即时编译以获得更快的代码。

如果您使用CPython进行数字处理,大多数人将使用numpy对数组和矩阵进行快速操作。最近我一直在使用numba,它允许您向代码添加简单的包装器。对于这个函数,我只需向incALot()添加@njit,就可以运行您在上面看到的代码。

在我的计算机上,CPython需要61秒,但使用numba包装器只需要7.2微秒,这将类似于C,并且可能比Go更快。这是一个800万倍的加速。

因此,在Python中,如果数字方面似乎有点慢,还有工具来解决它-而且您仍然可以获得Python的程序员生产力和REPL。

def incALot(y):
    x = 0
    while x < y:
        x += 1

@njit('i8(i8)')
def nbIncALot(y):
    x = 0
    while x < y:
        x += 1
    return x

size = 1000000000
start = time.time()
incALot(size)
t1 = time.time() - start
start = time.time()
x = nbIncALot(size)
t2 = time.time() - start
print('CPython3 takes %.3fs, Numba takes %.9fs' %(t1, t2))
print('Speedup is: %.1f' % (t1/t2))
print('Just Checking:', x)

CPython3 takes 58.958s, Numba takes 0.000007153s
Speedup is: 8242982.2
Just Checking: 1000000000

0
问题在于Python是解释性语言,而GO不是,因此没有真正的方法来进行基准测试。解释性语言通常(并非总是)具有vm组件,这就是问题所在,您运行的任何测试都是在解释性边界内运行而不是实际运行时边界内运行。Go在速度上略慢于C,这主要是由于它使用垃圾回收而不是手动内存管理。话虽如此,与Python相比,GO很快,因为它是一种编译语言,GO唯一缺少的是错误测试,如果我错了,请纠正我。

-1

编译器可能意识到您在循环后没有使用“i”变量,因此它通过删除循环来优化最终代码。

即使您之后使用了它,编译器也可能足够聪明以替换循环。

i = 1000000000;

希望这能有所帮助 =)

1
您可以通过获取汇编列表来检查循环是否仍然在代码中:go build -gcflags -S main.go - topskip

-6

我不熟悉Go语言,但我猜测Go版本会忽略循环,因为循环体什么也没做。另一方面,在Python版本中,你在循环体中递增了x,所以它可能实际上正在执行循环。


我将for循环更改为每次循环将i赋值给另一个变量(即i2 = i),但速度仍然相同(因此基本上我知道for循环被执行)。 - bab
我让程序在结尾打印i2,而i2的值是999999999。 - bab

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