【问题标题】:Very large execution time differences for virtually same C++ and Python code几乎相同的 C++ 和 Python 代码的执行时间差异很大
【发布时间】:2013-03-16 04:20:38
【问题描述】:

我试图在 Python 中为Problem 12 (Project Euler) 编写解决方案。解决方案太慢了,所以我尝试在互联网上检查其他人的解决方案。我发现this code 是用 C++ 编写的,它与我的 python 代码几乎完全相同,只有一些微不足道的差异。

Python:

def find_number_of_divisiors(n):
    if n == 1:
        return 1

    div = 2 # 1 and the number itself
    for i in range(2, n/2 + 1):
        if (n % i) == 0:
            div += 1
    return div

def tri_nums():
    n = 1
    t = 1
    while 1:
        yield t
        n += 1
        t += n

t = tri_nums()
m = 0
for n in t:
    d = find_number_of_divisiors(n)
    if m < d:
        print n, ' has ', d, ' divisors.'
        m = d

    if m == 320:
        exit(0)

C++:

#include <iostream>

int main(int argc, char *argv[])
{
    unsigned int iteration = 1;
    unsigned int triangle_number = 0;
    unsigned int divisor_count = 0;
    unsigned int current_max_divisor_count = 0;
    while (true) {
        triangle_number += iteration;
        divisor_count = 0;
        for (int x = 2; x <= triangle_number / 2; x ++) {
            if (triangle_number % x == 0) {
                divisor_count++;
            }
        }
        if (divisor_count > current_max_divisor_count) {
            current_max_divisor_count = divisor_count;
            std::cout << triangle_number << " has " << divisor_count
                      << " divisors." << std::endl;
        }
        if (divisor_count == 318) {
            exit(0);
        }

        iteration++;
    }
    return 0;
}

python 代码在我的机器上执行需要 1 分 25.83 秒。而 C++ 代码大约需要 4.628 秒。它的速度快了 18 倍。我曾预计 C++ 代码会更快,但并没有这么大的优势,而且这也只是一个简单的解决方案,它只包含 2 个循环和一堆增量和模块。

虽然我希望得到有关如何解决此问题的答案,但我想问的主要问题是 为什么 C++ 代码要快得多?我在 python 中使用/做错了什么?


用 xrange 替换范围:

将 range 替换为 xrange 后,python 代码大约需要 1 分 11.48 秒才能执行。 (大约快 1.2 倍)

【问题讨论】:

  • 考虑使用xrange 而不是range。也可以考虑使用 C++
  • 真的太晚了,所以我的脑子可能有点模糊,但是找到除数的一个小改进是在你的 for 循环中只去 sqrt(n) 而不是 n/2+ 1 ...但是您每次都必须将 2 添加到 div 。一个用于小于 sqrt(n) 的除数,一个用于其 codivisor(这是一个词吗??)
  • 是的,但他在两个版本中都这样做。
  • 这基本上与编译语言与解释语言有关。由于 C++ 是一种编译语言,它的运行更接近于硬件。解释性语言会产生额外的开销,以使它们以它们的方式运行。
  • 你试过 PyPy 吗?

标签: c++ python execution-time


【解决方案1】:

这正是 C++ 与 Python 相比要大放异彩的那种代码:一个相当紧凑的循环执行算术运算。 (我将在这里忽略算法加速,因为您的 C++ 代码使用相同的算法,而且您似乎没有明确要求...)

C++ 将这种代码编译成相对较少数量的处理器指令(它所做的一切可能都适合 CPU 缓存的超快级别),而 Python 有很多间接级别通过每个操作。例如,每次增加一个数字时,它都会检查该数字是否溢出并且需要移动到更大的数据类型中。

也就是说,一切都不一定会丢失!这也是像PyPy 这样的即时编译器系统擅长的那种代码,因为一旦它通过循环几次,它就会将代码编译成类似于 C++ 代码开始的东西。在我的笔记本电脑上:

$ time python2.7 euler.py >/dev/null
python euler.py  72.23s user 0.10s system 97% cpu 1:13.86 total

$ time pypy euler.py >/dev/null                       
pypy euler.py > /dev/null  13.21s user 0.03s system 99% cpu 13.251 total

$ clang++ -o euler euler.cpp && time ./euler >/dev/null
./euler > /dev/null  2.71s user 0.00s system 99% cpu 2.717 total

使用带有xrange 而不是range 的Python 代码版本。优化级别对我来说对 C++ 代码没有影响,使用 GCC 代替 Clang 也没有。

虽然我们正在这样做,但这也是Cython 可以做得很好的情况,它将几乎 Python 代码编译为使用 Python API 的 C 代码,但在可能的情况下使用原始 C。如果我们通过添加一些类型声明来稍微更改您的代码,并删除迭代器,因为我不知道如何在 Cython 中有效地处理这些,得到 ​​p>

cdef int find_number_of_divisiors(int n):
    cdef int i, div
    if n == 1:
        return 1

    div = 2 # 1 and the number itself
    for i in xrange(2, n/2 + 1):
        if (n % i) == 0:
            div += 1
    return div

cdef int m, n, t, d
m = 0
n = 1
t = 1
while True:
    n += 1
    t += n
    d = find_number_of_divisiors(t)
    if m < d:
        print n, ' has ', d, ' divisors.'
        m = d

    if m == 320:
        exit(0)

然后在我的笔记本电脑上我得到

$ time python -c 'import euler_cy' >/dev/null
python -c 'import euler_cy' > /dev/null  4.82s user 0.02s system 98% cpu 4.941 total

(在 C++ 代码的 2 倍以内)。

【讨论】:

  • 只是为了好玩,因为您尝试了其他所有方法……您可以使用2 + np.count_zero(n % np.arange(2, n/2+1))find_number_of_divisors 进行numpy-vectorize,这应该将充满算术的紧密循环转换为C 代码。通过快速测试,我得到了 7.34 秒——不如 Cython 版本快,但它非常好且可读,并且不需要编译任何东西。
  • 进一步考虑,如果你愿意用大量空间换取更快的速度,你可以通过构建一个二维数组来向量化下一个循环。显然不是整个循环,而是一次N个循环,对于一些合适的N个。
  • 好吧,我无聊了。 pastebin.com/7QkpE56E 在 4.95 秒内完成,而 1D numpy 版本为 7.34 秒……仍然不如 Cython 版本的 3.99 秒。
  • 如果你删除生成器并将所有内容放入函数中,pypy 应该会比这更快。
【解决方案2】:

重写除数计数算法以使用divisor function 使运行时间减少到不到 1 秒。仍然可以让它更快,但不是真的必要。

这是为了表明:在你对语言特性和编译器进行任何优化之前,你应该检查你的算法是否是瓶颈。编译器/解释器的技巧确实非常强大,如 Dougal 的回答所示,Python 和 C++ 之间的差距对于等效代码是封闭的。但是,正如您所看到的,算法的更改立即带来了巨大的性能提升,并将运行时间降低到算法效率低下的 C++ 代码的水平(我没有测试 C++ 版本,而是在我 6 岁的计算机上测试,下面的代码在~0.6s内完成运行)。

以下代码使用 Python 3.2.3 编写和测试。

import math

def find_number_of_divisiors(n):
    if n == 1:
        return 1

    num = 1

    count = 1
    div = 2
    while (n % div == 0):
        n //= div
        count += 1

    num *= count

    div = 3
    while (div <= pow(n, 0.5)):
        count = 1
        while n % div == 0:
            n //= div
            count += 1

        num *= count
        div += 2

    if n > 1:
        num *= 2

    return num

【讨论】:

【解决方案3】:

这是我自己的变体,基于 nhahtdh 的因子计数优化以及我自己的素因子分解代码:

def prime_factors(x):
    def factor_this(x, factor):
        factors = []
        while x % factor == 0:
            x /= factor
            factors.append(factor)
        return x, factors
    x, factors = factor_this(x, 2)
    x, f = factor_this(x, 3)
    factors += f
    i = 5
    while i * i <= x:
        for j in (2, 4):
            x, f = factor_this(x, i)
            factors += f
            i += j
    if x > 1:
        factors.append(x)
    return factors

def product(series):
    from operator import mul
    return reduce(mul, series, 1)

def factor_count(n):
    from collections import Counter
    c = Counter(prime_factors(n))
    return product([cc + 1 for cc in c.values()])

def tri_nums():
    n, t = 1, 1
    while 1:
        yield t
        n += 1
        t += n

if __name__ == '__main__':
    m = 0
    for n in tri_nums():
        d = factor_count(n)
        if m < d:
            print n, ' has ', d, ' divisors.'
            m = d
            if m == 320:
                break

【讨论】:

  • 所以你用 6 (= 2 * 3) 实现了车轮分解。
  • @nhahtdh:这是其中的一部分,但我已经有了质数分解代码。我一般不知道素数和因子数之间的关系,所以这就是我从你的代码中得到的想法。我认为我的公式值得添加,因为可重用组件:素数分解的良好例程,产生系列乘积的函数,组合这些以获得数字中因子计数的函数。如果 OP 打算做 Project Euler 谜题,这段代码会很有帮助。
猜你喜欢
  • 2016-03-11
  • 1970-01-01
  • 2021-03-07
  • 1970-01-01
  • 1970-01-01
  • 2020-05-14
  • 2020-03-15
  • 2020-09-26
  • 1970-01-01
相关资源
最近更新 更多