为什么float()比int()更快?

问题描述 投票:32回答:3

尝试一些代码并做一些微基准测试我发现在包含整数的字符串上使用float函数比在同一个字符串上使用int快2倍。

>>> python -m timeit int('1')
1000000 loops, best of 3: 0.548 usec per loop

>>> python -m timeit float('1')
1000000 loops, best of 3: 0.273 usec per loop

在测试int(float('1'))时,它甚至更奇怪,运行时间比裸int('1')短。

>>> python -m timeit int(float('1'))
1000000 loops, best of 3: 0.457 usec per loop

我在运行cPython 2.7.6的Windows 7和使用cPython 2.7.6的Linux Mint 16下测试了代码。

我必须补充一点,只有Python 2受到影响,Python 3显示了运行时之间的差异(不显着)差异。

我知道这些微基准测试得到的信息很容易被滥用,但我很好奇为什么函数的运行时存在这样的差异。

我试图找到intfloat的实现,但我在源代码中找不到它。

python python-2.7 performance python-internals
3个回答
14
投票

int有很多基地。

*,0 *,0x *,0b *,0 * *它可能很长,需要时间来确定基数和其他东西

如果设置了基数,则可以节省大量时间

python -m timeit "int('1',10)"       
1000000 loops, best of 3: 0.252 usec per loop

python -m timeit "int('1')"   
1000000 loops, best of 3: 0.594 usec per loop

正如@Martijn Pieters提到的代码qazxsw poi和qazxsw poi


6
投票

Object/intobject.c(int_new)必须考虑比Object/floatobject.c(float_new)更多可能转换的类型。当您将单个对象传递给int()并且它还不是整数时,则会测试各种事物:

  1. 如果它已经是整数,请直接使用它
  2. 如果对象实现了float(),则调用它并使用结果
  3. 如果对象是int()的C派生子类,则到达并将结构中的C整数值转换为__int__ method对象。
  4. 如果对象实现了int,则调用它并使用结果
  5. 如果对象是字符串,则将其转换为基数设置为10的整数。

传入基本参数时,这些测试都不执行,然后代码直接跳转到使用所选基数将字符串转换为int。那是因为没有其他可接受的类型,而不是在有基数的情况下。

因此,当您传入基础时,突然从字符串创建一个整数要快得多:

int()

当您将字符串传递给__trunc__ method时,第一个测试是查看参数是否是字符串对象(而不是子类),此时它正在被解析。没有必要测试其他类型。

因此,$ bin/python -m timeit "int('1')" 1000000 loops, best of 3: 0.469 usec per loop $ bin/python -m timeit "int('1', 10)" 1000000 loops, best of 3: 0.277 usec per loop $ bin/python -m timeit "float('1')" 1000000 loops, best of 3: 0.206 usec per loop 调用比float()int('1')更多的测试。在那些测试中,测试1,2和3非常快;它们只是指针检查。但第四次测试使用相当于昂贵的int('1', 10)的C当量。这必须测试实例,以及字符串的完整MRO,并且没有缓存,最后它会引发一个float('1'),格式化一个没有人会看到的错误消息。所有在这里都没用的工作。

在Python 3中,getattr(obj, '__trunc__')调用已被替换为速度更快的代码。这是因为在Python 3中,不需要考虑旧式类,因此可以直接在实例的类型(类,AttributeError()的结果)上查找属性,并且缓存MRO中的类属性查找在此刻。不需要创建任何例外。

getattr()对象实现type(instance)方法,这就是为什么float()更快;您从未在步骤4中执行__int__属性测试,因为步骤2产生了结果。

如果你想查看C代码,对于Python 2,请先查看int(float('1'))。解析参数后,代码实际上是这样做的:

__trunc__

无基本情况称为int_new() method,它执行此操作:

if (base == -909)  // no base argument given, the default is -909
    return PyNumber_Int(x);  // parse an integer from x, an arbitrary type. 
if (PyString_Check(x)) {
    // do some error handling; there is a base, so parse the string with the base
    return PyInt_FromString(string, NULL, base);
}

其中PyNumber_Int() function本质上是if (PyInt_CheckExact(o)) { // 1. it's an integer already // ... } m = o->ob_type->tp_as_number; if (m && m->nb_int) { /* This should include subclasses of int */ // 2. it has an __int__ method, return the result // ... } if (PyInt_Check(o)) { /* An int subclass without nb_int */ // 3. it's an int subclass, extract the value // ... } trunc_func = PyObject_GetAttr(o, trunc_name); if (trunc_func) { // 4. it has a __trunc__ method, call it and process the result // ... } if (PyString_Check(o)) // 5. it's a string, lets parse! return int_from_string(PyString_AS_STRING(o), PyString_GET_SIZE(o)); 的包装器,因此使用基数10解析字符串。

在Python 3中,int_from_string()被删除,只留下PyInt_FromString(string, length, 10),在Python端重命名为intobject。同样,longobject取代了int()。所以现在我们看看unicode,测试字符串是用str而不是long_new()完成的:

PyUnicode_Check()

所以当没有设置基数时,我们需要查看PyString_Check(),它执行:

if (obase == NULL)
    return PyNumber_Long(x);

// bounds checks on the obase argument, storing a conversion in base

if (PyUnicode_Check(x))
    return PyLong_FromUnicodeObject(x, (int)base);

注意PyNumber_Long()调用,这是if (PyLong_CheckExact(o)) { // 1. it's an integer already // ... } m = o->ob_type->tp_as_number; if (m && m->nb_int) { /* This should include subclasses of int */ // 2. it has an __int__ method // ... } trunc_func = _PyObject_LookupSpecial(o, &PyId___trunc__); if (trunc_func) { // 3. it has a __trunc__ method // ... } if (PyUnicode_Check(o)) // 5. it's a string return PyLong_FromUnicodeObject(o, 10); 实现;它最终使用_PyObject_LookupSpecial(),它使用缓存;因为没有special method lookup方法,缓存将在第一次MRO扫描后永远返回null。此方法也从不引发异常,它只返回请求的方法或null。

_PyType_Lookup()处理字符串的方式在Python 2和3之间没有变化,所以你只需要查看str.__trunc__,对于字符串来说非常简单:

float()

因此对于字符串对象,我们直接跳转到解析,否则使用Python 2 float_new() function查找实际的// test for subclass and retrieve the single x argument /* If it's a string, but not a string subclass, use PyFloat_FromString. */ if (PyString_CheckExact(x)) return PyFloat_FromString(x, NULL); return PyNumber_Float(x); 对象,或使用PyNumber_Float()方法或字符串子类。

这确实揭示了一种可能的优化:如果float在所有其他类型测试之前首先测试__float__,那么在字符串方面它将与int()一样快。 PyString_CheckExact()排除了一个具有float()PyString_CheckExact()方法的字符串子类,因此这是一个很好的第一次测试。


为了解决其他问题归咎于基础解析(所以寻找__int____trunc__0b0o前缀,不区分大小写),使用单个字符串参数的默认0调用确实寻找基数,基数被硬编码为10。在这种情况下传入带前缀的字符串是错误的:

0x

仅当您将第二个参数显式设置为int()时,才会执行基本前缀解析:

>>> int('0x1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '0x1'

因为没有对0进行测试,所以>>> int('0x1', 0) 1 前缀解析案例与将__trunc__明确设置为任何其他支持的值一样快:

base=0

2
投票

这不是一个完整的答案,只是一些数据和观察。


在运行Linux 4.15.8-1-ARCH的3.9GHz Skylake i7-6700k上运行x86-64 Arch Linux,Python 2.7.14的分析结果。 base:每循环0.0854 usec。 $ python2.7 -m timeit "int('1')" 1000000 loops, best of 3: 0.472 usec per loop $ python2.7 -m timeit "int('1', 10)" 1000000 loops, best of 3: 0.268 usec per loop $ python2.7 bin/python -m timeit "int('1', 0)" 1000000 loops, best of 3: 0.271 usec per loop $ python2.7 bin/python -m timeit "int('0x1', 0)" 1000000 loops, best of 3: 0.261 usec per loop :每循环0.196次使用。 (所以约为2)

浮动

float

IDK为什么狡猾的Python正在搞乱x87控制字,但是,是的,微小的int函数真的运行$ perf record python2.7 -m timeit 'float("1")' 10000000 loops, best of 3: 0.0854 usec per loop Samples: 14K of event 'cycles:uppp', Event count (approx.): 13685905532 Overhead Command Shared Object Symbol 29.73% python2.7 libpython2.7.so.1.0 [.] PyEval_EvalFrameEx 8.54% python2.7 libpython2.7.so.1.0 [.] _Py_dg_strtod 8.30% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords 5.81% python2.7 libpython2.7.so.1.0 [.] lookdict_string.lto_priv.1492 4.79% python2.7 libpython2.7.so.1.0 [.] PyFloat_FromString 4.67% python2.7 libpython2.7.so.1.0 [.] tupledealloc.lto_priv.335 4.16% python2.7 libpython2.7.so.1.0 [.] float_new.lto_priv.219 3.93% python2.7 libpython2.7.so.1.0 [.] _PyOS_ascii_strtod 3.54% python2.7 libc-2.26.so [.] __strchr_avx2 3.34% python2.7 libpython2.7.so.1.0 [.] PyOS_string_to_double 3.21% python2.7 libpython2.7.so.1.0 [.] PyTuple_New 3.05% python2.7 libpython2.7.so.1.0 [.] type_call.lto_priv.51 2.69% python2.7 libpython2.7.so.1.0 [.] PyObject_Call 2.15% python2.7 libpython2.7.so.1.0 [.] PyArg_ParseTupleAndKeywords 1.88% python2.7 itertools.so [.] _init 1.78% python2.7 libpython2.7.so.1.0 [.] _Py_set_387controlword 1.19% python2.7 libpython2.7.so.1.0 [.] _Py_get_387controlword 1.10% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords.cold.59 1.07% python2.7 libpython2.7.so.1.0 [.] PyType_IsSubtype 1.07% python2.7 libc-2.26.so [.] __memset_avx2_unaligned_erms ... 然后用_Py_get_387controlword将其重新加载到fnstcw WORD PTR [rsp+0x6]作为整数返回值,但可能花费更多的时间来编写和检查堆栈来自eax的金丝雀。

这很奇怪,因为movzx使用SSE2(-fstack-protector-strong)进行FP数学运算,而不是x87。 (这个输入的热部分大部分是整数,但那里有_Py_dg_strtodcvtsi2sd xmm1,rsi。)x86-64代码通常只使用x87用于mulsd(80位浮点数)。 divsd代表David Gay的弦乐加倍。 long double

请注意,此功能仅占总运行时间的9%。其余的基本上是解释器开销,与在循环中调用dg_strtod并丢弃结果的C循环相比。

INT

Interesting blog post about how it works under the hood

请注意,strtod$ perf record python2.7 -m timeit 'int("1")' 10000000 loops, best of 3: 0.196 usec per loop $ perf report -Mintel Samples: 32K of event 'cycles:uppp', Event count (approx.): 31257616633 Overhead Command Shared Object Symbol 29.00% python2.7 libpython2.7.so.1.0 [.] PyString_FromFormatV 13.11% python2.7 libpython2.7.so.1.0 [.] PyEval_EvalFrameEx 5.49% python2.7 libc-2.26.so [.] __strlen_avx2 3.87% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords 3.68% python2.7 libpython2.7.so.1.0 [.] PyNumber_Int 3.10% python2.7 libpython2.7.so.1.0 [.] PyInt_FromString 2.75% python2.7 libpython2.7.so.1.0 [.] PyErr_Restore 2.68% python2.7 libc-2.26.so [.] __strchr_avx2 2.41% python2.7 libpython2.7.so.1.0 [.] tupledealloc.lto_priv.335 2.10% python2.7 libpython2.7.so.1.0 [.] PyObject_Call 2.00% python2.7 libpython2.7.so.1.0 [.] PyOS_strtoul 1.93% python2.7 libpython2.7.so.1.0 [.] lookdict_string.lto_priv.1492 1.87% python2.7 libpython2.7.so.1.0 [.] _PyObject_GenericGetAttrWithDict 1.73% python2.7 libpython2.7.so.1.0 [.] PyString_FromStringAndSize 1.71% python2.7 libc-2.26.so [.] __memmove_avx_unaligned_erms 1.67% python2.7 libpython2.7.so.1.0 [.] PyTuple_New 1.63% python2.7 libpython2.7.so.1.0 [.] PyObject_Malloc 1.48% python2.7 libpython2.7.so.1.0 [.] int_new.lto_priv.68 1.45% python2.7 libpython2.7.so.1.0 [.] PyErr_Format 1.45% python2.7 libpython2.7.so.1.0 [.] PyObject_Realloc 1.37% python2.7 libpython2.7.so.1.0 [.] type_call.lto_priv.51 1.30% python2.7 libpython2.7.so.1.0 [.] PyOS_strtol 1.23% python2.7 libpython2.7.so.1.0 [.] _PyString_Resize 1.16% python2.7 libc-2.26.so [.] __ctype_b_loc 1.11% python2.7 libpython2.7.so.1.0 [.] _PyType_Lookup 1.06% python2.7 libpython2.7.so.1.0 [.] PyString_AsString 1.04% python2.7 libpython2.7.so.1.0 [.] PyArg_ParseTupleAndKeywords 1.02% python2.7 libpython2.7.so.1.0 [.] PyObject_Free 0.93% python2.7 libpython2.7.so.1.0 [.] PyInt_FromLong 0.90% python2.7 libpython2.7.so.1.0 [.] PyObject_GetAttr 0.52% python2.7 libc-2.26.so [.] __memset_avx2_unaligned_erms 0.52% python2.7 libpython2.7.so.1.0 [.] vgetargskeywords.cold.59 0.48% python2.7 itertools.so [.] _init ... 总时间的13%,而PyEval_EvalFrameEx占总时间的30%。那是绝对时间,int的时间是两倍。还有更多的功能需要更多的时间。

我还没弄清楚float做了什么,或者它花了多少时间。 7%的周期计数在开始附近收取PyString_FromFormatV指令;即加载由引用传递的16字节arg(作为第二个函数arg)。如果存储的内存很慢,那么这可能会得到比它应得的更多的计数。 (请参阅PyInt_FromString,了解更多关于循环计数如何从无序执行英特尔CPU的指令中获取费用,其中每个周期都有大量不同的工作在运行。)或者如果写入内存,它可能会从存储转发停顿中获取计数最近有独立的狭窄商店。

令人惊讶的是,movdqu xmm0, [rsi]花了这么多时间。从查看其中的指令配置文件,它获得短字符串,但不仅仅是1字节字符串。看起来像this Q&A字节和strlen字节的混合。可能有趣的是在gdb中设置断点并查看常见的args。

浮动版本有一个len < 32(可能正在寻找一个64 < len >= 32小数点?),但没有任何strchr。令人惊讶的是,.版本必须在循环内重做strlen

实际的int函数占总时间的2%,从strlen(占总时间的3%)运行。这些是“自我”时间,不包括他们的孩子,因此分配内存和决定数字基数比解析单个数字花费更多时间。

C中的等效循环运行速度要快50倍(如果我们慷慨的话,可能会运行20倍),在一个常量字符串上调用PyOS_strtoul并丢弃结果。


int with explicit base

出于某种原因,这和PyInt_FromString一样快。

strtoul

按功能分类看起来与float版本非常相似。

© www.soinside.com 2019 - 2024. All rights reserved.