numpy float:比算术运算中的内置慢10倍?

时间:2011-05-10 21:37:47

标签: python performance numpy floating-point

我对以下代码的时间非常奇怪:

import numpy as np
s = 0
for i in range(10000000):
    s += np.float64(1) # replace with np.float32 and built-in float
  • 内置浮球:4.9秒
  • float64:10.5 s
  • float32:45.0 s

为什么float64float慢两倍?为什么float32比float64慢5倍?

有没有办法避免使用np.float64的惩罚,让numpy函数返回内置float而不是float64

我发现使用numpy.float64比使用Python的浮点慢得多,numpy.float32甚至更慢(即使我使用的是32位计算机)。

在我的32位计算机上

numpy.float32。因此,每次我使用numpy.random.uniform等各种numpy函数时,我都会将结果转换为float32(以便以32位精度执行进一步的操作)。

有没有办法在程序或命令行中的某处设置单个变量,并使所有numpy函数返回float32而不是float64

编辑#1:

numpy.float64 10次 比算术计算中的浮动慢。它非常糟糕,甚至在计算之前转换为浮动和返回使程序运行速度提高了3倍。为什么?我能做些什么来解决它吗?

我想强调一下,我的时间安排不是由于以下任何原因:

  • 函数调用
  • numpy和python float之间的转换
  • 创建对象

我更新了我的代码,以便更清楚地解决问题所在。使用新代码,我发现使用numpy数据类型可以看到十倍的性能:

from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

for i in range(10000000):
    s = (s + 8) * s % 2399232

print(s)
print('Runtime:', datetime.now() - START_TIME)

时间安排如下:

  • float64:34.56s
  • float32:35.11s
  • float:3.53s

为了它的地狱,我也尝试过:

从datetime导入日期时间 导入numpy为np

START_TIME = datetime.now()

s = np.float64(1)
for i in range(10000000):
    s = float(s)
    s = (s + 8) * s % 2399232
    s = np.float64(s)

print(s)
print('Runtime:', datetime.now() - START_TIME)

执行时间为13.28秒;实际上,将float64转换为float并返回比使用原样快3倍。尽管如此,转换也会造成损失,因此与纯蟒float相比总体上要慢3倍。

我的机器是:

  • Intel Core 2 Duo T9300(2.5GHz)
  • WinXP Professional(32位)
  • ActiveState Python 3.1.3.5
  • Numpy 1.5.1

编辑#2:

感谢您的回答,他们帮助我了解如何处理这个问题。

但我仍然想知道确切的原因(可能基于源代码)为什么下面的代码使用float64比使用float慢10倍。

编辑#3:

我在Windows 7 x64(英特尔酷睿i7 930 @ 3.8GHz)下重新运行代码。

同样,代码是:

from datetime import datetime
import numpy as np

START_TIME = datetime.now()

# one of the following lines is uncommented before execution
#s = np.float64(1)
#s = np.float32(1)
#s = 1.0

for i in range(10000000):
    s = (s + 8) * s % 2399232

print(s)
print('Runtime:', datetime.now() - START_TIME)

时间安排如下:

  • float64:16.1s
  • float32:16.1s
  • float:3.2s

现在两个np浮点数(64或32)都比内置float慢5倍。仍然是一个显着的差异。我正在试图找出它的来源。

结束编辑

7 个答案:

答案 0 :(得分:44)

CPython浮动按块分配

将numpy标量分配与float类型进行比较的关键问题是CPython总是为大小为N的块中的floatint对象分配内存。

在内部,CPython维护一个块的链接列表,每个块都足够容纳N float个对象。当您调用float(1)时,CPython会检查当前块中是否有可用空间;如果没有,它会分配一个新块。一旦它在当前块中有空间,它只需初始化该空间并返回指向它的指针。

在我的机器上,每个块可以容纳41个float个对象,因此第一个float(1)调用会产生一些开销,但是随着内存的分配和准备就绪,接下来的40个运行速度会快得多。

慢numpy.float32与numpy.float64

看起来numpy在创建标量类型时可以采用2条路径:快速和慢速。这取决于标量类型是否具有可以推迟参数转换的Python基类。

出于某种原因,numpy.float32被硬编码为采用较慢的路径(defined by the _WORK0 macro),而numpy.float64则有机会采用更快的路径(defined by the _WORK1 macro)。请注意,scalartypes.c.src是一个在构建时生成scalartypes.c的模板。

您可以在Cachegrind中将其可视化。我已经添加了屏幕截图,显示了构建float32float64之间的调用次数:

float64采用快速路径

float64 takes the fast path

float32采用慢速路径

float32 takes the slow path

已更新 - 慢速/快速路径采用哪种类型可能取决于操作系统是32位还是64位。在我的测试系统上,Ubuntu Lucid 64位,float64类型比float32快10倍。

答案 1 :(得分:21)

在像这样的繁重循环中使用Python对象操作,无论它们是floatnp.float32,总是很慢。 NumPy对于向量和矩阵的操作非常快,因为所有操作都是由用C语言编写的库的大部分数据执行的,而不是由Python解释器执行的。在解释器中运行的代码和/或使用Python对象总是很慢,并且使用非本机类型会使它更慢。这是可以预期的。

如果您的应用程序运行缓慢且需要对其进行优化,则应尝试将代码转换为直接使用NumPy的矢量解决方案,并且速度很快,或者您可以使用Cython等工具创建快速实现在C中循环。

答案 2 :(得分:11)

也许,这就是为什么你应该直接使用Numpy而不是使用循环。

s1 = np.ones(10000000, dtype=np.float)
s2 = np.ones(10000000, dtype=np.float32)
s3 = np.ones(10000000, dtype=np.float64)

np.sum(s1) <-- 17.3 ms
np.sum(s2) <-- 15.8 ms
np.sum(s3) <-- 17.3 ms

答案 3 :(得分:7)

<强>摘要

如果算术表达式同时包含numpy和内置数字,则Python算法的工作速度会变慢。避免这种转换几乎消除了我报告的所有性能下降。

<强>详情

请注意,在我的原始代码中:

s = np.float64(1)
for i in range(10000000):
  s = (s + 8) * s % 2399232

类型floatnumpy.float64混合在一个表达式中。也许Python必须将它们全部转换为一种类型?

s = np.float64(1)
for i in range(10000000):
  s = (s + np.float64(8)) * s % np.float64(2399232)

如果运行时没有改变(而不是增加),那就表明Python确实在做什么,解释了性能拖累。

实际上,运行时间下降了1.5倍!这怎么可能? Python可能不得不做的最糟糕的事情是这两次转换?

我真的不知道。也许Python必须动态地检查需要转换成什么,这需要花费时间,并被告知要执行的精确转换会使它更快。也许,一些完全不同的机制被用于算术(它根本不涉及转换),并且在不匹配的类型上恰好是超慢的。阅读numpy源代码可能有所帮助,但这超出了我的技能。

无论如何,现在我们可以通过将转换移出循环来显然加快速度:

q = np.float64(8)
r = np.float64(2399232)
for i in range(10000000):
  s = (s + q) * s % r

正如预期的那样,运行时间大幅缩短:再增加2.3倍。

公平地说,我们现在需要通过将文字常量移出循环来稍微更改float版本。这导致微小(10%)的减速。

考虑到所有这些更改,代码的np.float64版本现在仅比同等版本float慢30%;这种荒谬的5倍性能打击基本消失了。

为什么我们仍然看到30%的延迟? numpy.float64数字与float占用的空间相同,因此这不是原因。对于用户定义的类型,算术运算符的分辨率可能更长。当然不是主要问题。

答案 4 :(得分:1)

如果您正在进行快速标量算术,那么您应该关注像gmpy而不是numpy这样的库(正如其他人所指出的那样,后者针对向量运算而不是标量运算更优化)

答案 5 :(得分:1)

我也可以确认结果。我试图看看使用所有numpy类型会是什么样子,并且差异仍然存在。那么,我的测试是:

def testStandard(length=100000):
    s = 1.0
    addend = 8.0
    modulo = 2399232.0
    startTime = datetime.now()
    for i in xrange(length):
        s = (s + addend) * s % modulo
    return datetime.now() - startTime

def testNumpy(length=100000):
    s = np.float64(1.0)
    addend = np.float64(8.0)
    modulo = np.float64(2399232.0)
    startTime = datetime.now()
    for i in xrange(length):
        s = (s + addend) * s % modulo
    return datetime.now() - startTime

所以在这一点上,numpy类型都相互交互,但10x的差异仍然存在(2秒vs 0.2秒)。

如果我不得不猜测,我会说有两个可能的原因导致默认浮点类型更快。第一种可能性是python在处理某些数字操作或一般循环(例如循环展开)的情况下进行了重要的优化。第二种可能性是numpy类型涉及额外的抽象层(即必须从地址读取)。为了研究每种效果,我做了一些额外的检查。

一个区别可能是python必须采取额外步骤来解析float64类型的结果。与生成高效表的编译语言不同,python 2.6(可能还有3个)在解决您通常认为是免费的事物方面需要付出巨大的代价。即使是简单的X.a分辨率也必须在每次调用时解析点运算符。 (这就是为什么如果你有一个调用instance.function()的循环,你最好在循环外声明一个变量“function = instance.function”。

根据我的理解,当你使用python标准运算符时,这些与使用“import operator”中的运算符非常相似。如果用+,*和%替换add,mul和mod,你会看到与标准运算符(两种情况)相比,静态性能下降约0.5秒。这意味着通过包装运算符,标准的python浮点运算速度会慢3倍。如果你再做一次,使用operator.add和那些变量大约增加0.7秒(超过1米的试验,分别从2秒和0.2秒开始)。那是5倍的缓慢。所以基本上,如果这些问题中的每一个都发生两次,那么你基本上要慢10倍。

所以让我们假设我们暂时是python解释器。情况1,我们对原生类型进行操作,假设a + b。在引擎盖下,我们可以检查a和b的类型,并将我们的补充发送到python的优化代码。情况2,我们有另外两种类型的操作(也是a + b)。在引擎盖下,我们检查它们是否是原生类型(它们不是)。我们继续讨论'其他'案例。 else案例向我们发送了类似。添加(b)的内容。 a。添加然后可以调度numpy的优化代码。所以在这一点上,我们有额外的分支额外开销,一个'。'获取slots属性和函数调用。我们只进入了加法操作。然后我们必须使用结果来创建一个新的float64(或改变现有的float64)。同时,python本机代码可能通过专门处理其类型来欺骗,以避免这种开销。

基于上面对python函数调用和范围开销的成本的检查,numpy很容易因为它的c数学函数而导致9x的惩罚。我完全可以想象这个过程比简单的数学运算调用要花费很多倍。对于每个操作,numpy库必须遍历python层以进入其C实现。

所以在我看来,这个原因可能是因为这个原因:

length = 10000000
class A():
    X = 10
startTime = datetime.now()
for i in xrange(length):
    x = A.X
print "Long Way", datetime.now() - startTime
startTime = datetime.now()
y = A.X
for i in xrange(length):
    x = y
print "Short Way", datetime.now() - startTime

这个简单的案例显示0.2秒对0.14秒的差异(明显更短的方式)。我认为你所看到的主要是这些问题加起来。

为了避免这种情况,我可以想到几个可能的解决方案,主要回应所说的内容。 Selinap说,第一个解决方案是尽可能地将您的评估保持在NumPy中。大量的损失可能是由于接口造成的。我会研究如何将你的工作分配到numpy或其他一些用C优化的数字库(已经提到过gmpy)。目标应该是尽可能多地将C推入C中,然后将结果返回。你想要从事大工作,而不是做大量的小工作。

当然,第二个解决方案是在python中进行更多的中间和小型操作。显然,使用本机对象会更快。它们将成为所有分支语句的第一个选项,并且始终具有到C代码的最短路径。除非你特别需要固定精度计算或默认运算符的其他问题,否则我不明白为什么人们不会在很多事情上使用直接python函数。

答案 6 :(得分:-1)

真奇怪......我在Ubuntu 11.04 32bit,python 2.7.1,numpy 1.5.1(官方软件包)中确认结果:

import numpy as np
def testfloat():
    s = 0
    for i in range(10000000):  
        s+= float(1)
def testfloat32():
    s = 0
    for i in range(10000000):  
        s+= np.float32(1)
def testfloat64():
    s = 0
    for i in range(10000000):  
        s+= np.float64(1)

%time testfloat()
CPU times: user 4.66 s, sys: 0.06 s, total: 4.73 s
Wall time: 4.74 s

%time testfloat64()
CPU times: user 11.43 s, sys: 0.07 s, total: 11.50 s
Wall time: 11.57 s


%time testfloat32()
CPU times: user 47.99 s, sys: 0.09 s, total: 48.08 s
Wall time: 48.23 s

我不明白为什么float32应该比float64慢5倍。