【问题标题】:Why is computing point distances so slow in Python?为什么在 Python 中计算点距离这么慢?
【发布时间】:2013-04-25 09:00:27
【问题描述】:

我的 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;
}

【问题讨论】:

  • 因为编译代码总是会击败字节码解释的动态语言?使用 numpy 跨如此大的数据集进行计算。
  • 不是您的问题的答案,但是,您是否考虑过使用 numpy?
  • @Ashwin:您在这里并没有完全使用 Python 的优势,您的代码也不是最有效的。使用局部作用域与全局作用域会有所不同,展开循环并避免属性取消引用也会有所帮助。
  • 您也可以尝试使用 pypy 运行此代码。编辑:对我来说 pypy 比 cpython 快 6.5 倍
  • 在比较 C 和 CPython 时,特别希望“简单数学计算”有 100 倍的差异。如果一个模块在不使用 numpy 的情况下产生数百万个 3D 点;编写一个包装器来获取 numpy 数组。如果您将循环保留在纯 Python 中,Cython 不会帮助您实现 C 性能(与 for i in xrange(num) 开销相比,在 Cython 中实现的get_dist() 几乎是瞬时的(在我的机器上 num=1000000 为 14 毫秒))。 Cython 与 numpy 数组的互操作性非常好。如果您不能将计算表达为向量化的 numpy 操作,则可以使用 Cython。

标签: python performance


【解决方案1】:

一系列优化:

原始代码,稍作改动

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:避开 ** 运算符。

您不要在 C 代码中使用 pow(d,2),因为每个人都知道这在 C 中是次优的。在 python 中也是次优的。 Python 的** 很聪明;它将x**2 评估为x*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

【讨论】:

  • 优秀的答案。优化#4:使用 numpy 提高性能怎么样? :-)
  • 太棒了。决赛 #5: Cython 怎么样 :-)
【解决方案2】:

一些一般提示:

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

if __name__ == "__main__":
    main()

构造。由于范围可变,它大大提高了速度。 请参阅 Why does Python code run faster in a function? 了解原因。

不要使用range(),因为它会一次生成完整的范围,这对于大数字来说很慢;而是使用使用生成器的xrange()

【讨论】:

  • 是的,这有帮助。它减少到 1.34 秒。仍然比 C 代码慢约 100 倍。
  • @FredrikPihl 一个点是 3 个 Python 浮点数的列表。 使用 xrange 会减慢它的速度,因为在这种小数字的情况下生成器开销
  • 我更多的是想到for i in range(num)这一行,其中num是1000000。如果是python3,是的,那么范围与xrange相同。将代码移入 main() 仍然有效。
  • @FredrikPihl 感谢这个我不知道的提示。但遗憾的是,这对我的问题没有帮助,因为 get_dist 是一个更大程序的一部分。请参阅我更新的问题。
【解决方案3】:

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

您应该考虑使用 PyPy、Cython,甚至可以用 C 语言编写 python 扩展。

我在 PyPy 中运行代码,所用时间为 250 毫秒

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

【讨论】:

  • 我不明白为什么这被否决了。 Cython 是在需要速度时优化代码的好方法。它易于处理且易于维护。
  • 我现在不能使用 CPython 以外的任何东西。我的实际程序是使用大量第三方库构建的。
  • @Ashwin:Cython 不是一个单独的实现。 Cython 与 CPython一起工作;您使用 Python 的子集,然后将其编译为优化的 C,然后在 在 CPython 内 运行该扩展。
  • @MartijnPieters 我现在意识到 :-) 我认为它是另一个 Python 实现,例如 Jython 和 IronPython。
【解决方案4】:

您不能期望在 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 版本的速度,最好的方法是显式写出来,以节省 Python for 循环的开销。

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),您还可以在主代码中改进其他方面。

【讨论】:

  • 这可能是你能做到的最快速度。使用 numpy 将使这很多更快,而无需大量迁移到 C 或 C++。
  • @MartijnPieters 哦,对了,我应该提到“不使用 numpy”,谢谢
  • 另外,您可以使用get_dist(pt0, pt1, sqrt=math.sqrt): return sqrt((pt0[0] - pt1[0]) ** 2 + (pt0[1] - pt1[1]) ** 2 + (pt0[2] - pt1[2]) ** 2)“缓存”sqrt 函数
  • 这个link 说 math.sqrt() 更快
  • 哦不知道,我去改一下
【解决方案5】:

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

  • Jamlak’s answer:优化你的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 编译时间要求严格的代码

【讨论】:

  • 您错过了“将代码移动到函数中”。在我的系统上运行提供的代码:4.8s,将其他未更改的代码移动到main():3.5s,使用 pypy 0.65s 运行它。 numpy 可能有更多帮助,但需要更改代码。
猜你喜欢
  • 2015-07-27
  • 1970-01-01
  • 2011-01-13
  • 2012-02-24
  • 2018-10-08
  • 1970-01-01
  • 1970-01-01
  • 2021-01-16
相关资源
最近更新 更多