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

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

Я всегда думал, что сравнение - это самая быстрая операция, которую мог выполнить компьютер. Я помню, как слышал это на презентации от Д. Кнута, где он писал петли в порядке убывания "потому что сравнение с 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 features.
%quickref -> Quick reference.
help      -> Python 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
4b9b3361

Ответ 1

Это происходит из-за Constant Folding в Peep Hole optimizer в компиляторе 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        

Ответ 2

Как объяснили другие, это связано с тем, что оптимизатор Python оптимизирует арифметические операции, но не сравнивает.

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

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

Кажется, что разработчики Python, достаточно разумно, решили сначала оптимизировать арифметические операции. Возможно, они придут в сравнение в будущем выпуске.

Ответ 3

Быстрый дизассемблинг показывает, что сравнение включает в себя больше операций. В соответствии с этим ответом, есть некоторый предварительный расчет, сделанный оптимизатором писем ( 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

Ответ 4

Как прокомментировали другие, это связано с оптимизатором пип-дыры, который предварительно вычисляет результаты 2 * 3 (6). Как показано на рисунке

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

Если вы выполняете эти функции, сравнение будет быстрее.

Ответ 5

В случае python приведенные выше ответы верны. Для машинного кода ситуация немного сложнее. Я предполагаю, что мы говорим о целых операциях здесь, с поплавками и сложными объектами ни одно из ниже не будет применяться. Кроме того, мы предполагаем, что значения, которые вы сравниваете, уже загружены в регистры. Если они не извлекают их из любой точки, где бы они ни находились, они могли в 100 раз превышать фактические операции

Современные процессоры имеют несколько способов сравнения двух чисел. Очень популярны XOR a, b, если вы просто хотите увидеть, равны ли два значения или CMP a, b, если вы хотите знать взаимосвязь между значениями (меньше, больше, равно и т.д.). Операция CMP - это просто вычитание с отброшенным результатом, потому что нас интересуют только флаги post-op.

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

Однако умножение на 0,1 или любую силу 2 можно было бы свести к операции переключения. Это также операция с одной глубиной. Таким образом, требуется то же самое время, что и сравнение двух чисел. Подумайте о десятичной системе, вы можете умножить любое число на 10, 100, 1000 путем добавления нулей в конце номера. Любой оптимизирующий компилятор распознает этот тип умножения и использует для него наиболее эффективную операцию. Современные процессоры также довольно продвинуты, поэтому они могут выполнять такую ​​же оптимизацию в аппаратном обеспечении, подсчитывая, сколько бит установлено в любом из операндов. И если это всего лишь один бит, операция будет сведена к сдвигу.

Итак, в вашем случае умножение на 2 выполняется так же быстро, как сравнение двух чисел. Как указывали выше люди, любой оптимизирующий компилятор увидит, что вы умножаете две константы, поэтому вместо замены замените функцию возвратом константы.

Ответ 6

Ничего себе, ответ @mu 無 взорвал мой разум! Тем не менее, важно не обобщать, когда вы делаете выводы... Вы проверяете время для CONSTANTS не переменных. Для переменных умножение кажется медленнее, чем сравнение.

Вот более интересный случай, в котором сравниваемые числа хранятся в реальных переменных...

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

что происходит с bools?

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 Теперь это интересно, он выглядит логическим и быстрее, чем ==. Однако все это было бы нормально, так как Дональд Кнут не потерял свой сон, лучший способ сравнения - использовать и...

На практике мы должны проверить 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

Опять же, такое же поведение... Поэтому, я думаю, можно воспользоваться, используя вместо этого для == в целом,

по крайней мере в Python 2 (Python 2.7.11 | Anaconda 2.4.1 (64-разрядная версия) | (по умолчанию, 16 февраля 2016, 09:58:36) [MSC v.1500 64 бит (AMD64)]), где я пробовал все это...