54

我想我可能错误地实现了这一点,因为结果没有意义。我有一个计数为 1000000000 的 Go 程序:

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 还是我错过了什么?

4

8 回答 8

93

10亿并不是一个很大的数字。任何合理的现代机器最多应该能够在几秒钟内完成这项工作,如果它能够使用本机类型完成工作的话。我通过编写一个等效的 C 程序来验证这一点,阅读程序集以确保它确实在进行加法,并对其进行计时(它在我的机器上大约 1.8 秒内完成)。

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

于 2012-09-25T01:35:26.690 回答
74

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快“那么多” ,Go 比CPython快那么多。

话虽如此,代码示例并没有真正做同样的事情。Python 需要实例化它的 1000000000 个int对象。Go 只是增加一个内存位置。

于 2012-09-25T05:43:46.280 回答
24

这种情况将非常有利于体面的本机编译的静态类型语言。本地编译的静态类型语言能够发出一个非常简单的循环,比如 4-6 个 CPU 操作码,它利用简单的检查条件来终止。这个循环实际上有零分支预测未命中,并且可以有效地被认为是在每个 CPU 周期执行一次增量(这并不完全正确,但是..)

Python 实现必须做更多工作,主要是由于动态类型。Python 必须进行多次不同的调用(内部和外部)才能将两个ints 相加。在 Python 中它必须调用__add__(它是有效i = i.__add__(1)的,但这种语法只在 Python 3.x 中有效),它又必须检查传递的值的类型(以确保它是一个int),然后它添加整数值(从两个对象中提取它们),然后将新的整数值再次包装在一个新的对象中。最后,它将新对象重新分配给局部变量。这要多得多的工作而不是增加单个操作码,甚至不解决循环本身 - 相比之下,Go/native 版本可能只是通过副作用增加一个寄存器。

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

于 2012-09-25T01:17:13.093 回答
10

你有两件事在这里工作。第一个是 Go 被编译成机器代码并直接在 CPU 上运行,而 Python 被编译成字节码,在(特别慢的)VM 上运行。

第二个也是更重要的影响性能的因素是这两个程序的语义实际上是显着不同的。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 根,使用变量名来引用place,而 Python 使用变量名来引用things。由于整数在 python 中被认为是唯一的、不可变的实体,我们必须不断地创建新的实体。Python 应该比 Go 慢,但您选择了最坏的情况 -在 Benchmarks Game 中,我们看到 Go 平均快 25 倍(最坏情况下为 100 倍)。

你可能已经读过,如果你的 Python 程序太慢,你可以通过将东西移到 C 中来加速它们。幸运的是,在这种情况下,有人已经为你做了这件事。如果你重写你的空循环来使用xrange()像这样:

for x in xrange(1000000000):
    pass
print "Done."

...您会看到它的运行速度大约是原来的两倍。如果您发现循环计数器实际上是程序中的主要瓶颈,那么可能是时候研究一种解决问题的新方法了。

于 2012-09-25T01:34:44.860 回答
3

@troq

我参加聚会有点晚了,但我会说答案是肯定的和否定的。正如@gnibbler 指出的那样,CPython 在简单实现中速度较慢,但​​ pypy 是 jit 编译的,可在您需要时获得更快的代码。

如果您使用 CPython 进行数值处理,大多数人将使用 numpy 进行处理,从而对数组和矩阵进行快速操作。最近我对 numba 做了很多工作,它允许您在代码中添加一个简单的包装器。对于这个,我刚刚将@njit 添加到一个函数 incALot() 中,该函数在上面运行你的代码。

在我的机器上,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
于 2014-10-20T02:52:45.900 回答
0

问题是 Python 被解释了,GO 没有,所以没有真正的方法来测试速度。解释语言通常(并不总是有一个 vm 组件)是问题所在,您运行的任何测试都是在解释边界而不是实际运行时边界中运行的。Go 在速度方面比 C 稍慢,这主要是因为它使用垃圾收集而不是手动内存管理。也就是说,与 Python 相比,GO 速度很快,因为它是一种编译语言,GO 中唯一缺少的就是错误测试,如果我错了,我会纠正。

于 2016-07-24T00:42:37.387 回答
-1

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

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

i = 1000000000;

希望这会有所帮助=)

于 2012-09-25T01:22:04.647 回答
-6

我对 go 不熟悉,但我猜 go 版本会忽略循环,因为循环的主体什么都不做。另一方面,在 python 版本中,您x在循环体中递增,因此它可能实际上是在执行循环。

于 2012-09-25T01:03:30.733 回答