Почему вычитание быстрее, чем добавление в Python?

Я оптимизировал код Python и пробовал следующий эксперимент:

import time start = time.clock() x = 0 for i in range(10000000): x += 1 end = time.clock() print '+=',end-start start = time.clock() x = 0 for i in range(10000000): x -= -1 end = time.clock() print '-=',end-start 

Второй цикл надежно работает быстрее, от усов до 10%, в зависимости от системы, в которой я запускаю ее. Я пробовал различать порядок циклов, количество казней и т. Д., И он все еще работает.

Незнакомец,

 for i in range(10000000, 0, -1): 

(т. е. запуск цикла назад) выполняется быстрее, чем

 for i in range(10000000): 

даже если содержимое цикла идентично.

Что дает, и есть ли здесь более общий урок программирования?

10 Solutions collect form web for “Почему вычитание быстрее, чем добавление в Python?”

Я могу воспроизвести это на моем Q6600 (Python 2.6.2); увеличивая диапазон до 100000000:

 ('+=', 11.370000000000001) ('-=', 10.769999999999998) 

Во-первых, некоторые наблюдения:

  • Это 5% для тривиальной операции. Это важно.
  • Скорость нативного кода сложения и вычитания не имеет значения. Он находится на уровне шума, полностью затмевается оценкой байт-кода. Это говорит о одной или двух собственных инструкциях вокруг тысяч.
  • Байт-код генерирует точно такое же количество инструкций; единственная разница – INPLACE_ADD против INPLACE_SUBTRACT и +1 против -1.

Глядя на источник Python, я могу сделать предположение. Это обрабатывается ceval.c, в PyEval_EvalFrameEx . INPLACE_ADD имеет значительный дополнительный блок кода для обработки конкатенации строк. Этот блок не существует в INPLACE_SUBTRACT , так как вы не можете вычитать строки. Это означает, что INPLACE_ADD содержит более собственный код. В зависимости (в значительной степени!) От того, как код генерируется компилятором, этот дополнительный код может быть встроен вместе с остальным кодом INPLACE_ADD, что означает, что дополнения могут поразить кеш команд сложнее, чем вычитание. Это может привести к дополнительным перехватам кеша L2, что может привести к значительной разнице в производительности.

Это сильно зависит от системы, в которой вы находитесь (разные процессоры имеют разные объемы кеш-памяти и кеш-архитектуры), используемый компилятор, включая конкретные версии и параметры компиляции (разные компиляторы будут решать по-разному, какие биты кода находятся на критическом путь, который определяет, как скомпонован код сборки) и т. д.

Кроме того, разница отменяется в Python 3.0.1 (+: 15.66, -: 16.71); без сомнения, эта критическая функция сильно изменилась.

 $ python -m timeit -s "x=0" "x+=1" 10000000 loops, best of 3: 0.151 usec per loop $ python -m timeit -s "x=0" "x-=-1" 10000000 loops, best of 3: 0.154 usec per loop 

Похоже, у вас есть предвзятое измерение

Я думаю, что «урок общего программирования» состоит в том, что его действительно трудно предсказать, только взглянув на исходный код, какая последовательность операторов будет самой быстрой. Программисты на всех уровнях часто сталкиваются с такой «интуитивной» оптимизацией. То, что, по вашему мнению, вы знаете, может быть не обязательно истинным.

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

С байт-скомпилированными языками, такими как Java, Python и .NET, недостаточно даже измерить производительность только на одной машине. Различия между версиями VM, реализациями реализации собственного кода, оптимизациями для ЦП и т. Д. Сделают этот вопрос еще более сложным для ответа.

«Второй цикл надежно быстрее …»

Это ваше объяснение прямо здесь. Повторно закажите свой скрипт, чтобы сначала выполнялся тест вычитания, затем добавление, и внезапное добавление снова становится более быстрой:

 -= 3.05 += 2.84 

Очевидно, что со второй половиной скрипта происходит что-то быстрее. Я предполагаю , что первый вызов range() медленнее, потому что python должен выделять достаточно памяти для такого длинного списка, но он может повторно использовать эту память для второго вызова range() :

 import time start = time.clock() x = range(10000000) end = time.clock() del x print 'first range()',end-start start = time.clock() x = range(10000000) end = time.clock() print 'second range()',end-start 

Несколько прогонов этого скрипта показывают, что дополнительное время, необходимое для первого range() учитывает почти всю разницу во времени между «+ =» и «- =», описанными выше:

 first range() 0.4 second range() 0.23 

Это всегда хорошая идея, когда вы задаете вопрос, какую платформу и какую версию Python вы используете. Иногда это не имеет значения. Это не одно из тех случаев:

  1. time.clock() подходит только для Windows. Отбросьте свой собственный измерительный код и используйте -m timeit как показано в ответе pixelbeat.

  2. range() Python 2.X range() создает список. Если вы используете Python 2.x, замените range на xrange и посмотрите, что произойдет.

  3. Python 3.X int – это Python2.X.

Есть ли более общий урок программирования здесь?

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

Можно рассуждать об алгоритмической сложности, гипотезе об оптимизации компилятора, оценке производительности кэша и т. Д. Однако, поскольку эти вещи могут взаимодействовать нетривиальными способами, единственный способ убедиться в том, насколько быстро будет выполняться конкретный фрагмент кода, – это сравнить его в целевой среде (как вы по праву сделали).

С Python 2.5 самая большая проблема здесь заключается в использовании диапазона, который будет выделять список, который большой для итерации по нему. При использовании xrange, в зависимости от того, что сделано, второе для меня немного быстрее. (Не уверен, что диапазон стал генератором в Python 3.)

Ваш эксперимент неисправен. Способ, которым должен быть разработан этот эксперимент, состоит в том, чтобы написать две разные программы – 1 для добавления, 1 для вычитания. Они должны быть точно такими же и работать в тех же условиях, когда данные будут помещены в файл. Затем вам нужно усреднить пробеги (не менее нескольких тысяч), но вам понадобится статистик, чтобы рассказать вам о соответствующем номере.

Если вы хотите проанализировать различные методы сложения, вычитания и циклирования, снова каждая из них должна быть отдельной программой.

Экспериментальная ошибка может возникнуть из-за высокой температуры процессора и другой активности, происходящей на процессоре, поэтому я бы выполнил прогоны в различных шаблонах …

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

  • Выполнение кода подтвердило ваши претензии: – = занимает постоянно меньше времени; 3,6% в среднем
  • Однако запуск моего кода противоречит исходу вашего эксперимента: + = занимает среднее (не всегда) на 0,5% меньше времени.

Чтобы показать все результаты, я разместил сюжеты онлайн:

Итак, я пришел к выводу, что ваш эксперимент имеет предвзятость, и это важно.

Наконец, вот мой код:

 import time addtimes = [0.] * 100 subtracttimes = [0.] * 100 range100 = range(100) range10000000 = range(10000000) j = 0 i = 0 x = 0 start = 0. for j in range100: start = time.clock() x = 0 for i in range10000000: x += 1 addtimes[j] = time.clock() - start for j in range100: start = time.clock() x = 0 for i in range10000000: x -= -1 subtracttimes[j] = time.clock() - start print '+=', sum(addtimes) print '-=', sum(subtracttimes) 

Ход цикла назад быстрее, потому что компьютер имеет более легкое сравнение времени, если число равно 0.

Python - лучший язык программирования в мире.