भविष्य को आकार देने वाली तकनीक पर गहन लेख।

Rob Pike के प्रोग्रामिंग नियम आज भी सही हैं

Rob Pike ने 1989 में प्रोग्रामिंग के पाँच नियम लिखे थे। आज ये पहले से ज़्यादा प्रासंगिक हैं, खासकर वे नियम जिन्हें हम लगातार अनदेखा करते रहते हैं।

एक अव्यवस्थित आधुनिक डेस्क के बगल में, एक लैंप की रोशनी में लगा पुराना इंडेक्स कार्ड

1989 में Rob Pike, जो बाद में Go, UTF-8 और Plan 9 के सह-निर्माता बने, ने प्रोग्रामिंग के पाँच नियम लिखे। ये इतने छोटे हैं कि एक इंडेक्स कार्ड पर आ जाएँ, और इतने गहरे हैं कि प्रोग्रामिंग की दुनिया 37 साल से इन पर बहस कर रही है। ज़्यादातर डेवलपर्स ने इनमें से कुछ न कुछ ब्लॉग पोस्ट या कॉन्फ़्रेंस टॉक में ज़रूर देखा होगा। लेकिन इन्हें सच में अपनाने वाले कम हैं, और यह अफ़सोस की बात है, क्योंकि ये बहुत सारी बेकार की मेहनत बचा सकते हैं।

ये नियम देखने में सीधे-सादे हैं। ये सिंटैक्स या आर्किटेक्चर पैटर्न के बारे में नहीं हैं। ये इस बारे में हैं कि प्रोग्रामर लगातार कहाँ समय बर्बाद करते हैं और उसे कैसे रोका जाए।

पाँच नियम

विश्लेषण करने से पहले, उन्हें सीधे-सीधे बता दूँ:

  1. आप यह नहीं बता सकते कि प्रोग्राम अपना समय कहाँ खर्च करेगा। बॉटलनेक अक्सर अप्रत्याशित जगहों पर होते हैं, इसलिए जब तक यह साबित न हो जाए कि बॉटलनेक कहाँ है, तब तक अंदाज़े से स्पीड हैक मत डालिए।
  2. मापिए। जब तक माप न लें, स्पीड के लिए ट्यून मत कीजिए, और तब भी तभी कीजिए जब कोड का कोई एक हिस्सा बाकी सब पर भारी पड़ रहा हो।
  3. छोटे n के लिए फैंसी एल्गोरिदम धीमे होते हैं, और n आमतौर पर छोटा ही होता है। फैंसी एल्गोरिदम के constant बड़े होते हैं। जब तक आपको पता न हो कि n अक्सर बड़ा होने वाला है, तब तक फैंसी मत बनिए।
  4. फैंसी एल्गोरिदम सादे एल्गोरिदम से ज़्यादा बग वाले होते हैं, और उन्हें लागू करना कहीं मुश्किल होता है। सादे एल्गोरिदम और सादे डेटा स्ट्रक्चर इस्तेमाल कीजिए।
  5. डेटा ही हावी होता है। अगर आपने सही डेटा स्ट्रक्चर चुने हैं और चीज़ों को अच्छी तरह व्यवस्थित किया है, तो एल्गोरिदम लगभग अपने-आप स्पष्ट हो जाएँगे। प्रोग्रामिंग के केंद्र में डेटा स्ट्रक्चर हैं, एल्गोरिदम नहीं।

नियम 1 और 2 ऑप्टिमाइज़ेशन के बारे में हैं। नियम 3 और 4 जटिलता के बारे में हैं। नियम 5 डिज़ाइन के बारे में है। साथ मिलकर ये एक ऐसा दर्शन बनाते हैं जो मूल रूप से विनम्रता के बारे में है: यह मानना कि परफ़ॉर्मेंस को लेकर हमारे अंदाज़े गलत होते हैं, जटिलता की कीमत हम कम आँकते हैं, और चतुर कोड से ज़्यादा मायने अच्छे डेटा स्ट्रक्चर रखते हैं।

नियम 1: आपको पता नहीं कि बॉटलनेक कहाँ है

यही वह नियम है जिसे डेवलपर्स सबसे आत्मविश्वास से तोड़ते हैं। ‘मुझे पता है यह फ़ंक्शन धीमा है क्योंकि इसमें नेस्टेड लूप है।’ ‘मुझे यहाँ hash map इस्तेमाल करना चाहिए क्योंकि lookup O(1) है।’ ‘मैं यह array पहले से allocate कर दूँगा क्योंकि allocation महँगा होता है।’ ये सुनने में समझदारी भरे लगते हैं। अक्सर ये गलत होते हैं।

मैंने काफ़ी production systems को profile किया है, और मेरे पास ऐसे उदाहरणों का संग्रह है जहाँ अंदाज़े से लगा बॉटलनेक असली बॉटलनेक नहीं निकला। एक सिस्टम में सबको लगता था कि database बॉटलनेक है, लेकिन profiling से पता चला कि JSON serialization request time का 60% खा रहा था। एक data pipeline में ‘महँगा’ matrix multiplication रनटाइम का सिर्फ 5% था, जबकि CSV parsing 70% ले रहा था। एक web application में टीम महीनों तक database queries को ऑप्टिमाइज़ करती रही, जबकि असली बॉटलनेक हर outbound HTTP request पर DNS resolution था।

इंसानी दिमाग एक बुरा profiler है। हम उन ऑपरेशनों को ज़्यादा वज़न देते हैं जो कॉन्सेप्चुअली महँगे लगते हैं (database queries, network calls), और उन ऑपरेशनों को कम आँकते हैं जो सस्ते लगते हैं (string concatenation, JSON parsing, memory allocation)। आधुनिक हार्डवेयर इसे और मुश्किल बना देता है। CPU caches, branch prediction और out-of-order execution का मतलब है कि कोड की जटिलता और execution time के बीच का रिश्ता बहुत अंतर्ज्ञान-विरोधी होता है।

नियम 2: पहले मापिए, फिर ऑप्टिमाइज़ कीजिए

यह नियम 1 का व्यावहारिक नतीजा है। अंदाज़े के आधार पर ऑप्टिमाइज़ मत कीजिए। Profile कीजिए। असली hotspot ढूँढिए। फिर सिर्फ़ उसी को ऑप्टिमाइज़ कीजिए।

इस नियम का दूसरा हिस्सा, ‘जब तक कोड का एक हिस्सा बाकी सब पर भारी न पड़े, तब तक मत कीजिए’, उतना ही ज़रूरी है, लेकिन इसे कम उद्धृत किया जाता है। अगर आपका profiler दिखाता है कि रनटाइम 20 फ़ंक्शनों में बराबर बँटा है, और हर एक 5% ले रहा है, तो कोई एक बॉटलनेक नहीं है जिसे ठीक किया जा सके। किसी एक फ़ंक्शन को 2 गुना तेज़ करने से कुल रनटाइम में सिर्फ़ 2.5% की बचत होगी। यह शायद ही कभी अतिरिक्त जटिलता के लायक होता है। आपको व्यक्तिगत फ़ंक्शनों को ऑप्टिमाइज़ करने के बजाय एक बुनियादी रूप से अलग तरीका खोजना होगा।

# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.

नियम 3: छोटे N के लिए फैंसी एल्गोरिदम धीमे होते हैं

यह वह नियम है जिसमें कंप्यूटर साइंस की पढ़ाई उल्टी दिशा में चलती है। हमें पढ़ाया जाता है कि O(n log n), O(n²) से बेहतर है, और asymptotically यह सच है। लेकिन n = 20 के लिए अच्छी तरह लागू किया गया O(n²) insertion sort, constant factors, cache behavior और overhead की वजह से O(n log n) merge sort से तेज़ चलता है।

असली दुनिया में उदाहरणों की कमी नहीं है। 50 elements वाले sorted array में linear search, binary search से तेज़ होता है, क्योंकि linear search का cache behavior परफ़ेक्ट होता है और branch mispredictions नहीं होते। लगभग 100 से कम elements वाले संग्रह के लिए एक सादी linked list, balanced binary tree को हरा देती है, क्योंकि tree में pointers का पीछा करना cache locality को बर्बाद कर देता है। Hash maps का amortized lookup O(1) है, लेकिन उसका constant इतना ऊँचा है कि लगभग 30-50 से कम elements के लिए array में linear search तेज़ निकलता है।

Standard libraries इसे जानती हैं। Python का sorted() Timsort इस्तेमाल करता है, जो छोटे subsequences के लिए insertion sort पर वापस चला जाता है। C++ का std::sort एक threshold (आमतौर पर 16-32 elements) से नीचे insertion sort पर switch कर जाता है। Rust का sort_unstable quicksort और insertion sort का मिश्रण इस्तेमाल करता है। ‘फैंसी’ एल्गोरिदम सिर्फ़ वहीं इस्तेमाल होता है जहाँ n सच में इतना बड़ा हो कि वह जीत सके।

बड़ा सबक यह है: अपने n को जानिए। अगर आप एक सादे O(n²) एल्गोरिदम और एक जटिल O(n log n) एल्गोरिदम के बीच चुन रहे हैं, तो खुद से पूछिए कि व्यवहार में n कितना बड़ा होगा। अगर वह कुछ सौ से कम है, तो सादा एल्गोरिदम लगभग निश्चित रूप से ठीक है, और उसे लिखना, डीबग करना और मेंटेन करना आसान होगा।

नियम 4: सादा होना चतुराई से बेहतर है

नियम 4 नियम 3 को परफ़ॉर्मेंस से आगे ले जाता है। फैंसी एल्गोरिदम सिर्फ़ छोटे n के लिए धीमे नहीं होते, वे ज़्यादा बग वाले भी होते हैं। एक red-black tree में sorted array से ज़्यादा edge cases होते हैं। एक lock-free concurrent data structure में mutex से सुरक्षित वाले से ज़्यादा सूक्ष्म failure modes होते हैं। एक custom memory allocator के पास system allocator से ज़्यादा तरीके होते हैं memory को करप्ट करने के।

मैंने टीमों को O(1) ऑपरेशनों वाला custom LRU cache लागू करने और डीबग करने में हफ़्ते बिताते देखा है, जबकि एक सादा bounded array, जिसमें linear eviction हो, एक दोपहर में लिखा जा सकता था, पहली ही बार में सही होता, और उनके workload के लिए काफ़ी तेज़ होता (जिसमें ज़्यादा से ज़्यादा कुछ सौ cache entries थीं)।

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

डीबग करना कोड लिखने से दोगुना कठिन है। इसलिए अगर आप कोड को जितना हो सके उतना चतुराई से लिखते हैं, तो आप परिभाषा के अनुसार उसे डीबग करने लायक पर्याप्त स्मार्ट नहीं हैं। — Brian Kernighan

नियम 5: डेटा हावी होता है

यह Pike का सबसे महत्वपूर्ण नियम है, और यह वह नियम है जिसे एल्गोरिदम और डिज़ाइन पैटर्न पर ध्यान देने वाले डेवलपर्स सबसे ज़्यादा नज़रअंदाज़ करते हैं। दावा यह है: अगर आपके डेटा स्ट्रक्चर सही हैं, तो एल्गोरिदम स्वाभाविक रूप से उनके पीछे-पीछे आ जाते हैं। अगर डेटा स्ट्रक्चर गलत हैं, तो एल्गोरिदमिक चतुराई कितनी भी हो, आपको नहीं बचा पाएगी।

Fred Brooks ने भी कुछ ऐसा ही कहा था: ‘मुझे अपने flowcharts दिखाओ और tables छिपा लो, और मैं हैरान ही रहूँगा। मुझे अपने tables दिखाओ, तो आपके flowcharts की आमतौर पर ज़रूरत नहीं पड़ेगी।’ Linus Torvalds ने इसे दोहराया: ‘बुरे प्रोग्रामर कोड की चिंता करते हैं। अच्छे प्रोग्रामर डेटा स्ट्रक्चर और उनके रिश्तों की चिंता करते हैं।’

यह सिद्धांत व्यवहार में लगातार दिखता है। एक codebase जो user permissions को permission strings की एक flat list में रखता है, पूरे कोड में बिखरा हुआ, जटिल और गलतियों वाला checking logic जमा करता जाएगा। डेटा को role hierarchy के रूप में दोबारा संरचित कीजिए, और checking logic तुच्छ हो जाता है। जो सिस्टम events को JSON blobs में रखता है, उसे हर consumer पर जटिल parsing और validation करना पड़ेगा। Events को explicit schemas वाले typed records के रूप में संरचित कीजिए, और consumers नाटकीय रूप से सरल हो जाते हैं।

# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = []  # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id]  # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending']  # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending']  # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {}      # user_id → [orders]
self.orders_by_status = {}    # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, [])  # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', [])  # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.

Go इन नियमों को कैसे अपनाता है

Pike के नियमों को देखते हुए Go का डिज़ाइन दर्शन भ्रूण रूप में दिखे बिना नहीं रहता। Go, जिसे Pike ने इन नियमों के लिखे जाने के 20 साल बाद सह-निर्मित किया, एक ऐसी भाषा है जो व्यवस्थित रूप से चतुराई के बजाय सादगी को तरजीह देती है।

  • शुरुआत में कोई generics नहीं, जो सादे डेटा स्ट्रक्चर के लिए मजबूर करता है। (Generics Go 1.18 में जोड़े गए, लेकिन तभी जब बरसों के विरोध के बाद पर्याप्त सादा डिज़ाइन खोजा गया।)
  • कोई operator overloading नहीं। कोड का वही मतलब है जो दिखता है।
  • कोई implicit type conversion नहीं। चतुराई के मुकाबले स्पष्टता।
  • कोई exceptions नहीं। त्रुटियों को जहाँ होती हैं वहीं संभालिए।
  • कम से कम standard library algorithms। slices और maps इस्तेमाल कीजिए, फैंसी data structures नहीं।
  • बिल्ट-इन profiling (pprof)। मापिए, अंदाज़ा मत लगाइए।

Go को उन डेवलपर्स द्वारा अक्सर ‘बोरिंग’ कहकर आलोचना मिलती है जो ज़्यादा expressive भाषाएँ पसंद करते हैं। यही तो बात है। Pike के नियम बोरिंग कोड का नुस्खा हैं: ऐसा कोड जो सरल हो, मापा जा सके, और चतुर एल्गोरिदम के बजाय अच्छे डेटा स्ट्रक्चर पर बना हो। Go वह है जो तब होता है जब आप उस नुस्खे को एक भाषा में बदल देते हैं।

जहाँ नियम लागू नहीं होते

कोई भी नियम सार्वभौमिक नहीं होता, और Pike के नियमों के भी वैध अपवाद हैं। परफ़ॉर्मेंस-क्रिटिकल सिस्टम (game engines, database internals, compilers) को कभी-कभी फैंसी एल्गोरिदम चाहिए, क्योंकि उनका n सच में बड़ा होता है। ऐसा infrastructure कोड जो प्रति सेकंड लाखों बार चलता है, उस ऑप्टिमाइज़ेशन को जायज़ ठहराता है जो application कोड के लिए ज़रूरी नहीं होता। और कभी-कभी ‘सादा’ एल्गोरिदम O(n³) जटिलता वाला होता है, जो मामूली n के लिए भी सच में अस्वीकार्य है।

ये नियम कानून नहीं, heuristics हैं। इनकी कीमत इस आम पूर्वाग्रह को सुधारने में है: डेवलपर्स जल्दी ऑप्टिमाइज़ करते हैं, बहुत जटिल एल्गोरिदम चुनते हैं, और कोड के बारे में ज़्यादा सोचते हैं तथा डेटा स्ट्रक्चर के बारे में कम। Pike के नियम इन प्रवृत्तियों के विरुद्ध धक्का देते हैं। अगर आप उस दुर्लभ स्थिति में हैं जहाँ उलटा पूर्वाग्रह लागू होता है, यानी जहाँ आपको सच में कम नहीं, ज़्यादा जटिलता चाहिए, तो ज़रूर, फैंसी बनिए। लेकिन पहले मापिए।

Pike के इन्हें लिखे जाने के सैंतीस साल बाद भी ये नियम प्रोग्रामिंग की सबसे अच्छी सलाहों में से कुछ बने हुए हैं। इसलिए नहीं कि ये चौंकाने वाले हैं। ज़्यादातर अनुभवी डेवलपर्स इन्हें पढ़कर सोचते हैं ‘हाँ, बिल्कुल।’ इनकी कीमत इस बात में है कि ये इतने साफ़ तरीके से लिखे गए हैं कि लगातार लागू किए जा सकें। अगली बार जब आप red-black tree, custom allocator, या ऐसा ‘optimization’ उठाएँ जिसे आपने profile नहीं किया, तो याद रखिए: पहले मापिए, सरल रखिए, और डेटा स्ट्रक्चर सही चुनिए।