5

我的 Python 程序太慢了。因此,我对其进行了分析,发现大部分时间都花在了一个计算两点之间距离的函数上(一个点是 3 个 Python 浮点数的列表):

def get_dist(pt0, pt1):
    val = 0
    for i in range(3):
        val += (pt0[i] - pt1[i]) ** 2
    val = math.sqrt(val)
    return val

为了分析这个函数为什么这么慢,我编写了两个测试程序:一个用 Python 编写,一个用 C++ 编写,它们执行类似的计算。他们计算 100 万对点之间的距离。(下面是 Python 和 C++ 中的测试代码。)

Python 计算需要 2 秒,而 C++ 需要 0.02 秒。相差100倍!

对于如此简单的数学计算,为什么 Python 代码比 C++ 代码慢得多?如何加快速度以匹配 C++ 性能?

用于测试的 Python 代码:

import math, random, time

num = 1000000

# Generate random points and numbers

pt_list = []
rand_list = []

for i in range(num):
    pt = []
    for j in range(3):
        pt.append(random.random())
    pt_list.append(pt)
    rand_list.append(random.randint(0, num - 1))

# Compute

beg_time = time.clock()
dist = 0

for i in range(num):
    pt0 = pt_list[i]
    ri  = rand_list[i]
    pt1 = pt_list[ri]

    val = 0
    for j in range(3):
        val += (pt0[j] - pt1[j]) ** 2
    val = math.sqrt(val)

    dist += val

end_time = time.clock()
elap_time = (end_time - beg_time)

print elap_time
print dist

用于测试的 C++ 代码:

#include <cstdlib>
#include <iostream>
#include <ctime>
#include <cmath>

struct Point
{
    double v[3];
};

int num = 1000000;

int main()
{
    // Allocate memory
    Point** pt_list = new Point*[num];
    int* rand_list = new int[num];

    // Generate random points and numbers
    for ( int i = 0; i < num; ++i )
    {
        Point* pt = new Point;

        for ( int j = 0; j < 3; ++j )
        {
            const double r = (double) rand() / (double) RAND_MAX;
            pt->v[j] = r;
        }

        pt_list[i] = pt;
        rand_list[i] = rand() % num;
    }

    // Compute

    clock_t beg_time = clock();
    double dist = 0;
    for ( int i = 0; i < num; ++i )
    {
        const Point* pt0 = pt_list[i];
        int r = rand_list[i];
        const Point* pt1 = pt_list[r];

        double val = 0;
        for ( int j = 0; j < 3; ++j )
        {
            const double d = pt0->v[j] - pt1->v[j];
            val += ( d * d );
        }

        val = sqrt(val);
        dist += val;
    }
    clock_t end_time = clock();
    double sec_time = (end_time - beg_time) / (double) CLOCKS_PER_SEC;

    std::cout << sec_time << std::endl;
    std::cout << dist << std::endl;

    return 0;
}
4

5 回答 5

6

一系列优化:

原始代码,稍作改动

import math, random, time

num = 1000000

# Generate random points and numbers

# Change #1: Sometimes it's good not to have too much randomness.
# This is one of those cases.
# Changing the code shouldn't change the results.
# Using a fixed seed ensures that the changes are valid.
# The final 'print dist' should yield the same result regardless of optimizations.
# Note: There's nothing magical about this seed.
# I randomly picked a hash tag from a git log.
random.seed (0x7126434a2ea2a259e9f4196cbb343b1e6d4c2fc8)
pt_list = []
rand_list = []

for i in range(num):
    pt = []
    for j in range(3):
        pt.append(random.random())
    pt_list.append(pt)

# Change #2: rand_list is computed in a separate loop.
# This ensures that upcoming optimizations will get the same results as
# this unoptimized version.
for i in range(num):
    rand_list.append(random.randint(0, num - 1))

# Compute

beg_time = time.clock()
dist = 0

for i in range(num):
    pt0 = pt_list[i]
    ri  = rand_list[i]
    pt1 = pt_list[ri]

    val = 0
    for j in range(3):
        val += (pt0[j] - pt1[j]) ** 2
    val = math.sqrt(val)

    dist += val

end_time = time.clock()
elap_time = (end_time - beg_time)

print elap_time
print dist


优化#1:将代码放入函数中。

第一个优化(未显示)是将除 之外的所有代码嵌入到import函数中。这个简单的更改为我的计算机提供了 36% 的性能提升。


优化#2:避开**运营商。

你不要pow(d,2)在你的 C 代码中使用,因为每个人都知道这在 C 中是次优的。它在 python 中也是次优的。Python**很聪明;它评估x**2x*x。然而,聪明是需要时间的。你知道你想要d*d,所以使用它。这是具有该优化的计算循环:

for i in range(num):
    pt0 = pt_list[i]
    ri  = rand_list[i]
    pt1 = pt_list[ri]

    val = 0 
    for j in range(3):
        d = pt0[j] - pt1[j]
        val += d*d 
    val = math.sqrt(val)

    dist += val 


优化#3:pythonic。

你的 Python 代码看起来很像你的 C 代码。你没有利用语言。

import math, random, time, itertools

def main (num=1000000) :
    # This small optimization speeds things up by a couple percent.
    sqrt = math.sqrt

    # Generate random points and numbers

    random.seed (0x7126434a2ea2a259e9f4196cbb343b1e6d4c2fc8)

    def random_point () :
        return [random.random(), random.random(), random.random()]

    def random_index () :
       return random.randint(0, num-1)

    # Big optimization:
    # Don't generate the lists of points.
    # Instead use list comprehensions that create iterators.
    # It's best to avoid creating lists of millions of entities when you don't
    # need those lists. You don't need the lists; you just need the iterators.
    pt_list = [random_point() for i in xrange(num)]
    rand_pts = [pt_list[random_index()] for i in xrange(num)]


    # Compute

    beg_time = time.clock()
    dist = 0 

    # Don't loop over a range. That's too C-like.
    # Instead loop over some iterable, preferably one that doesn't create the
    # collection over which the iteration is to occur.
    # This is particularly important when the collection is large.
    for (pt0, pt1) in itertools.izip (pt_list, rand_pts) :

        # Small optimization: inner loop inlined,
        # intermediate variable 'val' eliminated.
        d0 = pt0[0]-pt1[0]
        d1 = pt0[1]-pt1[1]
        d2 = pt0[2]-pt1[2]

        dist += sqrt(d0*d0 + d1*d1 + d2*d2)

    end_time = time.clock()
    elap_time = (end_time - beg_time)

    print elap_time
    print dist


更新

优化#4,使用 numpy

以下在代码的定时部分占用了原始版本的大约 1/40 时间。不如 C 快,但接近。

请注意注释掉的“Mondo 慢”计算。这大约是原始版本的十倍。使用 numpy 会产生间接费用。与我的非 numpy 优化 #3 中的设置相比,随后的代码中的设置需要更长的时间。

底线:使用 numpy 时需要小心,而且设置成本可能很高。

import numpy, random, time

def main (num=1000000) :

    # Generate random points and numbers

    random.seed (0x7126434a2ea2a259e9f4196cbb343b1e6d4c2fc8)

    def random_point () :
        return [random.random(), random.random(), random.random()]

    def random_index () :
       return random.randint(0, num-1)

    pt_list = numpy.array([random_point() for i in xrange(num)])
    rand_pts = pt_list[[random_index() for i in xrange(num)],:]

    # Compute

    beg_time = time.clock()

    # Mondo slow.
    # dist = numpy.sum (
    #            numpy.apply_along_axis (
    #                numpy.linalg.norm, 1, pt_list - rand_pts))

    # Mondo fast.
    dist = numpy.sum ((numpy.sum ((pt_list-rand_pts)**2, axis=1))**0.5)

    end_time = time.clock()
    elap_time = (end_time - beg_time)

    print elap_time
    print dist
于 2013-04-25T14:21:02.433 回答
4

一些一般提示:

将所有代码移入 main() 函数并使用正常的

if __name__ == "__main__":
    main()

构造。由于范围可变,它大大提高了速度。请参阅为什么 Python 代码在函数中运行得更快?解释为什么。

不要使用range(),因为它会立即生成完整的范围,这对于大量数据来说很慢;而是使用xrange()which 使用生成器。

于 2013-04-25T09:09:57.263 回答
3

Python 不是一种快速的语言,它不会产生“计算机代码”,它是在 Python 虚拟机中运行的。“一切”都是对象,所以你没有像 C 中那样的静态类型。只有这会减慢它的速度。- 无论如何,那不是我的领域,所以我不会多说。

你应该考虑 PyPy、Cython,甚至可能用 C 编写一个 python 扩展。

我在 PyPy 中运行代码,使用的时间是 250 毫秒 <-- 这就是你要找的吗?我为 Cython 编写了一个快速测试,并设法将其降低到 500 毫秒。

所以最好的选择是使用 PyPy,或者当速度非常重要时使用 Cython。

于 2013-04-25T09:21:40.780 回答
2

您不能期望在 Python 中与 C++ 性能相匹配,但是您可以稍微调整 Python 代码以使其更快:

def get_dist(pt0, pt1):
    val = 0
    for i in range(3):
        val += (pt0[i] - pt1[i]) ** 2
    val = math.sqrt(val)
    return val

此代码的for循环版本和您的 C++for循环完全不同。Python 版本创建一个列表,然后对其进行迭代,而 C++ 版本只是增加一个变量。如果你想加快 Python 版本的速度,最好的方法是显式地写出来以节省 Pythonfor循环的开销。

def get_dist(pt0, pt1, sqrt=math.sqrt): # cache function at definition time
    return sqrt((pt0[0] - pt1[0]) ** 2 + (pt0[1] - pt1[1]) ** 2 + (pt0[2] - pt1[2]) ** 2)

numpy对于该特定功能,这可能与您可以获得(不使用)一样快,您也可以在主代码中改进其他内容。

于 2013-04-25T09:11:24.543 回答
0

这个页面变得非常混乱,大多数答案实际上都在评论中,所以这里是可能的优化的快速概述:

  • Jamlak 的回答:优化你的 python 代码:

    def get_dist(pt0, pt1, sqrt=math.sqrt):  # cache function at definition time
        return sqrt((pt0[0] - pt1[0]) ** 2 + (pt0[1] - pt1[1]) ** 2 + (pt0[2] - pt1[2]) ** 2) 
    
  • 使用numpy模块进行计算

  • 使用pypy而不是 CPython运行代码
  • 使用Cython编译对时间要求严格的代码
于 2013-04-25T10:00:56.203 回答