なぜpow(a、d、n)はa ** d%nよりもはるかに速いのですか?


110

私はMiller-Rabin素数性テストを実装しようとしていましたが、中規模の数値(約7桁)でこれほど長い時間(> 20秒)がかかるのはなぜですか。最終的に次のコード行が問題の原因であることがわかりました。

x = a**d % n

(ここaで、、、dおよびnはすべて類似していますが、中規模の数値が等しくなく**、指数演算子であり%、モジュロ演算子です)

次に、次のものに置き換えてみました。

x = pow(a, d, n)

それに比べると、それはほとんど瞬時です。

コンテキストについては、元の関数は次のとおりです。

from random import randint

def primalityTest(n, k):
    if n < 2:
        return False
    if n % 2 == 0:
        return False
    s = 0
    d = n - 1
    while d % 2 == 0:
        s += 1
        d >>= 1
    for i in range(k):
        rand = randint(2, n - 2)
        x = rand**d % n         # offending line
        if x == 1 or x == n - 1:
            continue
        for r in range(s):
            toReturn = True
            x = pow(x, 2, n)
            if x == 1:
                return False
            if x == n - 1:
                toReturn = False
                break
        if toReturn:
            return False
    return True

print(primalityTest(2700643,1))

時限計算の例:

from timeit import timeit

a = 2505626
d = 1520321
n = 2700643

def testA():
    print(a**d % n)

def testB():
    print(pow(a, d, n))

print("time: %(time)fs" % {"time":timeit("testA()", setup="from __main__ import testA", number=1)})
print("time: %(time)fs" % {"time":timeit("testB()", setup="from __main__ import testB", number=1)})

出力(PyPy 1.9.0で実行):

2642565
time: 23.785543s
2642565
time: 0.000030s

出力(Python 3.3.0、2.7.2で実行すると、非常に類似した時間を返します):

2642565
time: 14.426975s
2642565
time: 0.000021s

そして関連する質問、なぜこの計算はPython 2または3で実行すると、通常PyPyの方がPyPyの場合よりも2倍速くなるのですか?

回答:


164

モジュラ指数に関するウィキペディアの記事を参照してください。基本的に、を実行する場合a**d % n、実際にはを計算する必要がありますがa**d、これは非常に大きくなる可能性があります。しかし、それ自体a**d % nを計算する必要のない計算の方法がa**dあり、それが何をするかpowです。**それはあなたがすぐにモジュラスを取るしようとしていることを知って、「未来に見る」ことができないので、オペレータは、これを行うことはできません。


14
+1は実際には>>> print pow.__doc__ pow(x, y[, z]) -> number With two arguments, equivalent to x**y. With three arguments, equivalent to (x**y) % z, but may be more efficient (e.g. for longs).
ドキュメント

6
Pythonのバージョンによっては、これは特定の条件下でのみ当てはまる場合があります。IIRC、3.xおよび2.7では、整数型(および非負の累乗)でのみ3つの引数の形式を使用できます。また、常にネイティブint型で剰余剰余を取得しますが、他の整数型では必ずしもそうではありません。しかし、以前のバージョンでは、Cへの適合に関する規則がありlong、3つの引数の形式が許可されていましたfloat(おそらく、2.1以前を使用しておらず、Cモジュールのカスタムの整数型を使用していないため、どれもありません。これはあなたにとって重要です。)
abarnert 2013年

13
あなたの答えから、コンパイラが式を見てそれを最適化することは不可能であるように見えますが、それは真実ではありません。ちょうど起こる何の現在のPythonコンパイラがそれを行わないこと。
danielkza 2013年

5
@danielkza:それは本当です、私はそれが理論的に不可能であることを示唆するつもりはありませんでした。「未来を見ない」の方が「未来を見ない」よりも正確かもしれません。ただし、一般に最適化は非常に困難または不可能でさえあることに注意してください。ための定数オペランドは最適化することができる、しかしでx ** y % nx実装することを目的とすることができる__pow__と、乱数に基づいて、戻り実装いくつかの異なるオブジェクトのいずれ__mod__も乱数に依存する方法で、等
BrenBarn

2
@danielkza:また、関数には同じドメインがありません:.3 ** .4 % .5完全に合法ですが、コンパイラがこれに変換するとpow(.3, .4, .5)、が発生しTypeErrorます。コンパイラーは、、、およびが整数型の値であることが保証されていること(または変換が他の方法では役に立たないため、おそらく型の特定の値であることが保証されていること)を知る必要がaありd、負でないことが保証されている必要があります。これは、おそらくJITが実行できることですが、動的な型を持ち、推論のない言語用の静的コンパイラーでは不可能です。nintd
abarnert 2013年

37

ブレンバーンはあなたの主な質問に答えました。あなたのために:

Python 2または3で実行すると、通常PyPyの方がはるかに高速なのに、なぜPyPyの2倍近い速度になるのですか?

PyPyのパフォーマンスページを読んだ場合、これはまさにPyPyが得意ではないことです。実際、PyPyが提供する最初の例は次のとおりです。

悪い例としては、長いlongで計算を行うことが挙げられます。これは、最適化できないサポートコードによって実行されます。

理論的には、巨大な指数に続いてmodをモジュラー指数に変換すること(少なくとも最初のパスの後)は、JITが実行できる変換ですが、PyPyのJITは変換できません。

余談ですが、巨大な整数で計算する必要gmpyがある場合は、のようなサードパーティのモジュールを確認することをお勧めします。これは、場合によっては、CPythonのネイティブ実装よりもはるかに高速で、メインストリーム以外で使用されることもあり、便利ではないという犠牲を払って、他の方法では自分で記述しなければならない追加機能の。


2
longsが修正されました。pypy 2.0 beta 1を試してください(CPythonよりも速くはありませんが、遅くなることもありません)。gmpyには、MemoryError :(
fijal

@fijal:ええ、そしてgmpyいくつかのケースでは高速ではなく低速でもあり、多くの単純なものが不便になります。常にそうであるとは限りませんが、場合によってはそうです。したがって、巨大な整数を処理していて、Pythonのネイティブ型が十分に高速に見えない場合は、一見の価値があります。
abarnert 2013年

1
そして、もしあなたの数字が大きくてもあなたのプログラムがセグメンテーションフォールトになるかどうか気にしないなら
fijal

1
それは長い間、PyPyがGMPライブラリを使用しないようにした要因です。それはあなたにとっては大丈夫かもしれませんが、Python VM開発者にとっては大丈夫ではありません。mallocは、大量のRAMを使用せずに失敗する可能性があります。非常に大きな数をそこに置くだけです。それ以降のGMPの動作は定義されておらず、Pythonではこれを許可できません。
fijal 2013年

1
@fijal:Pythonの組み込み型の実装には使用しないでください。だからといって、これを何にも使用してはならないという意味ではありません。
abarnert 2013年

11

べき乗剰余を実行するためのショートカットがあります。たとえば、必要な中間結果をa**(2i) mod nすべてifromから1to log(d)に乗算して乗算(mod n)できます。3引数のような専用のモジュラー指数関数pow()は、モジュラー演算を実行していることを認識しているため、このようなトリックを活用できます。Pythonパーサーは、最小限の式を指定するとこれを認識できないa**d % nため、完全な計算を実行します(これにはかなり時間がかかります)。


3

x = a**d % n計算方法はad累乗し、それをでモジュロしnます。まず、aが大きい場合、膨大な数が作成され、その後切り捨てられます。ただし、x = pow(a, d, n)最後のn桁のみが追跡されるように最適化されている可能性が高く、これらはすべて、数値を法とする乗算の​​計算に必要なものです。


6
「x ** dを計算するにはd乗算が必要です」-不正解です。O(log d)(非常に広い)乗算でそれを行うことができます。二乗によるべき乗は、モジュールなしで使用できます。ここでは、被乗数の純粋なサイズが主導権を握っています。
John Dvorak

私はPythonがために同じ累乗アルゴリズムを利用しないだろうと思った理由@JanDvorak Trueの場合、私はわからない**ためとしてpow
Yuushi

5
最後の "n"桁ではありません。計算はZ / nZで保持されます。
Thomas
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.