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

1
@ ZanLynx-こんにちは。明確にしていただけませんか。質問のタイトルは私が知りたいことを正確に表現しており(XがYよりも速い理由)、選択した答えはそれを正確に示していることがわかりました...私には完全に一致しているようです...しかし、何かを見落としているのではないでしょうか?
mac

8
乗算関数と累乗関数は、その性質上、除算関数とsqrt()関数よりも常に高速です。除算とルート演算は、一般に、一連のより細かい近似を使用する必要があり、乗算のように正しい答えに直接進むことはできません。
Zan Lynx 2011年

質問のタイトルは、値がすべてリテラル定数であるという事実について何かを言うべきだと思います。これが答えの鍵であることがわかります。一般的なハードウェアでは、整数とFPの乗算と加算/減算は安価です。integerとFPdiv、およびFP sqrtはすべて高価です(おそらく、FP mulの3倍のレイテンシー、10倍のスループット)。(ほとんどのCPUは、cube-rootやpow()などとは異なり、これらの操作を単一のasm命令としてハードウェアに実装します)。
ピーターコーデス2016

1
しかし、Pythonインタープリターのオーバーヘッドが、mul命令とdiv asm命令の違いを小さくしたとしても、私は驚かないでしょう。おもしろい事実:x86では、FP除算は通常整数除算よりもパフォーマンスが高くなります。(agner.org/optimize)。Intel Skylakeでの64ビット整数除算のレイテンシは42〜95サイクルであるのに対し、32ビット整数では26サイクル、倍精度FPでは14サイクルです。(64ビット整数乗算は3サイクルのレイテンシー、FP mulは4です)。スループットの差はさらに大きくなります(int / FP mulとaddはすべてクロックごとに少なくとも1つですが、除算と平方根は完全にパイプライン化されていません)
Peter Cordes 2016

回答:


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)

ご覧のとおり、乗算とべき乗はコードのコンパイル時に行われるため、まったく時間がかかりません。分割は実行時に発生するため、時間がかかります。平方根は、4つの中で最も計算コストの高い操作であるだけでなく、他の操作では発生しないさまざまなオーバーヘッド(属性ルックアップ、関数呼び出しなど)も発生します。

定数畳み込みの影響を排除すると、乗算と除算を分離することはほとんどありません。

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

編集2011-11-16:定数式の折りたたみは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フラグは、PEP238で定義されている「真の除算」を有効にします。


2
おそらくそれはゼロ除算から「保護」しているのでしょうか?
hugomg 2011年

2
@missingno:コンパイル時に両方の引数がわかっているので、なぜそのような「保護」が必要なのかは私にはわかりません。結果も同様です(+ inf、-inf、NaNのいずれか)。
NPE

13
一定の折りたたみがで動作する/Pythonの3で、及びで//Pythonの2と3に最も可能性が高いので、これは事実の結果である/一定の折りたたみが行われているかもしれないときはPython 2に異なる意味を持つことができ、それはかどうかはまだ知られていないfrom __future__ import divisionIS事実上?
interjay 2011年

4
@aix - 1./0.Pythonの2.7ににはなりませんNaNけどZeroDivisionError
2011年

2
@Caridorc:Pythonはバイトコード(.pycファイル)にコンパイルされ、Pythonランタイムによって解釈されます。バイトコードは、アセンブリ/マシンコード(たとえば、Cコンパイラが生成するもの)と同じではありません。disモジュールを使用して、特定のコードフラグメントがコンパイルされるバイトコードを調べることができます。
トニーサフォーク66
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.