Python: чому * та ** швидші за / та sqrt ()?


80

Оптимізуючи свій код, я зрозумів наступне:

>>> from timeit import Timer as T
>>> T(lambda : 1234567890 / 4.0).repeat()
[0.22256922721862793, 0.20560789108276367, 0.20530295372009277]
>>> from __future__ import division
>>> T(lambda : 1234567890 / 4).repeat()
[0.14969301223754883, 0.14155197143554688, 0.14141488075256348]
>>> T(lambda : 1234567890 * 0.25).repeat()
[0.13619112968444824, 0.1281130313873291, 0.12830305099487305]

а також:

>>> from math import sqrt
>>> T(lambda : sqrt(1234567890)).repeat()
[0.2597470283508301, 0.2498021125793457, 0.24994492530822754]
>>> T(lambda : 1234567890 ** 0.5).repeat()
[0.15409398078918457, 0.14059877395629883, 0.14049601554870605]

Я припускаю, що це пов’язано з тим, як python реалізований на мові C, але мені цікаво, чи хотів би хтось пояснити, чому це так?


Відповідь, яку ви прийняли на своє запитання (я припускаю, відповідає на ваше справжнє запитання), не має багато спільного з назвою вашого запитання. Не могли б ви відредагувати це, щоб мати щось спільне з постійним згортанням?
Zan Lynx

1
@ZanLynx - Привіт. Не могли б ви пояснити? Я вважаю, що заголовок запитання виражає саме те, що я хотів знати (чому X швидше Y), і що обрана мною відповідь робить саме це ... Мені здається цілком відповідним ... але, можливо, я щось пропускаю?
mac

8
Функції множення та степеня завжди швидші за функції ділення та sqrt () через їх саму природу. Ділення та кореневі операції, як правило, повинні використовувати низку точніших і тонших наближень і не можуть переходити безпосередньо до правильної відповіді, як це може робити множення.
Zan Lynx

Я відчуваю, що заголовок запитання повинен щось сказати про те, що ці значення - це буквальні константи, що, виявляється, є ключовим для відповіді. На типовому обладнанні цілі числа та FP множаться та додають / віднімають дешево; ціле число і FP div, і FP sqrt - все дорого (можливо, в 3 рази гірша затримка і в 10 разів гірша пропускна здатність, ніж FP mul). (Більшість процесорів реалізують ці операції в апаратному забезпеченні як інструкції з єдиним asm, на відміну від cube-root або pow () чи будь-якого іншого).
Пітер Кордес

1
Але я не був би здивований, якщо накладні витрати на інтерпретатор Python все-таки зменшили різницю між інструкціями mul та div asm. Цікавий факт: на x86 поділ FP, як правило, вищий, ніж цілочисельний. ( agner.org/optimize ). 64-розрядна цілочисельна поділка на Intel Skylake має латентність 42-95 циклів проти 26 циклів для 32-бітових цілих чисел проти 14 циклів для подвійної точності FP. (64-бітове ціле число помножується на 3 цикли затримки, FP mul - 4). Різниця в пропускній здатності ще більша (int / FP mul і add - це принаймні по одному на такт, але поділ і sqrt не повністю конвеєвані.)
Пітер Кордес

Відповіді:


114

(Дещо несподіваною) причиною ваших результатів є те, що Python, здається, складає постійні вирази, що передбачають множення з плаваючою точкою та піднесення до степені, але не ділення. math.sqrt()- це зовсім інший звір, оскільки для нього немає байт-коду, і він включає виклик функції.

На Python 2.6.5 такий код:

x1 = 1234567890.0 / 4.0
x2 = 1234567890.0 * 0.25
x3 = 1234567890.0 ** 0.5
x4 = math.sqrt(1234567890.0)

компілюється до таких байт-кодів:

  # x1 = 1234567890.0 / 4.0
  4           0 LOAD_CONST               1 (1234567890.0)
              3 LOAD_CONST               2 (4.0)
              6 BINARY_DIVIDE       
              7 STORE_FAST               0 (x1)

  # x2 = 1234567890.0 * 0.25
  5          10 LOAD_CONST               5 (308641972.5)
             13 STORE_FAST               1 (x2)

  # x3 = 1234567890.0 ** 0.5
  6          16 LOAD_CONST               6 (35136.418286444619)
             19 STORE_FAST               2 (x3)

  # x4 = math.sqrt(1234567890.0)
  7          22 LOAD_GLOBAL              0 (math)
             25 LOAD_ATTR                1 (sqrt)
             28 LOAD_CONST               1 (1234567890.0)
             31 CALL_FUNCTION            1
             34 STORE_FAST               3 (x4)

Як бачите, множення та піднесення до степеня взагалі не забирають часу, оскільки вони закінчуються, коли компілюється код. Ділення триває довше, оскільки це відбувається під час виконання. Квадратний корінь - це не тільки найбільш обчислювальна з розрахунку операція з чотирьох, вона також спричиняє різні накладні витрати, яких немає у інших (пошук атрибутів, виклик функції тощо).

Якщо усунути ефект постійного згортання, множення та ділення мало відокремлювати:

In [16]: x = 1234567890.0

In [17]: %timeit x / 4.0
10000000 loops, best of 3: 87.8 ns per loop

In [18]: %timeit x * 0.25
10000000 loops, best of 3: 91.6 ns per loop

math.sqrt(x)насправді трохи швидше, ніж x ** 0.5, мабуть, тому, що це особливий випадок останнього, і тому його можна зробити ефективніше, незважаючи на загальні витрати:

In [19]: %timeit x ** 0.5
1000000 loops, best of 3: 211 ns per loop

In [20]: %timeit math.sqrt(x)
10000000 loops, best of 3: 181 ns per loop

редагувати 16.11.2011: Постійне згортання виразів здійснюється за допомогою оптичного оптика Python. Вихідний код ( peephole.c) містить наступний коментар, який пояснює, чому константне ділення не складається:

    case BINARY_DIVIDE:
        /* Cannot fold this operation statically since
           the result can depend on the run-time presence
           of the -Qnew flag */
        return 0;

-QnewПрапор дозволяє «справжнє поділ» , певне в PEP 238 .


2
Можливо, він "захищає" від ділення на нуль?
hugomg

2
@missingno: Мені незрозуміло, чому такий "захист" був би необхідний, оскільки обидва аргументи відомі під час компіляції, а також результат (який є одним із: + inf, -inf, NaN).
NPE

13
Постійне згортання працює як з /Python 3, так і з //Python 2 і 3. Тож, швидше за все, це результат того факту, що /може мати різні значення в Python 2. Можливо, коли відбувається постійне згортання, поки невідомо, чи from __future__ import divisionє в ефекті?
інтермедія

4
@aix - 1./0.у Python 2.7 не випливає, NaNа a ZeroDivisionError.
помер

2
@Caridorc: Python компілюється в байт-код (файли .pyc), який потім інтерпретується під час виконання Python. Байт-код - це не те саме, що Асемблер / Машинний код (саме це генерує компілятор C). Модуль dis може бути використаний для перевірки байт-коду, до якого компілюється даний фрагмент коду.
Тоні Саффолк 66,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.