【问题标题】:Why does a large for loop with 10 billion iterations take a much longer time to run in Python than in C?为什么在 Python 中运行具有 100 亿次迭代的大型 for 循环比在 C 中运行的时间要长得多?
【发布时间】:2019-03-05 07:53:07
【问题描述】:

我目前正在比较 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 会怎样?是不是一样慢?
  • 因为 C 是在本机机器代码中编译的,已针对您当前的架构进行了高度优化并直接在硬件上运行,而 Python 是一种解释型语言,不做任何事情。
  • @skyboyer 注意用&amp;1 替换%2 在C 中创建不同的功能。%2 导致1,0,-1&amp;1 导致 1,0
  • @chux,谢谢,不知道。当它返回-1时,你能给我举个例子吗?只是好奇,第一次尝试找不到
  • @skyboyer printf("%d\n", -1%2);What's the difference between “mod” and “remainder”?

标签: python c python-3.x


【解决方案1】:

正如 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++ 编译的吗?

在禁用优化的情况下测试 C 版本 (-O0) 会产生以下输出:

$ 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:不是真的,测试很愚蠢。而是尝试使用经典的递归定义计算fib(50)。识别斐波那契函数并将递归转换为迭代代码将欺骗恕我直言。
  • 弄清楚程序员想要做什么并且做得更快并不是“作弊”。是优化。即使它用于愚蠢的斐波那契代码。
  • 顺便说一下,CPython 总是使用 bignum 表示,afaik。 Bignum 腿是 30 位(在 ALU 至少为 64 位的机器上),并且对单腿数的操作进行了一些优化。此外,还有一个小整数缓存以避免过度分配,但我认为它最多只能达到 256)。
  • @rici -5256, iirc。
【解决方案2】:

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

实际情况是,您的 C 代码可以更改数字的值,但在 Python 中,数字是不可变的,这意味着它们不会改变。这意味着当你进行求和时,Python 必须为每个新值创建一个新的int 对象,然后在它们不再使用后销毁旧的ints。这比仅仅修改单个内存值要慢得多。


还有一种可能是你的 C 编译器很聪明,并且通过一系列优化它可以完全删除你的 for 循环,结果将是相同的——就像循环实际运行一样。如果在您的示例中是这种情况,我希望代码运行得更快,但它可以这样做。

Python 没有这样的智能编译器。它不能做那么伟大和聪明的事情;它只是不是为了优化代码而设计的,因为在动态类型语言中很难可靠地做到这一点(尽管 Python 是强类型的事实确实使它成为可能。

【讨论】:

  • 这意味着。这只是实现细节
  • 这是真的吗?使用对象类型而不是整数的纯值,并且在对象无用时没有优化对象的机制?为什么???
  • @R.. Python 不是为速度而设计的。如果你想在 Python 中使用可变整数,你可以将其添加为扩展,但如果由于 Python 的默认引用模型,默认情况下它会像这样工作,这会让大多数程序员感到困惑。
  • 这不是“想要可变整数”的问题。没有人想要那样。这是一个需要没有对象或对象生命周期的纯值的问题,因为这是一个没有语义差异的巨大层低效率。
  • @R.. 我希望看到它完成。如果你在 GitHub 上 fork Python 并尝试实现它,我会提供帮助。
【解决方案3】:

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

【讨论】:

  • 这不是答案。它是其中的一部分,但大部分开销是由整数的不变性引起的内存分配和释放引起的,这是由于对象模型而不是解释性。
  • @wizzwizz4 就是答案
  • 在 Python 上运行 same 简单计算指令比在 C 上慢,但没有达到观察到的程度。 1000 的系数只能通过编译器优化大部分迭代来解释。
  • OP 没有说明使用了什么优化,但在我看来,一个聪明的优化器可以完全删除循环——因为 c%2 是 0,b 永远不会改变,因为 b%2 是1 循环的作用是从a中减去循环计数。
  • 没有什么能阻止解释器进行静态分析并使用它进行优化。很多人都这样做。
猜你喜欢
  • 2021-07-20
  • 2011-12-27
  • 2015-03-03
  • 2015-08-19
  • 2020-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多