33

尝试一些代码并做一些微基准测试,我发现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我在源代码中找不到它。

4

3 回答 3

16

int有很多基地。

*, 0*, 0x*, 0b*, 0o* 并且可以很长,确定基数和其他东西需要时间

如果设置了底座,可以节省很多时间

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 提到的代码Object/intobject.c(int_new)Object/floatobject.c(float_new)

于 2014-01-20T12:47:53.050 回答
8

int()必须考虑比必须转换更多的可能类型float()。当您将单个对象传递给int()并且它还不是整数时,会测试各种内容:

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

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

结果,当你传入一个基数时,突然从一个字符串创建一个整数要快得多:

$ 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(),进行的第一个测试是查看参数是否是字符串对象(而不是子类),此时它正在被解析。无需测试其他类型。

所以调用比orint('1')进行了更多的测试。在这些测试中,测试 1、2 和 3 非常快。它们只是指针检查。但是第四个测试使用的是 C 等价的,相对昂贵。这必须测试实例和字符串的完整 MRO,并且没有缓存,最后它会引发一个,格式化一个没有人会看到的错误消息。所有在这里都毫无用处的工作。int('1', 10)float('1')getattr(obj, '__trunc__')AttributeError()

在 Python 3 中,该getattr()调用已被替换为更快的代码。这是因为在 Python 3 中,不需要考虑旧式类,因此可以直接在实例的类型(类, 的结果type(instance))上查找属性,并且跨 MRO 的类属性查找缓存在这点。不需要创建任何异常。

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

如果您想查看 C 代码,对于 Python 2,请先查看int_new()方法。解析参数后,代码本质上是这样做的:

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()函数,它执行以下操作:

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));

whereint_from_string()本质上是 的包装器PyInt_FromString(string, length, 10),因此以 10 为基数解析字符串。

在 Python 3 中,intobject被删除,只留下longobject,在 Python 端重命名为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()调用,这是特殊的方法查找实现;它最终使用_PyType_Lookup(),它使用缓存;因为没有任何str.__trunc__方法可以让缓存在第一次 MRO 扫描后永远返回 null。此方法也从不引发异常,它只返回请求的方法或 null。

处理字符串的方式float()在 Python 2 和 3 之间没有变化,因此您只需要查看Python 2float_new()函数,它对于字符串来说非常简单:

// 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()一样快。排除具有or方法的字符串子类,因此是一个很好的第一个测试。float()PyString_CheckExact()__int____trunc__


为了解决其他将其归咎于基本解析的答案(因此查找a 0b、或前缀0o,不区分大小写),带有单个字符串参数的默认调用确实会查找 base,base 被硬编码为 10。传递是错误的在这种情况下,在带有前缀的字符串中:00xint()

>>> 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一样快:base

$ 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
于 2018-03-21T18:46:14.400 回答
2

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


x86-64 Arch Linux、Python 2.7.14 在运行 Linux 4.15.8-1-ARCH 的 3.9GHz Skylake i7-6700k 上的分析结果。 float:每个循环 0.0854 微秒。 int:每个循环 0.196 微秒。(所以大约是 2 倍)

漂浮

$ 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
   ...

IDK 为什么 Python 搞乱了 x87 控制字,但是,是的,这个小_Py_get_387controlword函数确实运行fnstcw WORD PTR [rsp+0x6],然后将其重新加载eax为整数返回值movzx,但可能会花费更多时间编写和检查堆栈金丝雀-fstack-protector-strong

这很奇怪,因为_Py_dg_strtod将 SSE2 ( cvtsi2sd xmm1,rsi) 用于 FP 数学,而不是 x87。(这个输入的热门部分主要是整数,但里面有mulsddivsd。)x86-64 代码通常只使用 x87 来表示long double(80 位浮点数)。 dg_strtod代表大卫盖的弦加倍。 有趣的博客文章,介绍它如何在幕后工作

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

整数

$ 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
   ...

请注意,PyEval_EvalFrameEx占总时间的 13%,而 占总时间的int30% float。这大约是相同的绝对时间,并且PyString_FromFormatV花费了两倍的时间。加上更多的功能需要更多的小块时间。

我还没有弄清楚它做了什么PyInt_FromString,或者它把时间花在了什么上面。其循环计数的 7% 计入movdqu xmm0, [rsi]开始附近的一条指令;即加载一个通过引用传递的 16 字节 arg(作为第二个函数 arg)。如果存储该内存的任何东西产生它的速度很慢,那么这可能会得到更多的计数。(有关循环计数如何计入乱序执行 Intel CPU 指令的更多信息,请参阅此问答,其中每个循环都有许多不同的工作在运行。)或者,如果该内存是,它可能是从存储转发停顿中获取计数的最近用单独的狭窄商店写的。

strlen花费这么多时间真是令人惊讶。通过查看其中的指令配置文件,它得到了短字符串,但不仅仅是 1 字节字符串。看起来像len < 32字节和64 < len >= 32字节的混合。在 gdb 中设置断点并查看常见的 args 可能会很有趣。

浮动版本有一个strchr(也许正在寻找.小数点?),但什么strlen都没有。令人惊讶的是,该int版本必须在循环内部重做一个strlen

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

C 中的等效循环将运行大约 50 倍(如果我们慷慨的话,可能会运行 20 倍),调用strtoul一个常量字符串并丢弃结果。


具有显式基数的 int

出于某种原因,这与float.

$ perf record python2.7 -m timeit 'int("1",10)'
10000000 loops, best of 3: 0.0894 usec per loop

$ perf report -Mintel
Samples: 14K of event 'cycles:uppp', Event count (approx.): 14289699408
Overhead  Command    Shared Object        Symbol
  30.84%  python2.7  libpython2.7.so.1.0  [.] PyEval_EvalFrameEx
  12.56%  python2.7  libpython2.7.so.1.0  [.] vgetargskeywords
   6.70%  python2.7  libpython2.7.so.1.0  [.] PyInt_FromString
   5.19%  python2.7  libpython2.7.so.1.0  [.] tupledealloc.lto_priv.335
   5.17%  python2.7  libpython2.7.so.1.0  [.] int_new.lto_priv.68
   4.12%  python2.7  libpython2.7.so.1.0  [.] lookdict_string.lto_priv.1492
   4.08%  python2.7  libpython2.7.so.1.0  [.] PyOS_strtoul
   3.78%  python2.7  libc-2.26.so         [.] __strchr_avx2
   3.29%  python2.7  libpython2.7.so.1.0  [.] type_call.lto_priv.51
   3.26%  python2.7  libpython2.7.so.1.0  [.] PyTuple_New
   3.09%  python2.7  libpython2.7.so.1.0  [.] PyOS_strtol
   3.06%  python2.7  libpython2.7.so.1.0  [.] PyObject_Call
   2.49%  python2.7  libpython2.7.so.1.0  [.] PyArg_ParseTupleAndKeywords
   2.01%  python2.7  libpython2.7.so.1.0  [.] PyType_IsSubtype
   1.65%  python2.7  libc-2.26.so         [.] __strlen_avx2
   1.52%  python2.7  libpython2.7.so.1.0  [.] object_init.lto_priv.86
   1.19%  python2.7  libpython2.7.so.1.0  [.] vgetargskeywords.cold.59
   1.03%  python2.7  libpython2.7.so.1.0  [.] PyInt_AsLong
   1.00%  python2.7  libpython2.7.so.1.0  [.] PyString_Size
   0.99%  python2.7  libpython2.7.so.1.0  [.] PyObject_GC_UnTrack
   0.87%  python2.7  libc-2.26.so         [.] __ctype_b_loc
   0.85%  python2.7  libc-2.26.so         [.] __memset_avx2_unaligned_erms
   0.47%  python2.7  itertools.so         [.] _init

float按功能划分的配置文件看起来也与版本非常相似。

于 2018-03-21T15:50:32.190 回答