क्या सूची-समझ और कार्यात्मक कार्य "छोरों के लिए" की तुलना में तेज़ हैं?


155

पायथन में प्रदर्शन के संदर्भ में, एक सूची-समझ, या कार्यों की तरह है map(), filter()और reduce()लूप के लिए तेजी से है? क्यों, तकनीकी रूप से, वे एक सी गति में चलते हैं, जबकि लूप अजगर आभासी मशीन की गति में चलता है

मान लीजिए कि जिस खेल में मैं विकसित हो रहा हूं, मुझे लूप के लिए जटिल और विशाल नक्शे खींचने की आवश्यकता है। यह प्रश्न निश्चित रूप से प्रासंगिक होगा, यदि उदाहरण के लिए एक सूची-समझ, वास्तव में तेज़ है, तो यह लैग से बचने के लिए एक बेहतर विकल्प होगा (कोड की दृश्य जटिलता के बावजूद)।

जवाबों:


146

अनुभव के आधार पर मोटे दिशानिर्देश और शिक्षित अनुमान हैं। आपको timeitहार्ड नंबर प्राप्त करने के लिए अपने ठोस उपयोग के मामले को प्रोफाइल करना चाहिए, या वे संख्याएँ कभी-कभी नीचे से असहमत हो सकती हैं।

एक सूची की समझ आमतौर पर ठीक समकक्ष forलूप (जो वास्तव में एक सूची बनाता है) की तुलना में एक छोटा सा तेज है , सबसे अधिक संभावना है क्योंकि इसमें appendप्रत्येक पुनरावृत्ति पर सूची और इसकी विधि को नहीं देखना पड़ता है । हालांकि, एक सूची समझ अभी भी एक बायोटेक-स्तर लूप करता है:

>>> dis.dis(<the code object for `[x for x in range(10)]`>)
 1           0 BUILD_LIST               0
             3 LOAD_FAST                0 (.0)
       >>    6 FOR_ITER                12 (to 21)
             9 STORE_FAST               1 (x)
            12 LOAD_FAST                1 (x)
            15 LIST_APPEND              2
            18 JUMP_ABSOLUTE            6
       >>   21 RETURN_VALUE

लूप के स्थान पर सूची समझ का उपयोग करना जो सूची का निर्माण नहीं करता है , निरर्थक मूल्यों की सूची को निरर्थक रूप से संचित करता है और फिर सूची को फेंक देता है, अक्सर सूची बनाने और निकालने के ओवरहेड के कारण धीमा होता है। सूची की समझ कोई जादू नहीं है जो स्वाभाविक रूप से एक अच्छे पुराने लूप की तुलना में तेज़ है।

कार्यात्मक सूची प्रसंस्करण कार्यों के लिए के रूप में: इन सी में लिखे गए हैं और शायद पायथन में लिखा बराबर कार्यों मात करते हैं, वे कर रहे हैं नहीं जरूरी सबसे तेजी से विकल्प। कुछ गति की उम्मीद है अगर फ़ंक्शन सी में भी लिखा गया है। लेकिन lambda(या अन्य पायथन फ़ंक्शन) का उपयोग करने वाले अधिकांश मामलों में , पायथन स्टैक फ्रेम आदि को बार-बार सेट करने का ओवरहेड किसी भी बचत को खाता है। बिना फ़ंक्शन कॉल के बस एक ही काम इन-लाइन, (उदाहरण के लिए mapया इसके बजाय एक सूची समझ filter) अक्सर थोड़ा तेज होता है।

मान लीजिए कि जिस खेल में मैं विकसित हो रहा हूं, मुझे लूप के लिए जटिल और विशाल नक्शे खींचने की आवश्यकता है। यह प्रश्न निश्चित रूप से प्रासंगिक होगा, यदि उदाहरण के लिए एक सूची-समझ, वास्तव में तेज़ है, तो यह लैग से बचने के लिए एक बेहतर विकल्प होगा (कोड की दृश्य जटिलता के बावजूद)।

संभावना है, अगर इस तरह का कोड पहले से ही काफी तेज नहीं है जब अच्छे गैर में लिखा गया है- "अनुकूलित" पायथन, पायथन स्तर के सूक्ष्म अनुकूलन की कोई राशि इसे तेजी से पर्याप्त नहीं बनाने जा रही है और आपको सी को छोड़ने के बारे में सोचना शुरू करना चाहिए, जबकि व्यापक। माइक्रो ऑप्टिमाइज़ेशन अक्सर पायथन कोड को गति प्रदान कर सकते हैं, काफी कम (पूर्ण शब्दों में) इसकी सीमा होती है। इसके अलावा, इससे पहले कि आप उस छत को मारते हैं, यह बुलेट को काटने और कुछ सी लिखने के लिए बस अधिक लागत कुशल (15% स्पीड बनाम 300% एक ही प्रयास के साथ) हो जाता है।


25

यदि आप python.org पर जानकारी की जाँच करते हैं , तो आप इस सारांश को देख सकते हैं:

Version Time (seconds)
Basic loop 3.47
Eliminate dots 2.45
Local variable & no dots 1.79
Using map function 0.54

लेकिन आपको वास्तव में प्रदर्शन अंतर के कारण को समझने के लिए उपरोक्त लेख को विवरण में पढ़ना चाहिए

मैं दृढ़ता से सुझाव देता हूं कि आपको अपने कोड का उपयोग करके समय देना चाहिए timeit । दिन के अंत में, ऐसी स्थिति हो सकती है जहां, उदाहरण के लिए, forकिसी शर्त के पूरा होने पर आपको लूप से बाहर निकलने की आवश्यकता हो सकती है। यह संभवतः कॉल करके परिणाम का पता लगाने की तुलना में तेज़ हो सकता है map


17
जबकि वह पृष्ठ एक अच्छा पढ़ा और आंशिक रूप से संबंधित है, बस उन संख्याओं को उद्धृत करना मददगार नहीं है, संभवतः भ्रामक भी।

1
यह इस बात का कोई संकेत नहीं देता है कि आप क्या कर रहे हैं। लूप / लिस्टकैंप / मैप में क्या है इसके आधार पर सापेक्ष प्रदर्शन बहुत भिन्न होगा।
user2357112

@delnan मैं सहमत हूँ। मैंने प्रदर्शन में अंतर को समझने के लिए प्रलेखन को पढ़ने के लिए ओपी से आग्रह करने के लिए अपना जवाब संशोधित किया है।
एंथनी कोंग

@ user2357112 आपको संदर्भ के लिए लिंक किए गए विकी पृष्ठ को पढ़ना होगा। मैंने इसे ओपी के संदर्भ के लिए पोस्ट किया।
एंथोनी कोंग

13

आप विशेष रूप से के बारे में पूछने map(), filter()और reduce()है, लेकिन मुझे लगता है कि आप सामान्य रूप में कार्यात्मक प्रोग्रामिंग के बारे में जानना चाहते हैं। अंक के एक सेट के भीतर सभी बिंदुओं के बीच कंप्यूटिंग दूरी की समस्या पर स्वयं इसका परीक्षण करने के बाद, फंक्शनल प्रोग्रामिंग ( starmapबिल्ट-इन itertoolsमॉड्यूल से फ़ंक्शन का उपयोग करके ) लूप की तुलना में थोड़ा धीमा हो गया (लंबे समय तक 1.25 बार ले रहा है, में) तथ्य)। यहाँ नमूना कोड मैंने इस्तेमाल किया है:

import itertools, time, math, random

class Point:
    def __init__(self,x,y):
        self.x, self.y = x, y

point_set = (Point(0, 0), Point(0, 1), Point(0, 2), Point(0, 3))
n_points = 100
pick_val = lambda : 10 * random.random() - 5
large_set = [Point(pick_val(), pick_val()) for _ in range(n_points)]
    # the distance function
f_dist = lambda x0, x1, y0, y1: math.sqrt((x0 - x1) ** 2 + (y0 - y1) ** 2)
    # go through each point, get its distance from all remaining points 
f_pos = lambda p1, p2: (p1.x, p2.x, p1.y, p2.y)

extract_dists = lambda x: itertools.starmap(f_dist, 
                          itertools.starmap(f_pos, 
                          itertools.combinations(x, 2)))

print('Distances:', list(extract_dists(point_set)))

t0_f = time.time()
list(extract_dists(large_set))
dt_f = time.time() - t0_f

क्रियात्मक संस्करण प्रक्रियात्मक संस्करण की तुलना में तेज़ है?

def extract_dists_procedural(pts):
    n_pts = len(pts)
    l = []    
    for k_p1 in range(n_pts - 1):
        for k_p2 in range(k_p1, n_pts):
            l.append((pts[k_p1].x - pts[k_p2].x) ** 2 +
                     (pts[k_p1].y - pts[k_p2].y) ** 2)
    return l

t0_p = time.time()
list(extract_dists_procedural(large_set)) 
    # using list() on the assumption that
    # it eats up as much time as in the functional version

dt_p = time.time() - t0_p

f_vs_p = dt_p / dt_f
if f_vs_p >= 1.0:
    print('Time benefit of functional progamming:', f_vs_p, 
          'times as fast for', n_points, 'points')
else:
    print('Time penalty of functional programming:', 1 / f_vs_p, 
          'times as slow for', n_points, 'points')

2
इस सवाल का जवाब देने के लिए एक बल्कि जटिल तरीका लगता है। क्या आप इसे कम कर सकते हैं ताकि यह बेहतर समझ में आए?
हारून हॉल

2
@AaronHall मैं वास्तव में andreipmbcn के उत्तर को अधिक दिलचस्प मानता हूं क्योंकि यह एक गैर-तुच्छ उदाहरण है। कोड हम साथ खेल सकते हैं।
एंथोनी कोंग

@AaronHall, क्या आप चाहते हैं कि मैं पाठ पैराग्राफ को संपादित करूं, ताकि यह अधिक स्पष्ट और सीधा लगे, या आप मुझे कोड संपादित करना चाहते हैं?
andreipmbcn

9

मैंने एक साधारण स्क्रिप्ट लिखी जो गति का परीक्षण करती है और यही मुझे पता चला है। वास्तव में पाश मेरे मामले में सबसे तेज था। यह वास्तव में मुझे आश्चर्यचकित करता है, बलो को देखें (वर्गों की गणना कर रहा था)।

from functools import reduce
import datetime


def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next**2, numbers, 0)


def square_sum2(numbers):
    a = 0
    for i in numbers:
        i = i**2
        a += i
    return a

def square_sum3(numbers):
    sqrt = lambda x: x**2
    return sum(map(sqrt, numbers))

def square_sum4(numbers):
    return(sum([int(i)**2 for i in numbers]))


time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
0:00:00.302000 #Reduce
0:00:00.144000 #For loop
0:00:00.318000 #Map
0:00:00.390000 #List comprehension

अजगर 3.6.1 के साथ अंतर इतना बड़ा नहीं है; कम करें और नक्शा 0.24 पर जाएं और 0.29 की सूची समझें। अधिक है, 0.18 पर।
jjmerelo

खत्म intमें square_sum4भी यह काफ़ी तेजी से और पाश के लिए की तुलना में सिर्फ थोड़ा धीमा कर देता है।
jjmerelo

6

मैंने @ अलीसा के कोड को संशोधित किया और cProfileयह दिखाने के लिए इस्तेमाल किया कि सूची की समझ तेज क्यों है:

from functools import reduce
import datetime

def reduce_(numbers):
    return reduce(lambda sum, next: sum + next * next, numbers, 0)

def for_loop(numbers):
    a = []
    for i in numbers:
        a.append(i*2)
    a = sum(a)
    return a

def map_(numbers):
    sqrt = lambda x: x*x
    return sum(map(sqrt, numbers))

def list_comp(numbers):
    return(sum([i*i for i in numbers]))

funcs = [
        reduce_,
        for_loop,
        map_,
        list_comp
        ]

if __name__ == "__main__":
    # [1, 2, 5, 3, 1, 2, 5, 3]
    import cProfile
    for f in funcs:
        print('=' * 25)
        print("Profiling:", f.__name__)
        print('=' * 25)
        pr = cProfile.Profile()
        for i in range(10**6):
            pr.runcall(f, [1, 2, 5, 3, 1, 2, 5, 3])
        pr.create_stats()
        pr.print_stats()

यहाँ परिणाम है:

=========================
Profiling: reduce_
=========================
         11000000 function calls in 1.501 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.162    0.000    1.473    0.000 profiling.py:4(reduce_)
  8000000    0.461    0.000    0.461    0.000 profiling.py:5(<lambda>)
  1000000    0.850    0.000    1.311    0.000 {built-in method _functools.reduce}
  1000000    0.028    0.000    0.028    0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: for_loop
=========================
         11000000 function calls in 1.372 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.879    0.000    1.344    0.000 profiling.py:7(for_loop)
  1000000    0.145    0.000    0.145    0.000 {built-in method builtins.sum}
  8000000    0.320    0.000    0.320    0.000 {method 'append' of 'list' objects}
  1000000    0.027    0.000    0.027    0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: map_
=========================
         11000000 function calls in 1.470 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.264    0.000    1.442    0.000 profiling.py:14(map_)
  8000000    0.387    0.000    0.387    0.000 profiling.py:15(<lambda>)
  1000000    0.791    0.000    1.178    0.000 {built-in method builtins.sum}
  1000000    0.028    0.000    0.028    0.000 {method 'disable' of '_lsprof.Profiler' objects}


=========================
Profiling: list_comp
=========================
         4000000 function calls in 0.737 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
  1000000    0.318    0.000    0.709    0.000 profiling.py:18(list_comp)
  1000000    0.261    0.000    0.261    0.000 profiling.py:19(<listcomp>)
  1000000    0.131    0.000    0.131    0.000 {built-in method builtins.sum}
  1000000    0.027    0.000    0.027    0.000 {method 'disable' of '_lsprof.Profiler' objects}

IMHO:

  • reduceऔर mapसामान्य रूप से बहुत धीमे हैं। इतना ही नहीं, सूची में शामिल होने की तुलना में, sumपुनरावृत्तियों पर उपयोग mapधीमा हैsum
  • for_loop एपेंड का उपयोग करता है, जो कुछ हद तक धीमा है
  • लिस्ट-कॉम्प्रिहेंशन ने न केवल लिस्ट बनाने में कम से कम समय बिताया, यह sumइसके विपरीत भी बहुत तेज हैmap

5

अल्फी जवाब में एक मोड़ जोड़कर , वास्तव में लूप के लिए दूसरा सबसे अच्छा होगा और लगभग 6 गुना धीमा होगाmap

from functools import reduce
import datetime


def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next**2, numbers, 0)


def square_sum2(numbers):
    a = 0
    for i in numbers:
        a += i**2
    return a

def square_sum3(numbers):
    a = 0
    map(lambda x: a+x**2, numbers)
    return a

def square_sum4(numbers):
    a = 0
    return [a+i**2 for i in numbers]

time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])

मुख्य परिवर्तन धीमे sumकॉल को खत्म करने के लिए किए गए हैं, साथ ही साथ int()आखिरी मामले में संभवतः अनावश्यक भी हैं । लूप और मानचित्र को समान शब्दों में रखना वास्तव में इसे काफी तथ्यपूर्ण बनाता है। याद रखें कि लैम्ब्डा कार्यात्मक अवधारणाएं हैं और सैद्धांतिक रूप से साइड इफेक्ट नहीं होना चाहिए, लेकिन, ठीक है, वे साइड इफेक्ट्स जैसे जोड़ सकते हैं a। इस मामले में पायथन 3.6.1, उबंटू 14.04, इंटेल (आर) कोर (टीएम) i7-4770 सीपीयू 3.30GHz के साथ परिणाम

0:00:00.257703 #Reduce
0:00:00.184898 #For loop
0:00:00.031718 #Map
0:00:00.212699 #List comprehension

2
square_sum3 और square_sum4 गलत हैं। वे योग नहीं देंगे। @Alisca chen से नीचे का उत्तर वास्तव में सही है।
शिखरडुआ

3

मैं कुछ @ अल्फ़िजी कोड को संशोधित करने में कामयाब रहा और पाया कि सूची समझ पाश की तुलना में थोड़ी तेज़ है। यह कारण हो सकता है int(), यह सूची समझ और लूप के बीच उचित नहीं है।

from functools import reduce
import datetime

def time_it(func, numbers, *args):
    start_t = datetime.datetime.now()
    for i in range(numbers):
        func(args[0])
    print (datetime.datetime.now()-start_t)

def square_sum1(numbers):
    return reduce(lambda sum, next: sum+next*next, numbers, 0)

def square_sum2(numbers):
    a = []
    for i in numbers:
        a.append(i*2)
    a = sum(a)
    return a

def square_sum3(numbers):
    sqrt = lambda x: x*x
    return sum(map(sqrt, numbers))

def square_sum4(numbers):
    return(sum([i*i for i in numbers]))

time_it(square_sum1, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum2, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum3, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
time_it(square_sum4, 100000, [1, 2, 5, 3, 1, 2, 5, 3])
0:00:00.101122 #Reduce

0:00:00.089216 #For loop

0:00:00.101532 #Map

0:00:00.068916 #List comprehension
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.