เหตุใดธาร (a, d, n) จึงเร็วกว่า ** d% n มาก


110

ฉันกำลังพยายามใช้การทดสอบพื้นฐานของมิลเลอร์ - ราบินและรู้สึกงงว่าทำไมจึงใช้เวลานานมาก (> 20 วินาที) สำหรับตัวเลขขนาดกลาง (~ 7 หลัก) ในที่สุดฉันก็พบว่าบรรทัดของโค้ดต่อไปนี้เป็นที่มาของปัญหา:

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

และคำถามที่เกี่ยวข้องทำไมการคำนวณนี้เกือบสองเท่าอย่างรวดเร็วเมื่อทำงานกับงูหลาม 2 หรือ 3 กว่าด้วย PyPy เมื่อมัก PyPy เป็นได้เร็วขึ้นมาก ?

คำตอบ:


164

ดูบทความวิกิพีเดียในการยกกำลังแบบแยกส่วน โดยทั่วไปเมื่อคุณทำa**d % nคุณต้องคำนวณจริงๆa**dซึ่งอาจมีขนาดค่อนข้างใหญ่ แต่มีวิธีการคำนวณa**d % nโดยไม่ต้องคำนวณa**dเองและนั่นคือสิ่งที่powทำ ตัว**ดำเนินการไม่สามารถทำสิ่งนี้ได้เพราะมันไม่สามารถ "มองเห็นอนาคต" เพื่อรู้ว่าคุณกำลังจะรับโมดูลัสทันที


14
+1 นั่นคือความหมายของ docstring>>> 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).
Hedde van der Heide

6
ขึ้นอยู่กับเวอร์ชัน Python ของคุณสิ่งนี้อาจเป็นจริงภายใต้เงื่อนไขบางประการเท่านั้น IIRC ใน 3.x และ 2.7 คุณสามารถใช้รูปแบบสามอาร์กิวเมนต์กับประเภทอินทิกรัลเท่านั้น (และกำลังที่ไม่ใช่ค่าลบ) และคุณจะได้รับการยกกำลังแบบแยกส่วนกับintประเภทเนทีฟเสมอ แต่ไม่จำเป็นต้องใช้กับประเภทอินทิกรัลอื่น ๆ แต่ในเวอร์ชันเก่าจะมีกฎเกี่ยวกับการปรับให้เข้ากับ C longอนุญาตให้ใช้รูปแบบสามอาร์กิวเมนต์สำหรับfloatฯลฯ (หวังว่าคุณจะไม่ได้ใช้ 2.1 หรือก่อนหน้านี้และไม่ได้ใช้ประเภทอินทิกรัลที่กำหนดเองใด ๆ จากโมดูล C ดังนั้นจึงไม่มี เรื่องนี้สำคัญสำหรับคุณ)
ยกเลิก

13
จากคำตอบของคุณดูเหมือนว่าเป็นไปไม่ได้ที่คอมไพเลอร์จะเห็นนิพจน์และปรับให้เหมาะสมซึ่งไม่เป็นความจริง มันเกิดขึ้นที่ไม่มีคอมไพเลอร์ Python ในปัจจุบันทำ
danielkza

5
@danielkza: นั่นเป็นความจริงฉันไม่ได้ตั้งใจจะบอกว่ามันเป็นไปไม่ได้ในทางทฤษฎี บางที "มองไม่เห็นอนาคต" น่าจะถูกต้องกว่า "มองไม่เห็นอนาคต" อย่างไรก็ตามโปรดทราบว่าการเพิ่มประสิทธิภาพอาจเป็นเรื่องยากมากหรือเป็นไปไม่ได้โดยทั่วไป สำหรับการคงถูกดำเนินการก็อาจจะเหมาะ แต่ในx ** y % n, xอาจจะเป็นวัตถุที่นำไปปฏิบัติ__pow__และขึ้นอยู่กับจำนวนสุ่มกลับหนึ่งของวัตถุที่แตกต่างกันการดำเนินการ__mod__ในรูปแบบที่ยังขึ้นอยู่กับตัวเลขสุ่ม ฯลฯ
BrenBarn

2
@danielkza: นอกจากนี้ฟังก์ชั่นไม่มีโดเมนเดียวกัน: .3 ** .4 % .5ถูกต้องตามกฎหมาย แต่ถ้าคอมไพเลอร์เปลี่ยนเป็นไฟล์pow(.3, .4, .5)นั้นจะเพิ่มไฟล์TypeError. คอมไพเลอร์จะต้องสามารถที่จะรู้ว่าa, dและnมีการรับประกันว่าค่านิยมของประเภทหนึ่ง (หรืออาจจะแค่เฉพาะประเภทintเพราะการเปลี่ยนแปลงไม่ได้ช่วยอย่างอื่น) และdรับประกันได้ว่าจะไม่เป็นลบ นั่นเป็นสิ่งที่ JIT สามารถทำได้ แต่คอมไพเลอร์แบบคงที่สำหรับภาษาที่มีชนิดไดนามิกและไม่มีการอนุมานก็ทำไม่ได้
ยกเลิก

37

BrenBarn ตอบคำถามหลักของคุณ สำหรับคุณ:

ทำไมเวลารันด้วย Python 2 หรือ 3 ถึงเร็วกว่า PyPy เกือบสองเท่าในเมื่อปกติแล้ว PyPy จะเร็วกว่ามาก

หากคุณอ่านหน้าประสิทธิภาพของ PyPy นี่เป็นสิ่งที่ PyPy ไม่ดีในความเป็นจริงตัวอย่างแรกที่พวกเขาให้:

ตัวอย่างที่ไม่ดี ได้แก่ การคำนวณด้วยความยาวขนาดใหญ่ซึ่งดำเนินการโดยรหัสสนับสนุนที่ไม่สามารถปรับขนาดได้

ในทางทฤษฎีการเปลี่ยนเลขชี้กำลังขนาดใหญ่ตามด้วย mod เป็นเลขชี้กำลังแบบโมดูลาร์ (อย่างน้อยหลังจากผ่านครั้งแรก) เป็นการเปลี่ยนแปลงที่ JIT อาจสามารถสร้างได้ ... แต่ไม่ใช่ JIT ของ PyPy

โปรดทราบว่าหากคุณต้องการคำนวณด้วยจำนวนเต็มจำนวนมากคุณอาจต้องการดูโมดูลของบุคคลที่สามเช่นgmpyซึ่งบางครั้งอาจเร็วกว่าการใช้งานแบบเนทีฟของ CPython ในบางกรณีนอกการใช้งานกระแสหลักและยังมีจำนวนมาก ฟังก์ชันเพิ่มเติมที่คุณต้องเขียนเองโดยเสียค่าใช้จ่ายน้อยกว่า


2
ความยาวได้รับการแก้ไข ลอง pypy 2.0 beta 1 (จะไม่เร็วกว่า CPython แต่ก็ไม่ควรช้าลงเช่นกัน) gmpy ไม่มีวิธีจัดการ MemoryError :(
fijal

@fijal: ใช่และgmpyยังช้ากว่าแทนที่จะเร็วกว่าในบางกรณีและทำให้สิ่งง่ายๆหลายอย่างไม่ค่อยสะดวก ไม่ใช่คำตอบเสมอไป แต่บางครั้งก็เป็นเช่นนั้น ดังนั้นจึงควรดูว่าคุณกำลังจัดการกับจำนวนเต็มจำนวนมากหรือไม่และประเภทเนทีฟของ Python ดูเหมือนจะไม่เร็วพอ
ยกเลิก

1
และถ้าคุณไม่สนใจว่าตัวเลขของคุณจะใหญ่ทำให้โปรแกรมของคุณ
ผิดพลาดหรือไม่

1
เป็นปัจจัยที่ทำให้ PyPy ไม่ใช้ไลบรารี GMP เนื่องจากเป็นเวลานาน มันอาจจะโอเคสำหรับคุณไม่เป็นไรสำหรับนักพัฒนา Python VM malloc สามารถล้มเหลวได้โดยไม่ต้องใช้ RAM เป็นจำนวนมากเพียงแค่ใส่ตัวเลขจำนวนมากไว้ที่นั่น พฤติกรรมของ GMP จากจุดนั้นไม่ได้กำหนดไว้และ Python ไม่สามารถอนุญาตสิ่งนี้ได้
fijal

1
@fijal: ฉันเห็นด้วยอย่างยิ่งว่าไม่ควรใช้สำหรับการใช้งานประเภทในตัวของ Python นั่นไม่ได้หมายความว่าไม่ควรใช้กับอะไรเลย
ยกเลิก

11

มีทางลัดในการทำเลขชี้กำลังแบบโมดูลาร์เช่นคุณสามารถค้นหาa**(2i) mod nทุก ๆiจาก1ถึงlog(d)และคูณด้วยกัน (mod n) ผลลัพธ์ระดับกลางที่คุณต้องการ ฟังก์ชันโมดูลาร์ - เลขชี้กำลังเฉพาะเช่น 3 อาร์กิวเมนต์pow()สามารถใช้ประโยชน์จากเทคนิคดังกล่าวได้เนื่องจากรู้ว่าคุณกำลังทำเลขคณิตแบบแยกส่วน ตัวแยกวิเคราะห์ Python ไม่สามารถจดจำสิ่งนี้ได้เนื่องจากนิพจน์เปล่าa**d % nดังนั้นจะทำการคำนวณแบบเต็ม (ซึ่งจะใช้เวลานานกว่ามาก)


3

วิธีการที่x = a**d % nมีการคำนวณคือการเพิ่มaกับdอำนาจแล้ว modulo nว่า ประการแรกถ้าaมีขนาดใหญ่สิ่งนี้จะสร้างจำนวนมากซึ่งจะถูกตัดทอน อย่างไรก็ตามx = pow(a, d, n)มีแนวโน้มที่จะปรับให้เหมาะสมที่สุดเพื่อให้nมีการติดตามเฉพาะตัวเลขสุดท้ายซึ่งเป็นสิ่งที่จำเป็นสำหรับการคำนวณโมดูโลตัวเลข


6
"ต้องใช้การคูณ d ในการคำนวณ x ** d" - ไม่ถูกต้อง คุณสามารถทำได้ในการคูณ O (log d) (กว้างมาก) การยกกำลังสองสามารถใช้โดยไม่ต้องใช้โมดูล ขนาดที่แท้จริงของตัวคูณคือสิ่งที่นำไปสู่ที่นี่
John Dvorak

@JanDvorak ทรูผมไม่แน่ใจว่าทำไมผมคิดว่างูใหญ่จะไม่ใช้ขั้นตอนวิธีการยกกำลังเหมือนกันสำหรับสำหรับ** pow
Yuushi

5
ไม่ใช่ "n" หลักสุดท้าย .. มันจะเก็บการคำนวณเป็น Z / nZ
Thomas
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.