为什么在Python中,一个有100亿次迭代的大型for循环比C中慢得多?

9

我目前正在比较Python3和C中的两个循环计算。对于Python,我有以下内容:

# Python3
t1 = time.process_time()
a = 100234555
b = 22333335
c = 341500
for i in range(1, 10000000001):
    a = a - (b % 2)
    b = b - (c % 2)
print("Sum is", a+b)
t2 = time.process_time()
print(t2-t1, "Seconds")

然后在C语言中,我做同样的事情:

#include <stdio.h>

int main() {
   long long a = 100234555;
   long long b = 22333335;  
   long long c = 341500;
   for(long long i = 1; i <= 10000000000; i++){
        a = a - (b % 2);
        b = b - (c % 2);
   }
   printf("Sum is %lld\n", a+b);
   return 0;
}

我对Python和C代码进行了计时。Python的计时约为3500秒,而C语言的计时(包括编译和执行)仅需约0.3秒。

我想知道为什么时间差别如此之大。执行是在一台配备有100 GB内存和足够处理能力的服务器上完成的。


有趣。如果用位运算符& 1替换%,会怎样呢?速度是否相同缓慢? - skyboyer
2
因为C语言是编译成本地机器代码并针对当前架构进行了大量优化,直接在硬件上运行,而Python是一种解释性语言,不会执行任何这样的操作。 - Havenard
@skyboyer 注意在C语言中将%2替换为&1会产生不同的功能。%2的结果是1,0,-1,而&1的结果是1,0 - chux - Reinstate Monica
@chux,谢谢,我不知道。你能给我一个返回-1的例子吗?只是好奇,第一次尝试时找不到。 - skyboyer
@skyboyer printf("%d\n", -1%2); 请参阅“mod”和“remainder”的区别是什么? - chux - Reinstate Monica
我明白了,谢谢你解释。 - skyboyer
3个回答

19

部分原因是Python的字节码由程序执行而不是直接由CPU执行,但大多数开销是由整数不可变性引起的内存分配和释放导致的,这是由于对象模型而不是解释性。

问题在于您的C代码可以更改数字的值,而在Python中数字是不可变的,这意味着每次计算总和时,Python都必须为每个新值创建一个新的int对象,然后在它们不再使用后销毁旧的int。这使得它比修改单个内存值要慢得多。


还有可能是您的C编译器非常聪明,通过一系列优化推断出可以完全删除您的for循环,并且结果将与实际运行循环的结果相同。如果在您的示例中确实是这种情况,我希望代码运行速度会快得多,但可能会这样做。

Python没有这样聪明的编译器。它无法像那样聪明地做某些事情;由于在动态类型语言中可靠地进行优化如此困难,所以它不会设计去优化代码(尽管Python是强类型的事实会使其成为一种可能)。


1
这意味着,这只是实现细节。 - 0___________
2
这是真的吗?使用对象类型代替纯值整数,并且没有机制在它们无用时优化对象?为什么?? - R.. GitHub STOP HELPING ICE
1
@R.. Python并不是为了速度而设计的。如果你想在Python中使用可变整数,可以将其作为扩展添加进去,但是由于Python默认的引用模型,如果它默认情况下就像那样工作,大多数程序员会感到困惑。 - wizzwizz4
这不是“想要可变整数”的问题。没有人想要那个。问题在于想要纯值,它们没有与对象或对象生命周期相关联,因为这是一个巨大的低效层,没有语义差异。 - R.. GitHub STOP HELPING ICE
1
@R.. 我想看到那个完成。如果你在 GitHub 上 fork Python 并尝试实现它,我会帮忙。 - wizzwizz4
显示剩余5条评论

10

正如dmuir所指出的那样,如果编译器正确地传播一些常量,代码可以大大简化。例如:clang -O1将C代码编译成以下内容(参见https://gcc.godbolt.org/z/1ZH8Rm):

main:                                   # @main
        push    rax
        movabs  rsi, -9877432110
        mov     edi, offset .L.str
        xor     eax, eax
        call    printf
        xor     eax, eax
        pop     rcx
        ret
.L.str:
        .asciz  "Sum is %lld\n"

gcc -O1 生成基本相似的代码。

由于这只涉及一个对printf的调用,解释似乎是:

  • Python编译器没有C编译器那样聪明,不能优化此代码。
  • 您的C编译器在编译此12行代码时花费了很长时间。 鉴于您的硬件设置,3秒钟实在太长了! 在我的小笔记本电脑上,使用所有优化编译和运行该代码仅需0.15秒。 您是否以C++方式编译?

禁用优化(-O0)测试C版本会产生此输出:

$ time (clang -O0 -o loop10g loop10g.c && ./loop10g)
Sum is -9877432110

real    4m15.352s
user    3m47.232s
sys     0m3.252s

用未经优化的C比Python快得多:255秒 vs: >3500。

Python代码由字节码和动态类型值的堆栈解释:通常会减慢10到20倍。此外,对于大值,整数算术会自动切换到bignum模式,这可能是这里的情况,尽管惩罚应该更高。


那算作作弊吗?;) - Stargateur
@Stargateur:并不是真的,这个测试很愚蠢。相反,试着计算经典递归定义中的 fib(50)。识别斐波那契函数并将递归转换为迭代代码会在我看来有作弊之嫌。 - chqrlie
1
找出程序员想要做什么并更快地完成它不是“作弊”。这是优化。即使在愚蠢的斐波那契代码上也是如此。 - rici
顺便提一下,据我所知,CPython总是使用bignum表示法。Bignum的位数为30位(对于至少有64位ALU的机器),并且单个leg数字的操作已经进行了一定的优化。此外,还有一个小整数缓存以避免过度分配,但我认为它只能缓存到256。 - rici
@rici:如果我没记错的话,范围是从-5到256。 - wizzwizz4

-2
答案非常简单。Python是一种解释性语言。所有指令都由解释器(执行脚本的特殊程序)执行。它比编译成本地机器代码的C代码慢得多。

2
@wizzwizz4 这就是答案。 - 0___________
1
我猜编译器优化也涉及到了相当大的部分。 - Klaus D.
2
在Python上运行相同的简单计算指令比在C上慢,但并不像观察到的那样明显。1000的倍数只能通过编译器优化大部分迭代来解释。 - Klaus D.
4
原文:The OP didn't say what optimisation was used, but it seems to me a smart optimiser could remove the loop entirely -- since c%2 is 0, b never changes and since b%2 is 1 the effect of the loop is to subtract the loop count from a.翻译:原帖没有说明使用了什么优化,但我认为聪明的优化器可以完全删除循环 - 因为c%2为0,b永远不会改变,并且由于b%2为1,循环的效果是从a中减去循环计数。 - dmuir
2
没有什么能阻止解释器进行静态分析并使用它来进行优化,很多解释器都这样做。 - rici
显示剩余2条评论

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