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

时间:2014-01-20 11:24:00

标签: python python-2.7 performance python-internals

尝试一些代码并做一些微基准测试我发现在包含整数的字符串上使用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的实现,但我在源代码中找不到它。

3 个答案:

答案 0 :(得分: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

as @Martijn Pieters提到代码Object/intobject.c(int_new)Object/floatobject.c(float_new)

答案 1 :(得分:6)

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

  1. 如果已经是整数,请直接使用
  2. 如果对象实现__int__ method,则调用它并使用结果
  3. 如果对象是int的C派生子类,则进入并将结构中的C整数值转换为int()对象。
  4. 如果对象实现__trunc__ method,则调用它并使用结果
  5. 如果对象是字符串,则将其转换为基数设置为10的整数。
  6. 当您传入基本参数时,这些测试都不执行,然后代码直接跳转到使用所选基础将字符串转换为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()时,第一个测试是查看参数是否是字符串对象(而不是子类),此时它正在被解析。没有必要测试其他类型。

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

    在Python 3中,getattr()调用已被替换为快得多的代码。这是因为在Python 3中,不需要考虑旧式类,因此可以直接在实例的类型(类,type(instance)的结果)和类属性查找中查找属性。此时缓存MRO。不需要创建任何例外。

    float()个对象实现__int__方法,这就是int(float('1'))更快的原因;你从未在步骤4点击__trunc__属性测试,因为步骤2产生了结果。

    如果您想查看C代码,对于Python 2,请先查看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));
    

    其中int_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()来电,这是special method lookup实施;它最终使用_PyType_Lookup(),它使用缓存;因为没有str.__trunc__方法,缓存将在第一次MRO扫描后永久返回null。此方法也从不引发异常,它只返回请求的方法或null。

    {2}处理字符串的方式在Python 2和3之间保持不变,因此您只需要查看Python 2 float_new() function,对于字符串来说非常简单:

    float()

    因此,对于字符串对象,我们直接跳转到解析,否则使用// 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__0b0o0前缀(不区分大小写),默认为{{ 1}}使用单个字符串参数调用确实查找基础,基数被硬编码为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 :(得分:2)

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

在运行Linux 4.15.8-1-ARCH的3.9GHz Skylake i7-6700k上,x86-64 Arch Linux,Python 2.7.14的分析结果。 float:每个循环0.0854 usec。 int:每个循环使用0.196个usec。 (所以约为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代表David Gay的字符串加倍。 Interesting blog post about how it works under the hood

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

INT

$ 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_EvalFrameExint总时间的13%,而float占总时间的30%。那个绝对时间大致相同,PyString_FromFormatV时间是两倍。还有更多的功能需要更多的时间。

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

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

float版本有一个strchr(可能正在查找.小数点?),但没有任何strlen。令人惊讶的是,int版本必须在循环内重做strlen

实际PyOS_strtoul函数占总时间的2%,从PyInt_FromString开始(占总时间的3%)。这些是&#34; self&#34;时间,不包括他们的孩子,所以分配内存和决定数字基数比解析单个数字花费更多的时间。

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

int with explicit base

出于某种原因,这与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版本非常相似。