【问题标题】:numpy float: 10x slower than builtin in arithmetic operations?numpy float:比算术运算中内置的慢 10 倍?
【发布时间】:2011-08-22 20:13:21
【问题描述】:

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

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 秒
  • float32:45.0 秒

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

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

我发现使用numpy.float64 比Python 的float 慢得多,而numpy.float32 甚至更慢(即使我在32 位机器上)。

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

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

编辑#1:

numpy.float64 在算术计算中比 float 慢 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
  • 浮动:3.53s

为了它,我也试过了:

从日期时间导入日期时间 将 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 s;实际上,将float64 转换为float 并返回比按原样使用要快3 倍。尽管如此,转换还是要付出代价,因此总体而言,与纯 python float 相比,它的速度要慢 3 倍以上。

我的机器是:

  • 英特尔酷睿 2 双核 T9300 (2.5GHz)
  • WinXP Professional(32 位)
  • ActiveState Python 3.1.3.5
  • Numpy 1.5.1

编辑 #2:

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

但我仍然想知道为什么下面的代码在float64 下运行速度比float 慢10 倍的确切原因(也许基于源代码)。

编辑#3:

我在 Windows 7 x64 (Intel Core 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
  • 浮动:3.2s

现在np 的两个浮点数(64 或 32)都比内置的 float 慢 5 倍。尽管如此,还是有很大的不同。我想弄清楚它是从哪里来的。

编辑结束

【问题讨论】:

  • 什么版本的 Python?什么版本的numpy?如果是 Python 2.x,请使用 xrange 而不是 range(range 将构建一个巨大的列表)。 float(1) 不是许多人期望经常使用的操作; float(i) 可能更现实一点。为什么要使用 32 位精度?
  • Numpy 说它的浮点数默认为 64 位,这可以解释为什么 32 位浮点数较慢(它必须更改它们)。我不知道为什么指定 float64 会使其速度变慢。请注意,AFAIK,您的架构不会影响浮点数据:32 位或 64 位架构仅与内存地址相关。
  • 试试s=10000000.,应该会更快。更严重的是:您正在分析函数调用速度,而 Numpy 在可以矢量化操作时表现出色。 import语句是否也在使用内置float的版本中?
  • Core 2 Duos 不是 64 位机器吗? ark.intel.com/Product.aspx?id=33917
  • 您可以使用python -mtimeit -s "import numpy; s = numpy.float(1)" "(s + 8) * s % 2399232" 来计时。将 numpy.float 替换为 numpy.float32(1)numpy.float64(1)1.0 以获得其他变体。

标签: python performance numpy floating-point


【解决方案1】:

在这样一个繁重的循环中使用 Python 对象,无论它们是 floatnp.float32,总是很慢。 NumPy 对向量和矩阵的运算速度很快,因为所有的运算都是由用 C 编写的库的一部分而不是由 Python 解释器对大块数据执行的。在解释器中运行和/或使用 Python 对象的代码总是很慢,使用非本机类型会使其更慢。这是意料之中的。

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

【讨论】:

  • 嗯.. 对不起,也许我误解了你的评论。但我的问题不在于float 速度慢;这是关于np.float64float 慢得多。如果您说即使循环中的float 也太慢了,我会很高兴听到您的替代建议(不过我不会从 Python 切换到 C。)
  • Rosh 拥有它的权利。 np.float64 是非本地类型,在 python 解释器中会有额外的(慢)间接层。 numpy 快速的原因在于它避免了集体操作的 python 解释器,并且可以利用顺序内存访问。
  • 啊,谢谢。我想我现在明白了。 numpy 不适用于单数操作,因为使用非内置类型会产生开销(numpy 非常适合数组,因为这种开销分散在许多操作上)。为了提高单数运算的速度,我需要找到一种方法在 numpy 的数组中执行它们,或者使用 CPython 之类的东西。对吗?
  • @Rosh Oxymoron:“使用 non_native 类型会使它变得更慢”......你这么说的依据是什么?
  • @John Machin:我有不同的意思。对于许多对象类型,Python 维护一个“已释放”对象的列表,这些对象在创建对象的新实例时“复活”。这避免了内存分配开销,并且比从头开始创建对象更快。这与创建对小整数的多个引用不同。 (我在 gmpy 中为对象实现了一个空闲列表,它在实际应用中提高了 20% 的性能。)
【解决方案2】:

真的很奇怪...我在 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 倍。

【讨论】:

  • 您似乎得到了与我最初相同的结果。但是使用我更新的代码,float64float32 在性能方面几乎相同。我真的很想关注float64float。毕竟,如果它很慢,谁在乎使用 float32。
【解决方案3】:

也许,这就是为什么你应该直接使用 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

【讨论】:

  • 我同意;在我的机器上,numpy 数组总和比内置列表上的内置总和快 70-140 倍(float 为 70,np.float64 为 140)。但正如我更新的示例所示,并不总是可以使用数组。在这种情况下,使用np.float64 将执行速度提高了一个巨大的常数因子(在简单求和的情况下为 2;在我的代码的情况下为 10),这有点令人不安。
  • 您更新的示例在 numpy 上运行良好,不需要 for 循环。
  • @tillsten 你将如何重写它以使其在没有 for 循环的情况下工作?
  • IINM,在 64 位机器上,np.floatnp.float64。和内置的float不一样。
【解决方案4】:

如果您追求快速标量运算,您应该查看 gmpy 之类的库,而不是 numpy(正如其他人所指出的,后者更适合向量运算而不是标量运算)。

【讨论】:

  • 我不确定gmpy 在这里是否真的有帮助:它主要是关于快速任意精度算术。如果有的话,我预计在使用 gmpy 类型代替 Python 浮点数和小型 Python 整数时会出现小幅减速。
  • 这些天,我同意你的看法,在 2011 年,我想我知道的不是更好:)
  • 是的,很抱歉;那是我的阅读失败。 SO问题与最近的内部讨论相关联,直到发表评论后我才注意到日期。
【解决方案5】:

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 走捷径

float32 走慢路

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

【讨论】:

  • 酷。我了解这如何使 float32 变慢。但是为什么 float64 比内置的 float 慢很多呢? (在我的最新示例中慢了 10 倍!)仅仅是分配内存所需的时间吗?但是在我的循环中,只需要为少数几个对象分配内存,然后可以在后续循环迭代中重复使用,不是吗?
  • @max 我用猜测更新了我的答案。由于您运行的是 32 位操作系统,float64 类型可能会在您的平台上采用慢速路径。如果您可以访问 valgrind+cachegrind,请查看是否可以在您的平台上重现我的调用跟踪。
  • 我尝试了 64 位操作系统(请参阅我对问题的更新)。 np 两种浮点类型都比内置的 float 慢 5 倍。我没有 valgrind,它有助于分析这种特殊的性能损失吗?
  • @max Valgrind 的 cachegrind 工具可以向您显示有关特定函数的调用频率以及从何处调用的大量详细信息。它的主要用途之一是发现应用程序中的瓶颈。
【解决方案6】:

我也可以确认结果。我试图看看使用所有 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 类型都在相互交互,但 10 倍的差异仍然存在(2 秒对 0.2 秒)。

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

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

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

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

基于上述对 python 函数调用的成本和作用域开销的检查,numpy 很容易在进出其 c 数学函数时遭受 9 倍的惩罚。我完全可以想象这个过程比简单的数学运算调用花费的时间要长很多倍。对于每个操作,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,然后返回结果。你想从事大工作,而不是很多小工作。

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

【讨论】:

  • 这很有帮助。我使用 numpy 是因为我想要它的随机函数;它们比 Python 的函数快得多(尤其是当我要求一个包含许多随机数的数组时)。但不幸的是,他们不能被告知返回内置的float。所以我发现在进行算术运算之前将np.float64 转换为内置float 会更便宜...
【解决方案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 相同的空间量,所以这不是原因。对于用户定义的类型,算术运算符的解析可能需要更长的时间。当然不是主要问题。

【讨论】:

  • 我从所有答案中学到了很多,但我接受了这个答案,因为它直接解决了原始问题。如果有人关心使用numpy.float 进行标量运算,他们应该知道这不是问题,只要everythingnumpy.float
【解决方案8】:

答案很简单:内存分配可能是其中的一部分,但最大的问题是 numpy 标量的算术运算是使用“ufuncs”完成的,这意味着对于数百个值来说速度很快,而不仅仅是 1。选择正确的函数来调用和设置循环是一些开销。标量不需要的开销。

将标量转换为 0-d 数组然后传递给相应的 numpy ufunc 然后为 NumPy 支持的许多不同标量类型中的每一种编写单独的计算方法会更容易。

其目的是将标量数学的优化版本添加到 C 中的类型对象中。这仍然可能发生,但从未发生过,因为没有人有足够的动力去做。可能是因为解决方法是将 numpy 标量转换为具有优化算术的 Python 标量。

【讨论】:

  • 我想如果 numpy 的开发者回答了这个问题,那么这最终应该成为公认的答案......
猜你喜欢
  • 2019-04-05
  • 1970-01-01
  • 1970-01-01
  • 2015-10-24
  • 2020-07-13
  • 1970-01-01
  • 2014-01-21
  • 2022-11-20
  • 2019-03-02
相关资源
最近更新 更多