2025, Oct 01 15:31

NumPy 1D ऐरे में अंतिम non-zero इंडेक्स: Python लूप बनाम Numba JIT

1D NumPy ऐरे में अंतिम non-zero इंडेक्स तेजी से कैसे पाएं? Python लूप, NumPy flatnonzero और Numba JIT की तुलना, परफॉर्मेंस और व्यावहारिक ऑप्टिमाइज़ेशन टिप्स।

1D NumPy ऐरे में अंतिम non-zero तत्व का इंडेक्स ढूंढना सुनने में आसान लगता है, लेकिन बड़े इनपुट पर इसे लाखों बार करना पड़े तो बात बदल जाती है। जब परफॉर्मेंस दांव पर हो, तो सीधा-सादा तरीका भी हैरान कर सकता है, और सबसे “NumPy-जैसा” दिखने वाला उपाय हमेशा तेज नहीं होता। नीचे समस्या का संक्षिप्त खाका, संभावित मुश्किलें, और वह समाधान दिया है जो व्यवहार में लगातार बेहतर साबित होता है।

समस्या विवरण

आपके पास 0 और 1 से बना 1D NumPy ऐरे है और आपको अंतिम non-zero प्रविष्टि का इंडेक्स चाहिए। उदाहरण के तौर पर [0, 1, 1, 0, 0, 1, 0, 0] के लिए परिणाम 5 होगा, क्योंकि यही वह आख़िरी स्थान है जहाँ मान 1 है।

मूलभूत तरीके

सबसे सरल तरीका है अंत से शुरुआत की ओर एक शुद्ध Python लूप चलाना। व्यवहार में, यह कई स्थितियों में सामान्य NumPy-केवल विकल्पों से तेज भी साबित होता है।

import numpy as np
def tail_active_index(arr):
    for j in range(arr.size - 1, -1, -1):
        if arr[j] != 0:
            return j
    return None

एक NumPy-आधारित विकल्प उल्टे व्यू को स्कैन करता है और उसमें पहले non-zero स्थान को ढूंढने के लिए flatnonzero का उपयोग करता है, फिर उसे मूल इंडेक्स में मैप कर देता है।

import numpy as np
def tail_active_index_np(arr):
    rev_hits = np.flatnonzero(arr[::-1])
    return arr.size - 1 - rev_hits[0] if rev_hits.size else None

व्यवहार में, साधारण लूप अक्सर NumPy वाले तरीक़े से बेहतर चलता है। लेकिन यह बढ़त तभी रहती है जब आख़िरी non-zero तत्व अंत के क़रीब हो; बहुत बड़े ऐरे में यदि अकेला non-zero शुरुआत के पास हो, तो यही लूप विकल्पों की तुलना में कई गुना धीमा पड़ जाता है। एक छोटा-सा इटरेशन विवरण भी मायने रखता है: उल्टा व्यू बनाकर उस पर range(arr.size) के साथ आगे की ओर इटरेट करना, रेंज को उलटने की तुलना में लगभग 2x तेज हो सकता है।

असल में हो क्या रहा है

यह काम मूलतः एक रैखिक स्कैन है: या तो आप पीछे से चलते हैं जब तक पहला non-zero न मिल जाए, या कोई सहायक फ़ंक्शन लेते हैं जो अंततः वही खोज करता है। जब यही चीज़ लाखों बार कसी हुई लूप में चलती है, तो कंट्रोल-फ्लो का ओवरहेड बेहद मायने रखता है। शुद्ध Python तरीका कॉम्पैक्ट है और अगर आख़िरी non-zero अंत के पास हो तो जल्दी रुक सकता है। उल्टा, बहुत बड़े ऐरे में वह non-zero यदि शुरुआत के करीब छिपा हो, तो लूप लगभग पूरा रेंज पार करता है और काफी धीमा हो जाता है। NumPy वाला तरीका ऐरे-स्तर पर ज़्यादा काम करता है और अक्सर अतिरिक्त ओवरहेड देता है; इसलिए दिखने में कम “vectorized” होने के बावजूद सीधा लूप कई स्थितियों में उसे पछाड़ देता है।

बेहतर परिणाम देने वाला तरीका: Numba JIT संकलन

इस कार्य के लिए Numba JIT संकलन NumPy-आधारित किसी भी तरीके से काफ़ी तेज है। यह लूप की सादगी बनाए रखता है और प्रदर्शन की समस्या दूर करता है। नीचे दिया फ़ंक्शन अंतिम non-zero तत्व का इंडेक्स लौटाता है; अगर कुछ नहीं मिले तो -1 देता है।

from numba import njit
import numpy as np
@njit
def idx_last_active(a):
    for p in range(len(a) - 1, -1, -1):
        if a[p] != 0:
            return p
    return -1

उपयोग:

arr = np.array([0, 1, 1, 0, 0, 1, 0, 0])
pos = idx_last_active(arr)
print(pos)

यह क्यों मायने रखता है

जब यही रूटीन बड़े ऐरे पर लाखों बार चलता है, तो “काफी तेज” और “वास्तव में तेज” के बीच का फ़र्क़ निर्णायक हो जाता है। प्रतिकूल डेटा-वितरण में साधारण लूप अप्रत्याशित रूप से धीमा पड़ सकता है, और NumPy-केंद्रित तरकीबें भी ज़रूरी नहीं कि मदद करें। इस पैटर्न में Numba JIT एक भरोसेमंद रास्ता देता है और NumPy-आधारित तरीकों पर ठोस स्पीडअप प्रदान करता है।

मुख्य निष्कर्ष

1D NumPy ऐरे में आख़िरी non-zero इंडेक्स पाने के लिए, साधारण Python लूप एक अच्छा शुरुआती बिंदु है, पर non-zero तत्व के प्रतिकूल स्थान पर यह धीमा पड़ सकता है। एक छोटा बदलाव—उल्टे व्यू पर आगे की ओर इटरेट करना—रेंज को उलटने की तुलना में स्पष्ट रूप से तेज हो सकता है। जब परफॉर्मेंस सच में अहम हो, तो Numba के साथ उसी लूप को JIT करें: यह NumPy-आधारित रणनीतियों से काफ़ी तेज है और पढ़ने-सम्भालने में आसान भी। अगर “नहीं मिला” का संकेत चाहिए, तो JIT वाले संस्करण की तरह -1 लौटाएँ; अन्यथा, शुद्ध Python विकल्पों में सामान्य Python सेमान्टिक्स के अनुरूप None लौटाएँ।