为什么我的计算在C#中比Python快得多

26

以下是两段简单的代码,分别使用C#Python编写(如果你对这个进程感到好奇,它是Project Euler问题5的解决方案)。

我的问题是,下面的C#代码只需9秒即可迭代完成,而完成Python代码需要283秒(确切地说,Python 3.4.3 - 64位需要283秒,Python 2.7.9 - 32位需要329秒)。

到目前为止,我已经编写了类似的C#Python进程,并且执行时间差异是可以比较的。但是这一次,经过的时间差异极大。

我认为,这种差异的一部分来自于Python语言的灵活变量类型(我怀疑,Python会将一些变量转换为double),但这仍然很难解释。

我做错了什么?

我的系统:Windows-7 64位,

C#-VS Express 2012(9秒)

Python 3.4.3 64位(283秒)

Python 2.7.9 32位(329秒)

C#代码:

using System;

namespace bug_vcs {
    class Program {
        public static void Main(string[] args) {
            DateTime t0 = DateTime.Now;
            int maxNumber = 20;
            bool found = false;
            long start = maxNumber;
            while (!found) {
                found = true;
                int i = 2;
                while ((i < maxNumber + 1) && found) {
                    if (start % i != 0) {
                        found = false;
                    }
                    i++;
                }
                start++;
            }
            Console.WriteLine("{0:d}", start - 1);
            Console.WriteLine("time elapsed = {0:f} sec.", (DateTime.Now - t0).Seconds);
            Console.ReadLine();
        }
    }
}

以及 Python 代码:

from datetime import datetime

t0 = datetime.now()
max_number = 20
found = False
start = max_number
while not found:
    found = True
    i = 2
    while ((i < max_number + 1) and found):
        if (start % i) != 0:
            found = False
        i += 1
    start += 1

print("number {0:d}\n".format(start - 1))

print("time elapsed = {0:f} sec.\n".format((datetime.now() - t0).seconds))

17
在C#中计算执行时间时,应该使用 StopWatch 而不是 DateTime - juharr
9
Python 中的 timeit - jonrsharpe
8
C#编译器生成的经过优化的代码与Python的直接解释执行非常不同。它们是为不同情况而创建的不同工具。 - David Crowell
1
我测试了两个,我认为时间差确实很大。 - Cristian Lupascu
1
尝试使用pypy(http://pypy.org):`python3.4` -> 286秒,pypy -> 10秒。 - Ned Deily
显示剩余10条评论
6个回答

38
答案很简单,Python 处理一切都使用对象,并且默认情况下不具备 JIT。因此,与其通过修改栈上的少量字节并优化代码的热部分(即迭代)来高效执行 - Python 使用表示数字的丰富对象,并没有进行即时优化。
如果您尝试在具有 JIT 的 Python 变体中运行此代码(例如 PyPy),我保证您会看到巨大的差异。
一个通用的提示是避免在非常计算密集的操作中使用标准 Python(特别是如果这是为多个客户端提供请求服务的后端)。Java、C#、JavaScript 等,配合 JIT 使用效率非常高。
顺便说一下,如果您想以更 Pythonic 的方式编写示例,可以这样做:
from datetime import datetime
start_time = datetime.now()

max_number = 20
x = max_number
while True:
    i = 2
    while i <= max_number:
        if x % i: break
        i += 1
    else:
        # x was not divisible by 2...20
        break
    x += 1

print('number:       %d' % x)
print('time elapsed: %d seconds' % (datetime.now() - start_time).seconds)

上面的代码在90秒内完成了执行。之所以更快,是因为看似愚蠢的事情,比如xstart短,我不经常分配变量,并且我依靠Python自己的控制结构而不是变量检查来跳入/跳出循环。

7
同时,我已经下载了pypy并尝试了我的原始代码;执行时间降至10秒。然后尝试了你的代码(同样在pypy上),将时间提高到了5秒。因此,这与Python本身有关。 - ssd
3
是的,正如我在答案中暗示的那样,PyPy具有JIT,可以在更低的层面上对代码进行优化。这消除了Python的许多问题,例如它不会像使用运算符方法在对象上执行数学运算,而是直接在栈上的整数上执行数学运算(就像C#一样)。谈到栈,纯Python将变量存储在字典中,因此即使只是将“start”更改为“x”,也可以提高纯Python的性能。 - Blixt
1
其实我想了想,可能是我把Python和其他解释型语言混淆了,关于变量名对性能的影响。Python可能会在解释步骤中优化变量查找,使得变量名不会影响性能。无论如何,我帖子中的其他内容应该是准确的。 - Blixt
2
我发现使用 while some_expression: 循环几乎总是可以通过使用 for item in some_iterable:for i in range(...): 来加速。在这种情况下,for i in range(2, max_number + 1): 效果很好。通过使用 for x in itertools.count(max_number): 重写外部的 while True 循环,可以获得更小的改进。 - Kevin J. Chase

6
TL;DR: 这篇文章是我试图捍卫我选择的编程语言 Python,反对 C# 的冗长文章。在这个例子中,C# 的表现更好,但仍需要更多代码行来完成同样数量的工作,但最终的性能优势是,当正确编码时,C# 比 Python 快约5倍。最终结果是您应该使用适合自己的语言。
当我运行 C# 例子时,在我的机器上大约需要 3 秒才能完成,并给出了 232,792,560 的结果。可以使用已知事实进行优化,即只有当数字是20的倍数时,才能被1到20之间的数字整除,因此不需要逐个增加1,而是增加20。这个单一的优化使得代码在短短的353毫秒内执行得更快。
当我运行 Python 例子时,我放弃等待并尝试编写自己的版本,使用 itertools,但没有取得更好的效果,而且大约需要与你的示例相同的时间。然后我找到了一个可接受的 itertools 版本,如果考虑到只有最大数的倍数才能被从最小到最大的所有数字整除。因此,Python(3.6)的改进代码如下,带有打印执行所需秒数的装饰器定时函数:
import time
from itertools import count, filterfalse


def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        print(time.time() - start)
        return res
    return wrapper


@timer
def test(stop):
    return next(filterfalse(lambda x: any(x%i for i in range(2, stop)), count(stop, stop)))


print("Test Function")
print(test(20))
# 11.526668787002563
# 232792560

这也让我想起了最近在CodeFights上需要用Python中的最大公约数函数来回答最小公倍数问题的一个问题。代码如下:
import time
from fractions import gcd
from functools import reduce


def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        res = func(*args, **kwargs)
        print(time.time() - start)
        return res
    return wrapper


@timer
def leastCommonDenominator(denominators):
    return reduce(lambda a, b: a * b // gcd(a, b), denominators)


print("LCM Function")
print(leastCommonDenominator(range(1, 21)))
# 0.001001596450805664
# 232792560

在大多数编程任务中,有时最简单的方法并不总是最快的。不幸的是,这次尝试Python时真的很明显。话虽如此,Python的美妙之处在于获得高性能执行的简单性,在C#中需要10行代码,我能够通过(可能)一个一行的lambda表达式返回正确的答案,并且比我在C#上进行简单优化还要快300倍。虽然我不是C#专家,但在此实现相同方法的代码和其结果(比Python快约5倍)如下:

using System;
using System.Diagnostics;

namespace ConsoleApp1
{
    class Program
    {
        public static void Main(string[] args)
        {
            Stopwatch t0 = new Stopwatch();
            int maxNumber = 20;

            long start;
            t0.Start();
            start = Orig(maxNumber);
            t0.Stop();

            Console.WriteLine("Original | {0:d}, {1:d}", maxNumber, start);
            // Original | 20, 232792560
            Console.WriteLine("Original | time elapsed = {0}.", t0.Elapsed);
            // Original | time elapsed = 00:00:02.0585575

            t0.Restart();
            start = Test(maxNumber);
            t0.Stop();

            Console.WriteLine("Test | {0:d}, {1:d}", maxNumber, start);
            // Test | 20, 232792560
            Console.WriteLine("Test | time elapsed = {0}.", t0.Elapsed);
            // Test | time elapsed = 00:00:00.0002763

            Console.ReadLine();
        }

        public static long Orig(int maxNumber)
        {
            bool found = false;
            long start = 0;
            while (!found)
            {
                start += maxNumber;
                found = true;
                for (int i=2; i < 21; i++)
                {
                    if (start % i != 0)
                        found = false;
                }
            }
            return start;
        }

        public static long Test(int maxNumber)
        {
            long result = 1;

            for (long i = 2; i <= maxNumber; i++)
            {
                result = (result * i) / GCD(result, i);
            }

            return result;
        }

        public static long GCD(long a, long b)
        {
            while (b != 0)
            {
                long c = b;
                b = a % b;
                a = c;
            }

            return a;
        }
    }
}

然而,对于大多数高级任务,与.NET实现相比,我通常看到Python表现异常出色,尽管我目前无法证实这些说法,除了说Python Requests库在性能方面给我带来了两倍到三倍的回报,相比之下,以相同方式编写的C# WebRequest。当编写Selenium进程时,这也是真实的,因为我可以在Python中以100毫秒或更短的时间读取文本元素,但每个元素检索需要C# > 1秒才能返回。话虽如此,由于其面向对象的方法,我实际上更喜欢C#的实现,而Python的Selenium实现则采用函数式方法,有时很难阅读。


6
尝试使用类似pypy、numba或cython之类的Python JIT实现,如果您想要像C一样快速但牺牲一些代码可读性。例如,在pypy中。
# PyPy

number 232792560

time elapsed = 4.000000 sec.

例如在Cython中

# Cython

number 232792560

time elapsed = 1.000000 sec.

Cython源代码:

from datetime import datetime

cpdef void run():
    t0 = datetime.now()
    cdef int max_number = 20
    found = False
    cdef int start = max_number
    cdef int i
    while not found:
        found = True
        i = 2
        while ((i < max_number + 1) and found):
            if (start % i) != 0:
                found = False
            i += 1
        start += 1

    print("number {0:d}\n".format(start - 1))

    print("time elapsed = {0:f} sec.\n".format((datetime.now() - t0).seconds))

5

一些人认为最好的方法是使用JIT实现。我知道这是一个老话题,但我对不同实现之间的执行时间差异很感兴趣,所以我在Jupiter Notebook中使用了Numba和Cython进行了一些测试,以下是我的结果:

您的代码嵌入到函数中

%%time

def test():
    max_number = 20
    found = False
    start = max_number
    while not found:
        found = True
        i = 2
        while ((i < max_number + 1) and found):
            if (start % i) != 0:
                found = False
            i += 1
        start += 1
    return start-1
test()

CPU时间:用户1分钟18秒,系统:462毫秒,总计:1分钟19秒 墙上时间:1分钟21秒

@Blixt的写作方式

%%time

def test():
    max_number = 20
    x = max_number
    while True:
        i = 2
        while i <= max_number:
            if x % i: break
            i += 1
        else:
            # x was not divisible by 2...20
            break
        x += 1
    
    return x
    
test()

CPU时间:用户40.1秒,系统305毫秒,总共40.4秒 墙壁时间:41.9秒

Numba

%%time
from numba import jit

@jit(nopython=True)
def test():
    max_number = 20
    x = max_number
    while True:
        i = 2
        while i <= max_number:
            if x % i: break
            i += 1
        else:
            # x was not divisible by 2...20
            break
        x += 1
    
    return x
    
test()

CPU时间:用户用时4.48秒,系统用时70.5毫秒,总共用时4.55秒。 Wall时间:用时5.01秒。

使用函数签名的Numba

%%time
from numba import jit, int32

@jit(int32())
def test():
    max_number = 20
    x = max_number
    while True:
        i = 2
        while i <= max_number:
            if x % i: break
            i += 1
        else:
            # x was not divisible by 2...20
            break
        x += 1
    
    return x
    
test()

CPU时间:用户3.56秒,系统43.1毫秒,总共3.61秒 墙上时间:3.79秒

Cython

%load_ext Cython

%%time
%%cython

def test():
    cdef int max_number = 20
    cdef int x = max_number
    cdef int i = 2
    while True:
        i = 2
        while i <= max_number:
            if x % i: break
            i += 1
        else:
            # x was not divisible by 2...20
            break
        x += 1
    
    return x
    
test()

CPU时间:用户 617 毫秒,系统:20.7 毫秒,总计:637 毫秒 墙壁时间:1.31 秒


3

Python(以及包括Matlab在内的所有脚本语言)并不适用于大规模数值计算。为了得到与编译程序兼容的结果,必须尽可能避免使用循环,并将公式转换为矩阵格式(这需要一些数学理解和技能),以便我们可以利用numpy、scipy等提供的后台C库进行尽可能多的推动。

再次强调,不要在Python中为数值计算编写循环,只要有可能使用矩阵等效方法!


-2

首先,您需要更改算法来解决这个问题:

#!/usr/bin/env python

import sys
from timeit import default_timer as timer

pyver = sys.version_info;

print(">")
print(">  Smallest multiple of 2 ... K");
print(">")
print(">  Python version, interpreter version: {0}.{1}.{2}-{3}-{4}".format(
    pyver.major, pyver.minor, pyver.micro, pyver.releaselevel, pyver.serial))
print(">")

K = 20;

print("  K = {0:d}".format(K))
print("")

t0 = timer()

N = K
NP1 = N + 1
N2 = (N >> 1) + 1
vec = range(0, NP1)
smalestMultiple = 1

for i in range(2, N2):
    divider = vec[i]
    if divider == 1:
        continue
    for j in range(i << 1, NP1, i):
        if (vec[j] % divider) == 0:
            vec[j] /= divider

for i in range(2, NP1):
    if vec[i] != 1:
        smalestMultiple = smalestMultiple * vec[i]

t1 = timer()

print("  smalest multiple = {0:d}".format(smalestMultiple))
print("  time elapsed = {0:f} sec.".format(t1 - t0))

在Linux/Fedora 28/Intel(R) Core(TM) i7-2760QM CPU @ 2.40GHz上的输出:

>  Smallest multiple of 2 ... K
>
>  Python version, interpreter version: 2.7.15-final-0
>
>  K = 20
>
>  smalest multiple = 232792560
>  time elapsed = 0.000032 sec.

我不需要一个快速算法。我的问题是,为什么看似相同的算法在不同的时间间隔内运行(差异非常大),使用不同的编程语言。我知道这个代码(https://codeshare.io/2W4NBb)确实运行得非常快。 - ssd

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