Есть ли причина, по которой Python 3 перечисляет медленнее, чем Python 2?

Python 3, по-видимому, медленнее в перечислениях для минимального цикла, чем Python 2, со значительным отрывом, который, как представляется, ухудшается с более новыми версиями Python 3.

У меня есть Python 2.7.6, Python 3.3.3 и Python 3.4.0, установленные на моем компьютере с 64-разрядной операционной системой Windows (Intel i7-2700K - 3,5 ГГц) с 32-разрядной и 64-разрядной версиями каждого установленного Python. Хотя нет существенной разницы в скорости выполнения между 32-разрядной и 64-разрядной версиями для данной версии в пределах ее ограничений в отношении доступа к памяти, между различными уровнями версий существует очень существенная разница. Я позволю результатам расчета времени говорить за себя следующим образом:

C:\**Python34_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **900 msec** per loop

C:\**Python33_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **820 msec** per loop

C:\**Python27_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: **480 msec** per loop

Поскольку "диапазон" в Python 3 отличается от "диапазона" в Python 2 и функционально совпадает с "xrange" в Python 2, я также рассчитал следующее:

C:\**Python27_64**\python -mtimeit -n 5 -r 2 -s"cnt = 0" "for i in **xrange**(10000000): cnt += 1"
5 loops, best of 2: **320 msec** per loop

Легко увидеть, что версия 3.3 почти в два раза медленнее, чем версия 2.7, а Python 3.4 примерно на 10% медленнее, чем снова.

Мой вопрос: есть ли параметр или параметр среды, которые исправляют это, или это просто неэффективный код или интерпретатор делает больше для версии Python 3?


Кажется, ответ заключается в том, что Python 3 использует целые числа "бесконечной точности", которые раньше назывались "long" в Python 2.x, его тип по умолчанию "int", без какой-либо возможности использовать Python 2 с фиксированной битовой длиной "int" и это обработка этих переменных длины "int" требует дополнительного времени, как описано в ответах и ​​комментариях ниже.

Возможно, что Python 3.4 несколько медленнее, чем Python 3.3, из-за изменений в выделении памяти для поддержки синхронизации, которые немного замедляют выделение / освобождение памяти, что, вероятно, является основной причиной того, что текущая версия "длинной" обработки работает медленнее.

2 ответа

Решение

Разница связана с заменой int введите с long тип. Очевидно, что операции с длинными целыми числами будут медленнее, потому что long операции являются более сложными.

Если вы заставляете python2 использовать long, установив cnt в 0L разница исчезает:

$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in range(10000000): cnt += 1L"
5 loops, best of 2: 1.1 sec per loop
$python3 -mtimeit -n5 -r2 -s"cnt=0" "for i in range(10000000): cnt += 1"
5 loops, best of 2: 686 msec per loop
$python2 -mtimeit -n5 -r2 -s"cnt=0L" "for i in xrange(10000000): cnt += 1L"
5 loops, best of 2: 714 msec per loop

Как вы можете видеть на моей машине python3.4 быстрее, чем оба python2, используя range и используя xrange когда используешь long s. Последний бенчмарк с питоном 2 xrange показывает, что разница в этом случае минимальна.

У меня не установлен python3.3, поэтому я не могу провести сравнение между 3.3 и 3.4, но, насколько я знаю, ничего существенного не изменилось между этими двумя версиями (в отношении range), поэтому сроки должны быть примерно одинаковыми. Если вы видите существенную разницу, попробуйте проверить сгенерированный байт-код, используя dis модуль. Произошло изменение в распределителях памяти ( PEP 445), но я понятия не имею, были ли изменены распределители памяти по умолчанию и какие последствия это повлияло на производительность.

Краткий ответ того, что я узнал из этого вопроса, может быть полезным для тех, кто интересуется тем же, что и я:

  1. Причина замедления заключается в том, что все целочисленные переменные в Python 3.x теперь имеют "бесконечную точность" как тип, который раньше назывался "long" в Python 2.x, но теперь это единственный целочисленный тип, как определено в PEP 237. Согласно этому документу, "короткие" целые числа, имеющие битовую глубину базовой архитектуры, больше не существуют (или только внутри).

  2. Старые "короткие" переменные операции могли выполняться достаточно быстро, потому что они могли напрямую использовать базовые операции машинного кода и оптимизировать распределение новых объектов "int", потому что они всегда имели одинаковый размер.

  3. Тип "long" в настоящее время представлен только объектом класса, выделенным в памяти, так как он может превышать заданную фиксированную длину регистра / глубину памяти в ячейке памяти; поскольку эти представления объектов могут увеличиваться или уменьшаться для различных операций и, следовательно, иметь переменный размер, им нельзя дать фиксированное выделение памяти и оставить там.

  4. Эти "длинные" типы (в настоящее время) не используют полный размер слова машинной архитектуры, но резервируют бит (обычно знаковый бит) для проверки переполнения, поэтому "длинная бесконечная точность" делится (в настоящее время) на 15-бит /30-битные "цифры" для 32-битных /64-битных архитектур соответственно.

  5. Многие из общего использования этих "длинных" целых чисел не требуют более одной (или, может быть, двух) для 32-разрядных архитектур "цифр", поскольку диапазон одной "цифры" составляет около одного миллиарда /32768 для 64-разрядных /32-битные архитектуры соответственно.

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

  7. Наибольшим ударом по производительности, вероятно, являются постоянные выделения / освобождения памяти, по одной паре на каждую целочисленную операцию цикла, что довольно дорого, особенно потому, что Python переходит на поддержку многопоточности с блокировками синхронизации (вероятно, поэтому Python 3.4 хуже, чем 3.3).

  8. В настоящее время реализация всегда обеспечивает достаточные "цифры", выделяя одну дополнительную "цифру" выше фактического размера "цифр", используемых для самого большого операнда, если есть вероятность, что он может "расти", выполняя операцию (которая может или может на самом деле не использовать эту дополнительную "цифру"), а затем нормализует длину результата, чтобы учесть фактическое количество используемых "цифр", которое может фактически остаться прежним (или, возможно, "сжаться" для некоторых операций); это достигается просто уменьшением числа размеров в "длинной" структуре без нового выделения, поэтому можно тратить одну "цифру" пространства памяти, но при этом экономить затраты на производительность еще одного цикла выделения / освобождения.

  9. Есть надежда на улучшение производительности: для многих операций можно предсказать, будет ли операция вызывать "рост" или нет - например, для сложения достаточно взглянуть на наиболее значимые биты (MSB) и операцию не может расти, если оба MSB равны нулю, что будет иметь место для многих операций цикла / счетчика; вычитание не будет "расти" в зависимости от знаков и MSB двух операндов; сдвиг влево будет "расти", только если MSB равен единице; и так далее.

  10. Для тех случаев, когда оператор является чем-то вроде "cnt += 1"/"i += step" и т. Д. (Открывая возможность операций на месте для многих вариантов использования), версия операций "на месте" может быть вызываемый, который будет выполнять соответствующие быстрые проверки и выделять новый объект только в том случае, если необходимо "увеличить", в противном случае выполнять операцию вместо первого операнда. Сложность может заключаться в том, что компилятор должен будет генерировать эти "байтовые" коды "на месте", однако, это уже было сделано с созданием соответствующих специальных байтовых кодов "на месте", только то, что направляет текущий интерпретатор байт-кода их до обычной версии, как описано выше, потому что они еще не были реализованы (нулевые / нулевые значения в таблице).

  11. Вполне может быть, что все, что нужно сделать, это записать версии этих "операций на месте" и заполнить их в таблице "длинных" методов с помощью интерпретатора байт-кода, который уже найдет и запустит их, если они существуют, или внесут незначительные изменения в таблица, чтобы вызывать их, - это все, что требуется.

Обратите внимание, что поплавки всегда имеют одинаковый размер, поэтому могут быть сделаны те же улучшения, хотя поплавки размещаются в блоках запасных мест для повышения эффективности; было бы гораздо труднее сделать это "долго", так как они занимают переменное количество памяти.

Также обратите внимание, что это нарушило бы неизменность "long" (и, возможно, float), поэтому не определены операторы на месте, но тот факт, что они обрабатываются как изменяемые только для этих особых случаев, не влияет на внешние Мир, поскольку он никогда не осознает, что иногда данный объект имеет тот же адрес, что и старое значение (до тех пор, пока сравнения равенства смотрят на содержимое, а не только на адреса объектов).

Я считаю, что, избегая выделения / перемещения памяти для этих распространенных случаев, производительность Python 3.x будет очень близка к Python 2.7.

Многое из того, что я узнал здесь, взято из исходного файла C C для ствола Python для "длинного" объекта


EDIT_ADD: Ой, забыл, что если переменные иногда изменчивы, то замыкания на локальные переменные не работают или не работают без значительных изменений, а это означает, что описанные выше операции на месте "сломают" замыкания. Казалось бы, лучшим решением было бы добиться того, чтобы предварительное распределение резервов работало для "длинных" так же, как раньше для коротких целых и для чисел с плавающей запятой, даже если только для случаев, когда "длинный" размер не работает изменение (которое охватывает большую часть времени, например, для циклов и счетчиков в соответствии с вопросом). Это должно означать, что код не работает намного медленнее, чем Python 2 для обычного использования.

Другие вопросы по тегам