Почему сложение и умножение быстрее, чем сравнение?
Я всегда думал, что сравнения были самой быстрой операцией, которую мог выполнить компьютер. Я помню, как слышал это на презентации от д. кнута, где он писал циклы в порядке убывания "потому что сравнение с 0 быстро". Я также читал, что умножение должно быть медленнее, чем сложение здесь.
Я удивлен, увидев, что в Python 2 и 3, тестируя под Linux и Mac, сравнения кажутся намного медленнее, чем арифметика оперативный.
может кто-нибудь объяснить, почему?
%timeit 2 > 0
10000000 loops, best of 3: 41.5 ns per loop
%timeit 2 * 2
10000000 loops, best of 3: 27 ns per loop
%timeit 2 * 0
10000000 loops, best of 3: 27.7 ns per loop
%timeit True != False
10000000 loops, best of 3: 75 ns per loop
%timeit True and False
10000000 loops, best of 3: 58.8 ns per loop
и под python 3:
$ ipython3
Python 3.5.2 | packaged by conda-forge | (default, Sep 8 2016, 14:36:38)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra details.
In [1]: %timeit 2 + 2
10000000 loops, best of 3: 22.9 ns per loop
In [2]: %timeit 2 * 2
10000000 loops, best of 3: 23.7 ns per loop
In [3]: %timeit 2 > 2
10000000 loops, best of 3: 45.5 ns per loop
In [4]: %timeit True and False
10000000 loops, best of 3: 62.8 ns per loop
In [5]: %timeit True != False
10000000 loops, best of 3: 92.9 ns per loop
6 ответов
это происходит из-за Постоянный Складной на Глазок оптимизатор в компиляторе Python.
используя модуль dis, если мы сломаем каждый из операторов, чтобы посмотреть, как они переводятся на машинном уровне, вы заметите, что все операторы, такие как неравенство, равенство и т. д., сначала загружаются в память, а затем оцениваются. Однако все выражения, такие как умножение, сложение и т. д., вычисляются и загружаются как постоянно в памяти.
в целом, это приводит к меньшему количеству шагов выполнения, делая шаги быстрее:
>>> import dis
>>> def m1(): True != False
>>> dis.dis(m1)
1 0 LOAD_GLOBAL 0 (True)
3 LOAD_GLOBAL 1 (False)
6 COMPARE_OP 3 (!=)
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
>>> def m2(): 2 *2
>>> dis.dis(m2)
1 0 LOAD_CONST 2 (4)
3 POP_TOP
4 LOAD_CONST 0 (None)
7 RETURN_VALUE
>>> def m3(): 2*5
>>> dis.dis(m3)
1 0 LOAD_CONST 3 (10)
3 POP_TOP
4 LOAD_CONST 0 (None)
7 RETURN_VALUE
>>> def m4(): 2 > 0
>>> dis.dis(m4)
1 0 LOAD_CONST 1 (2)
3 LOAD_CONST 2 (0)
6 COMPARE_OP 4 (>)
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
>>> def m5(): True and False
>>> dis.dis(m5)
1 0 LOAD_GLOBAL 0 (True)
3 JUMP_IF_FALSE_OR_POP 9
6 LOAD_GLOBAL 1 (False)
>> 9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
как объясняли другие, это потому, что оптимизатор глазка Python оптимизирует арифметические операции, но не сравнения.
написав свой собственный оптимизатор глазка для базового компилятора, я могу заверить вас, что оптимизация постоянных сравнений так же проста, как оптимизация постоянных арифметических операций. Поэтому нет технической причины, по которой Python должен делать последнее, но не первое.
однако, каждая такая оптимизация должна быть отдельно запрограммирован, и поставляется с двумя затратами: время для его программирования и дополнительный оптимизирующий код, занимающий место в исполняемом файле Python. Таким образом, вам нужно сделать сортировку: какая из этих оптимизаций достаточно распространена, чтобы оправдать затраты?
кажется, что разработчики Python, достаточно разумно, решили сначала оптимизировать арифметические операции. Возможно, они перейдут к сравнениям в будущем выпуске.
быстрый disassambling показывает, что сравнение включает в себя несколько операций. Согласно ответ, есть некоторые предварительные расчеты, сделанные "глазок оптимизатора" (wiki) для умножения, сложения и др. но не для операторов сравнения:
>>> import dis
>>> def a():
... return 2*3
...
>>> dis.dis(a)
2 0 LOAD_CONST 3 (6)
3 RETURN_VALUE
>>> def b():
... return 2 < 3
...
>>> dis.dis(b)
2 0 LOAD_CONST 1 (2)
3 LOAD_CONST 2 (3)
6 COMPARE_OP 0 (<)
9 RETURN_VALUE
Как и другие прокомментировали-это связано с оптимизатором peep hole, который предварительно вычисляет результаты 2 * 3 (6). Как показывает dis
0 LOAD_CONST 3 (6)
но попробуйте это-он отключит оптимизатор от предварительного вычисления результатов
>>> def a(a, b):
... return a*b
...
>>> dis.dis(a)
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_MULTIPLY
7 RETURN_VALUE
>>> def c(a,b):
... return a<b
...
>>> dis.dis(c)
2 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 COMPARE_OP 0 (<)
9 RETURN_VALUE
>>>
Если вы время эти функции сравнение будет быстрее.
для случая python приведенные выше ответы верны. Для машинного кода все немного сложнее. Я предполагаю, что мы говорим о целочисленных операциях здесь, с поплавками и сложными объектами, ни один из приведенных ниже не будет применяться. Кроме того, мы предполагаем, что сравниваемые значения уже загружены в регистры. Если они не извлекают их оттуда, где они находятся, это может занять в 100 раз больше времени, чем фактические операции
современные процессоры имеют несколько способов сравнения двух чисел. Очень популярные из них делают XOR a,b, Если вы просто хотите увидеть, равны ли два значения или CMP a, b, если вы хотите знать связь между значениями ( меньше, больше, равны и т. д. ). Операция CMP - это просто вычитание с отброшенным результатом, потому что нас интересуют только флаги после операции.
обе эти операции имеют глубину 1, поэтому они могут быть выполнены в одном цикле процессора. Это так быстро, как вы можете идти. Умножение-это форма повторных дополнений, поэтому глубина операция обычно равна размеру вашего регистра. Есть некоторые оптимизации, которые могут быть сделаны для уменьшения глубины, но обычно умножение является одной из более медленных операций, которые может выполнять CPU.
однако умножение на 0,1 или любую мощность 2 может быть уменьшено до операции сдвига. Это также операция глубины один. Это занимает столько же времени, сколько сравнение двух чисел. Подумайте о десятичной системе, вы можете умножить любое число на 10, 100, 1000, добавив нули на конец номера. Любой оптимизирующий компилятор распознает этот тип умножения и использует для него наиболее эффективную операцию. Современные процессоры также довольно продвинутые, поэтому они могут выполнять такую же оптимизацию в оборудовании, подсчитывая, сколько бит установлено в любом из операндов. И если это всего лишь один бит, операция будет сведена к сдвигу.
таким образом, в вашем случае умножение на 2 так же быстро, как сравнение двух чисел. Как указывалось выше, любой оптимизирующий компилятор будет смотрите, что вы умножаете две константы, поэтому он заменит просто заменить функцию с возвращением константы.
Вау, ответ @mu 無 взорвал мой разум! Однако важно не делать обобщений при выводе своих выводов... Вы проверяете время для константы не переменные. Для переменных умножение происходит медленнее, чем сравнение.
вот более интересный случай, в котором сравниваемые числа хранятся в фактических переменных...
import timeit
def go():
number=1000000000
print
print 'a>b, internal:',timeit.timeit(setup="a=1;b=1", stmt="a>b", number=number)
print 'a*b, internal:',timeit.timeit(setup="a=1;b=1", stmt="a*b", number=number)
print 'a>b, shell :',
%%timeit -n 1000000000 "a=1;b=1" "a>b"
print 'a*b, shell :',
%%timeit -n 1000000000 "a=1;b=1" "a*b"
go()
результат дает:
a>b, internal: 51.9467676445
a*b, internal: 63.870462403
a>b, shell :1000000000 loops, best of 3: 19.8 ns per loop
a>b, shell :1000000000 loops, best of 3: 19.9 ns per loop
и порядок восстановлен во Вселенной ;)
для полноты картины, рассмотрим еще несколько случаев... А если у нас есть одна переменная и одна константа?
import timeit
def go():
print 'a>2, shell :',
%%timeit -n 10000000 "a=42" "a>2"
print 'a*2, shell :',
%%timeit -n 10000000 "a=42" "a*2"
go()
a>2, shell :10000000 loops, best of 3: 18.3 ns per loop
a*2, shell :10000000 loops, best of 3: 19.3 ns per loop
что происходит с рисовые шарики?
import timeit
def go():
print
number=1000000000
print 'a==b : ', timeit.timeit(setup="a=True;b=False",stmt="a==b",number=number)
print 'a and b : ', timeit.timeit(setup="a=True;b=False",stmt="a and b",number=number)
print 'boolean ==, shell :',
%%timeit -n 1000000000 "a=True;b=False" "a == b"
print 'boolean and, shell :',
%%timeit -n 1000000000 "a=False;b=False" "a and b"
go()
a==b : 70.8013108982
a and b : 38.0614485665
boolean ==, shell :1000000000 loops, best of 3: 17.7 ns per loop
boolean and, shell :1000000000 loops, best of 3: 16.4 ns per loop
: D это интересно, вроде boolean and быстрее, чем ==. Однако все это было бы хорошо, так как Дональд Кнут не потерял бы сон, лучший способ сравнить было бы использовать И...
на практике, мы должны проверить numpy, что может быть еще более значительным...
import timeit
def go():
number=1000000 # change if you are in a hurry/ want to be more certain....
print '==== int ===='
print 'a>b : ', timeit.timeit(setup="a=1;b=2",stmt="a>b",number=number*100)
print 'a*b : ', timeit.timeit(setup="a=1;b=2",stmt="a*b",number=number*100)
setup = "import numpy as np;a=np.arange(0,100);b=np.arange(100,0,-1);"
print 'np: a>b : ', timeit.timeit(setup=setup,stmt="a>b",number=number)
print 'np: a*b : ', timeit.timeit(setup=setup,stmt="a*b",number=number)
print '==== float ===='
print 'float a>b : ', timeit.timeit(setup="a=1.1;b=2.3",stmt="a>b",number=number*100)
print 'float a*b : ', timeit.timeit(setup="a=1.1;b=2.3",stmt="a*b",number=number*100)
setup = "import numpy as np;a=np.arange(0,100,dtype=float);b=np.arange(100,0,-1,dtype=float);"
print 'np float a>b : ', timeit.timeit(setup=setup,stmt="a>b",number=number)
print 'np float a*b : ', timeit.timeit(setup=setup,stmt="a*b",number=number)
print '==== bool ===='
print 'a==b : ', timeit.timeit(setup="a=True;b=False",stmt="a==b",number=number*1000)
print 'a and b : ', timeit.timeit(setup="a=True;b=False",stmt="a and b",number=number*1000)
setup = "import numpy as np;a=np.arange(0,100)>50;b=np.arange(100,0,-1)>50;"
print 'np a == b : ', timeit.timeit(setup=setup,stmt="a == b",number=number)
print 'np a and b : ', timeit.timeit(setup=setup,stmt="np.logical_and(a,b)",number=number)
print 'np a == True : ', timeit.timeit(setup=setup,stmt="a == True",number=number)
print 'np a and True : ', timeit.timeit(setup=setup,stmt="np.logical_and(a,True)",number=number)
go()
==== int ====
a>b : 4.5121130192
a*b : 5.62955748632
np: a>b : 0.763992986986
np: a*b : 0.723006032235
==== float ====
float a>b : 6.39567713272
float a*b : 5.62149055215
np float a>b : 0.697037433398
np float a*b : 0.847941712765
==== bool ====
a==b : 6.91458288689
a and b : 3.6289697892
np a == b : 0.789666454087
np a and b : 0.724517620007
np a == True : 1.55066706189
np a and True : 1.44293071804
опять же, такое же поведение... Поэтому, я думаю, можно извлечь выгоду, используя вместо этого for == В целом,
по крайней мере, в Python 2 (Python 2.7.11 / Anaconda 2.4.1 (64-бит)| (по умолчанию, февраль 16 2016, 09:58:36) [MSC V. 1500 64 бит (AMD64)]), где я пробовал все это...