2025, Oct 31 11:03

Polars में Copy-on-Write: DataFrame/Series मेमोरी मॉडल समझें

Polars के Copy-on-Write को सरल समझें: DataFrame/Series में कॉलम-स्तरीय क्लोन बनाम साझा मेमोरी, with_columns का असर, n_chunks से जाँच, परफॉर्मेंस टिप्स.

Polars कॉपी-ऑन-राइट सेमांटिक्स पेश करता है जो सिस्टम डेवलपर्स को परिचित लगते हैं, लेकिन जब आप सूक्ष्म स्तर पर इन-प्लेस अपडेट की उम्मीद करते हैं तो वे उलझा भी सकते हैं। बात यह नहीं कि ऑब्जेक्ट स्वतंत्र दिखते हैं या नहीं, बल्कि यह कि मेमोरी वास्तव में कब क्लोन होती है और बफ़र कब दोबारा इस्तेमाल होते हैं। नीचे एक हैंड्स-ऑन मार्गदर्शिका है जो यह साफ़ करती है कि ब्रांच और ट्रांसफ़ॉर्म करते समय DataFrame और उनके कॉलम कैसे बर्ताव करते हैं।

परिदृश्य को पुनःनिर्मित करना

उदाहरण एक सरल DataFrame से शुरू होता है, फिर व्यू बनाता है और एक कॉलम को बदलता है। हर चरण में नए वेरिएबल नाम लिए जाते हैं ताकि साफ़ रहे कि नया ऑब्जेक्ट कब बनता है, और कॉपी-ऑन-राइट के तहत कौन-सी मेमोरी अब भी साझा रह सकती है।

import polars as pl
base_df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
view_df = base_df  # अर्थ की दृष्टि से इसे कॉपी मानें; अंदरूनी तौर पर यह उसी डेटा को संदर्भित करता है।
view_df = view_df.with_columns(
    pl.Series([7, 8, 9]).alias("b")
)  # कॉपी-ऑन-राइट केवल कॉलम b पर लागू होता है; a साझा ही रहता है।
final_df = view_df  # अर्थ के स्तर पर कॉपी; अंदर से अब भी view_df के बफ़र्स को संदर्भित कर रहा है।
final_df = final_df.with_row_index("rid")  # शर्तीय संपादन के लिए एक अस्थायी रो इंडेक्स जोड़ें।
final_df = final_df.with_columns(
    pl.when(pl.col("rid") == 0)
    .then(10)
    .otherwise(pl.col("b"))
    .alias("b")
)  # b के लिए नया Series बनता है, पहली वैल्यू 10 कर दी जाती है।
final_df = final_df.with_columns(
    pl.when(pl.col("rid") == 1)
    .then(11)
    .otherwise(pl.col("b"))
    .alias("b")
)  # b के लिए एक और नया Series बनता है, दूसरी वैल्यू 11 सेट होती है।
final_df = final_df.drop("rid")  # सहायक इंडेक्स कॉलम हटाएँ.

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

मूल मॉडल यह है कि Polars कॉपी-ऑन-राइट का उपयोग करता है। जब तक किसी कॉलम को बदला नहीं गया है, असाइनमेंट या ट्रांसफ़ॉर्मेशन से बने अलग-अलग DataFrame उसी मेमोरी चंक को संदर्भित करते रह सकते हैं। इस क्रम में कॉलम a कभी बदला ही नहीं, इसलिए base_df, view_df और final_df a के लिए वही चंक साझा करते हैं। यही अनुकूलन का सार है: जिस डेटा को छुआ नहीं गया, उसके लिए अनावश्यक कॉपी नहीं होती।

कॉलम एटॉमिक इकाइयों की तरह व्यवहार करते हैं। किसी कॉलम में कोई भी बदलाव एक नया Series बनाता है और इस तरह एक नया चंक; और जिस DataFrame को आप वह असाइन करते हैं, वह एक नया ऑब्जेक्ट बन जाता है जो अब उस नए चंक को संदर्भित करता है। इसका मतलब यह भी है कि with_columns हमेशा नया DataFrame लौटाता है; भले सिर्फ एक तत्व बदले, बदला हुआ Series नई मेमोरी होता है। यानी अपडेट इन-प्लेस नहीं होते।

एक आम गलतफ़हमी दूर करना

अक्सर लोग यह पढ़ लेते हैं कि “अभी केवल एक कॉलम a मौजूद है।” अगर आपका आशय यह है कि इन सभी DataFrame में एक ही कॉलम चंक साझा हो रहा है, तो यह कथन इस उदाहरण में Polars के व्यवहार से मेल खाता है। a के लिए अब भी एक साझा चंक है, और हर DataFrame उसी की ओर इशारा करता है। फ़र्क इसलिए मायने रखता है कि DataFrame अर्थ की दृष्टि से स्वतंत्र हैं, फिर भी वे तब तक समान बफ़र पुन: उपयोग कर सकते हैं जब तक कोई म्यूटेशन उन्हें छू नहीं लेता।

यहाँ कॉपी-ऑन-राइट को कैसे समझें

पहला, ध्यान दें कि शुरुआती DataFrame बनाने के बाद आपने a को छुआ ही नहीं। इसलिए उसकी मेमोरी अब भी साझा है। दूसरा, जब भी आप नया b असाइन करते हैं—चाहे किसी नए Series से उसे बदलकर या किसी शर्तीय अभिव्यक्ति से बनाकर—b के लिए एक नया चंक तैयार होता है। पिछला b उन सभी DataFrame के लिए जस का तस रहता है जो उसे संदर्भित कर रहे थे। Polars स्वामित्व को चंक स्तर पर ट्रैक करता है; यदि कोई अभिव्यक्ति मूल बफ़र को प्रभावित करती, तो चंक क्लोन होता, अन्यथा वही पुन: उपयोग होता है।

इसे और ठोस बनाने के लिए, इसी वर्कफ़्लो को बहुत बड़े इंटीजर या फ़्लोट कॉलम के साथ आज़माएँ। कुछ ही पंक्तियों में मेमोरी उपयोग का फ़र्क नगण्य रहता है। लेकिन लगभग 100M पंक्तियों और 64-बिट मानों पर, आप साफ़ देखेंगे कि करीब ~800MB कब कॉपी होता है और कब नहीं—यह इस पर निर्भर करेगा कि कोई कॉलम क्लोन हुआ या साझा रहा। यह देखने के लिए कि इस समय प्रत्येक कॉलम में कितने चंक हैं, DataFrame.n_chunks सुविधा का उपयोग करें; इससे पता चलता है कि क्या पुन: उपयोग हो रहा है और क्या क्लोन किया गया है।

प्रक्रिया की सुधरी हुई तस्वीर

उदाहरण के जीवनचक्र का सार एक सुसंगत मानसिक मॉडल देता है: DataFrame असाइनमेंट एक नया हैंडल बनाता है जो मूल बफ़र को ही संदर्भित करता है; b में बदलाव नया Series और नया DataFrame बनाते हैं; b पर आगे की शर्तीय अपडेट नए Series बनाती रहती हैं; a साझा ही रहता है क्योंकि उसे कभी बदला नहीं गया। स्वतंत्रता DataFrame स्तर पर अर्थगत है, जबकि वास्तविक मेमोरी का पुन: उपयोग कॉलम-चंक स्तर पर तब तक चलता है जब तक किसी म्यूटेशन को क्लोनिंग की ज़रूरत न पड़े।

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

इस मॉडल को समझने से आप प्रदर्शन और मेमोरी व्यवहार के बारे में अनुमान लगाए बिना तर्क कर पाते हैं। यह बताता है कि कुछ ऑपरेशन तेज़ क्यों हैं—यदि आप किसी कॉलम को नहीं छूते तो कोई डेटा कॉपी नहीं होता—और चुनिंदा अपडेट कुशल क्यों हैं: केवल प्रभावित कॉलम क्लोन होते हैं। यह यह भी स्पष्ट करता है कि एकल-तत्व, इन-प्लेस म्यूटेशन की उम्मीद क्यों नहीं करनी चाहिए; हर ट्रांसफ़ॉर्मेशन का परिणाम एक नया DataFrame होता है, और जहाँ ज़रूरत पड़ती है वहाँ नए Series बनते हैं—जो इम्यूटेबिलिटी और कॉपी-ऑन-राइट के अनुरूप है।

व्यावहारिक सलाह

DataFrame और Series को अपरिवर्तनीय मानें, और कॉलम-स्तरीय चंकों के संदर्भ में सोचें। with_columns से नए DataFrame की अपेक्षा रखें और बदले हुए कॉलमों को नए Series समझें। यदि कोई कॉलम क्लोनों में बिना छुए रहता है, तो वह साझा ही रहता है। संदेह हो तो प्रति कॉलम चंकों की संख्या देखें और बड़े पैमाने के परीक्षण चलाकर मेमोरी के पुन: उपयोग बनाम क्लोनिंग को स्पष्ट रूप से परखें।

यह लेख StackOverflow के प्रश्न user2961927 द्वारा और Aren के उत्तर पर आधारित है।