Почему вычитание быстрее, чем добавление в 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):

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

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

Ответ 1

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

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

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

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

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

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

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

Ответ 2

$ 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

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

Ответ 3

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

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

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

Ответ 4

"Второй цикл надежно быстрее..."

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

-= 3.05
+= 2.84

Очевидно, что во второй половине script происходит что-то, что делает его быстрее. Я предполагаю, что первый вызов 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

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

first range() 0.4
second range() 0.23

Ответ 5

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

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

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

  • Python 3.X int - это Python2.X long.

Ответ 6

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

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

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

Ответ 7

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

Ответ 8

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

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

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

Ответ 9

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

  • Выполнение кода подтвердило ваши претензии: - = занимает постоянно меньше времени; 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)

Ответ 10

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