Подтвердить что ты не робот

Почему float() быстрее, чем int()?

Экспериментируя с некоторым кодом и выполняя некоторые микрообъекты, я узнал, что использование функции float в строке, содержащей целое число, является фактором 2 быстрее, чем использование int в той же строке.

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

Я протестировал код под Windows 7 с помощью cPython 2.7.6 и Linux Mint 16 с помощью cPython 2.7.6.

Мне нужно добавить, что затронуто только Python 2, Python 3 показывает путь меньшей (не замечательной) разницы между временем выполнения.

Я знаю, что информация, которую я получаю с помощью таких микрообъектов, легко использовать, но мне любопытно, почему существует такая разница во времени выполнения функций.

Я попытался найти реализации int и float, но я не могу найти его в источниках.

4b9b3361

Ответ 1

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)

Ответ 2

int() должен учитывать более возможные типы для преобразования из float(). Когда вы передаете один объект в int() и он уже не является целым числом, тогда различные вещи проверяются на:

  • Если это целое число, используйте его непосредственно
  • Если объект реализует метод __int__, вызовите его и используйте результат
  • если объект является подклассом C-типа int, достигните и преобразуйте целочисленное значение C в структуру в объект int().
  • Если объект реализует метод __trunc__, вызовите его и используйте результат
  • если объект является строкой, преобразуйте его в целое число с базовым значением в 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(), первый выполненный тест - это увидеть, является ли аргумент строковым объектом (а не подклассом), после чего он разбирается. Нет необходимости тестировать другие типы.

Таким образом, вызов int('1') выполняет несколько тестов, чем int('1', 10) или float('1'). Из этих тестов тесты 1, 2 и 3 довольно быстрые; это просто проверки указателей. Но четвертый тест использует эквивалент C getattr(obj, '__trunc__'), что относительно дорого. Это должно проверить экземпляр и полный MRO строки, и нет кеша, и в итоге он вызывает AttributeError(), форматируя сообщение об ошибке, которое никто никогда не увидит. Все работы здесь бесполезны.

В Python 3 этот вызов getattr() был заменен кодом, который выполняется намного быстрее. Это связано с тем, что в Python 3 нет необходимости учитывать классы старого стиля, поэтому атрибут можно искать непосредственно по типу экземпляра (класс, результат type(instance)) и поиск атрибутов класса через MRO кешируются в этот момент. Никаких исключений не требуется.

float() объекты реализуют метод __int__, поэтому int(float('1')) выполняется быстрее; вы никогда не попадали в тест атрибута __trunc__ на шаге 4, так как шаг 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 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 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 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, переименованный в int() со стороны Python. В том же духе 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 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 a string
    return PyLong_FromUnicodeObject(o, 10);

Обратите внимание на вызов _PyObject_LookupSpecial(), это специальный поиск метода; он в конечном итоге использует _PyType_Lookup(), в котором используется кеш; поскольку не существует метода str.__trunc__, кэш которого навсегда вернет null после первого сканирования MRO. Этот метод также никогда не вызывает исключение, он просто возвращает либо запрошенный метод, либо нуль.

Способ float() обрабатывает строки без изменений между Python 2 и 3, поэтому вам нужно только посмотреть Python 2 float_new() function, который для строк довольно прост:

// test for subclass and retrieve the single x argument
/* If it 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__, поэтому это хороший первый тест.


Чтобы обратиться к другим ответам, обвиняя это в анализе базы (так что поиск префикса 0b, 0o, 0 или 0x, регистр без учета регистра), вызов по умолчанию int() с единственным аргументом строки ищет базу, база жестко закодирована до 10. Это ошибка передачи в строке с префиксом в этом случае:

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

Ответ 3

Это не полный ответ, просто некоторые данные и наблюдения.


Результаты профилирования от x86-64 Arch Linux, Python 2.7.14, на 3,9 ГГц Skylake i7-6700k под управлением Linux 4.15.8-1-ARCH. float: 0,0854 мксек за цикл. 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, почему heck Python возится с управляющим словом x87, но да, крошечная функция _Py_get_387controlword действительно запускает fnstcw WORD PTR [rsp+0x6], а затем перезагружает ее в eax как целое возвращаемое значение с movzx, но вероятно, тратит больше времени на запись и проверку стека канарейки от -fstack-protector-strong.

Это странно, потому что _Py_dg_strtod использует SSE2 (cvtsi2sd xmm1,rsi) для математики FP, а не x87. (Горячая часть с этим входом в основном целая, но там есть mulsd и divsd.) В коде x86-64 обычно используется x87 для long double (80-битный float). dg_strtod означает, что цепочка Дэвида Гей удвоится. Интересное сообщение в блоге о том, как он работает под капотом.

Обратите внимание, что эта функция занимает всего 9% от общего времени выполнения. Остальное - в основном служебные данные интерпретатора, по сравнению с циклом C, который вызвал strtod в цикле и отбросил результат.

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_EvalFrameEx занимает 13% от общего времени для int, против 30% от общего количества для float. Это примерно такое же абсолютное время, и PyString_FromFormatV занимает в два раза больше времени. Плюс больше функций, занимающих более мелкие куски времени.

Я не понял, что делает PyInt_FromString, или то, на что он тратит свое время. 7% от его количества циклов заряжаются до команды movdqu xmm0, [rsi] рядом с началом; то есть загрузка 16-байтового аргумента, который был передан по ссылке (в качестве второй функции arg). Это может привести к большему количеству счетчиков, чем того заслуживает, если бы память, хранящаяся в памяти, была медленной для ее создания. (См. этот Q & A, чтобы узнать больше о том, как подсчет циклов получает инструкции по исполнению вне очереди. Процессоры Intel, где в каждом цикле выполняется много разных работ. ) Или, может быть, он получает счет от магазина-пересылки, если эта память была написана недавно с отдельными более узкими магазинами.

Удивительно, что strlen занимает так много времени. От взгляда на профиль инструкций внутри него он получает короткие строки, но не только 1-байтовые строки. Похож на сочетание байтов len < 32 и 64 < len >= 32. Может быть интересно установить точку останова в gdb и посмотреть, какие аргументы являются общими.

Версия с плавающей запятой имеет 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.