400 गुना प्रदर्शन अंतर खत्म करने वाला 40-लाइन का सुधार
(questdb.com)- OpenJDK में
ThreadMXBean.getCurrentThreadUserTime()को/procफ़ाइल parsing के बजायclock_gettime()कॉल से बदला गया, जिससे अधिकतम 400 गुना प्रदर्शन सुधार हासिल हुआ - पुराना implementation
/proc/self/task/<tid>/statफ़ाइल को खोलने, पढ़ने और parse करने वाले जटिल I/O path से गुजरता था - नया implementation Linux kernel के
clockid_tbit encoding का उपयोग करता है औरpthread_getcpuclockid()से मिले ID के निचले bits को समायोजित करके सिर्फ user time को सीधे पढ़ता है - benchmark नतीजों में औसत call time 11μs → 279ns तक घटा, और बाद में kernel fast-path लागू करने पर लगभग 13% अतिरिक्त सुधार मिला
- यह दिखाने वाला एक उदाहरण कि POSIX की सीमाओं से आगे बढ़कर Linux internal ABI की समझ के जरिए optimization संभव है
पुराने implementation की समस्या
getCurrentThreadUserTime()/proc/self/task/<tid>/statफ़ाइल खोलकर 13वें और 14वें field को parse करता था और CPU user time की गणना करता था- फ़ाइल path बनाना, फ़ाइल खोलना, buffer पढ़ना, string parse करना,
sscanf()कॉल करना जैसी बहु-स्तरीय प्रक्रिया की ज़रूरत थी - command name में parentheses शामिल हो सकते थे, इसलिए आख़िरी
)खोजने के लिएstrrchr()का उपयोग करने वाला जटिल logic भी था
- फ़ाइल path बनाना, फ़ाइल खोलना, buffer पढ़ना, string parse करना,
- इसके विपरीत
getCurrentThreadCpuTime()केवल एकclock_gettime(CLOCK_THREAD_CPUTIME_ID)कॉल करता था - 2018 के bug report (JDK-8210452) के अनुसार, दोनों methods के बीच speed gap 30~400 गुना तक पहुँचता था
/proc access path और clock_gettime() path की तुलना
/procतरीकाopen(),read(),sscanf(),close()जैसी कई system calls और kernel के भीतर string generation को शामिल करता हैclock_gettime()तरीका एक single system call सेsched_entitystructure से सीधे time value पढ़ता है- parallel load में
/procaccess kernel lock contention की वजह से और धीमा हो जाता है
नया implementation तरीका
- POSIX standard के अनुसार
CLOCK_THREAD_CPUTIME_IDuser+system time लौटाता है - Linux kernel
clockid_tके निचले bits में clock kind को encode करता है00=PROF,01=VIRT(user-only),10=SCHED(user+system)
pthread_getcpuclockid()से मिलेclockidके निचले bits को01में बदलने पर इसे user-time-only clock में बदला जा सकता है- नए code में फ़ाइल I/O और parsing हटाकर, केवल
clock_gettime()कॉल से user time लौटाया जाता है
प्रदर्शन मापन के नतीजे
- बदलाव से पहले औसत call time 11.186μs, बदलाव के बाद 0.279μs रहा, यानी लगभग 40 गुना सुधार
- 16-thread environment में मापा गया, जो पहले रिपोर्ट की गई 30~400 गुना range से मेल खाता है
- CPU profile में फ़ाइल open/close से जुड़े system calls गायब हो गए, और केवल एक
clock_gettime()कॉल बची
kernel fast-path के साथ अतिरिक्त optimization
- kernel,
clockidमें PID=0 encode होने पर current thread तक सीधे पहुँचने वाला fast-path देता है - अगर JVM
pthread_getcpuclockid()की जगह सीधेclockidबनाकर PID=0 डाले, तो radix tree lookup को छोड़ा जा सकता है - manually बनाए गए
clockidके उपयोग पर औसत समय 81.7ns → 70.8ns, यानी लगभग 13% अतिरिक्त सुधार - लेकिन यह
clockid_tके size जैसी kernel internal implementation पर निर्भर करता है, इसलिए readability और compatibility में कमी की आशंका है
निष्कर्ष और सीख
- 40 लाइनों को हटाकर 400 गुना प्रदर्शन अंतर खत्म किया गया, और यह बिना किसी नए kernel feature के, सिर्फ मौजूदा ABI की बारीक संरचना के उपयोग से संभव हुआ
- kernel source code को गहराई से पढ़ने के मूल्य पर ज़ोर: POSIX portability की गारंटी देता है, लेकिन kernel code संभावनाओं की सीमा दिखाता है
- पुरानी धारणाओं की फिर से समीक्षा का महत्व:
/procparsing पहले उचित रहा होगा, लेकिन अब यह अप्रभावी है - यह बदलाव JDK 26 (मार्च 2026 में रिलीज़ होने की योजना) में शामिल होगा, और
ThreadMXBean.getCurrentThreadUserTime()कॉल पर स्वचालित प्रदर्शन सुधार देगा
5 टिप्पणियां
असल में पूरे कोडबेस को फिर से लिखकर भी 2~3 गुना सुधार हासिल करना मुश्किल होता है, ऐसे में सिर्फ कुछ लाइनें बदलकर अधिकतम 400 गुना सुधार मिलना वाकई कमाल की बात है।
Hacker News की राय
मुझे पता चला कि “इस thread का CPU usage time कितना है?” जैसा सवाल उम्मीद से काफी महंगा operation है
atomic clock जैसे मानक के बिना absolute numbers पर दावा करना मुश्किल लगता है
clock_gettime()vDSO के ज़रिए context switch से बचता है। इसलिए flamegraph में भी उसका निशान दिखता हैCLOCK_VIRTयाCLOCK_SCHEDजैसे मामलों में अब भी syscall call की ज़रूरत होती हैCLOCK_THREAD_CPUTIME_IDआखिरकार kernel में जाता है, क्योंकि उसे task struct को reference करना पड़ता हैसंबंधित kernel source: posix-cpu-timers.c,
cputime.c,
gettimeofday.c
PERF_COUNT_SW_TASK_CLOCKका इस्तेमाल करने पर लगभग 8ns स्तर की measurement भी संभव हैयह
perf_event_mmap_pageके जरिए shared page से read करके,rdtsccall से delta निकालने का तरीका हैइसकी documentation अच्छी नहीं है और open source implementation भी लगभग नहीं के बराबर हैं
perf_eventsetup और permission requirements बड़े हैं, इसलिए यह long-lived thread के लिए ज़्यादा उपयुक्त लगता हैseqlockकी ज़रूरत क्यों पड़ती है। क्या इसका मकसद page value औरrdtscके बीच context switch न होने देना है?शायद structure ऐसा है कि
rdtscके बाद page value फिर से check की जाती है, और बदलने पर retry किया जाता हैवैसे
clock_gettimeभी vdso-आधारित virtual syscall हैclock_gettimesyscall नहीं, vdso का इस्तेमाल करता हैसिर्फ code देखकर सब ठीक लगता है, लेकिन flamegraph देखने पर अक्सर लगता है, “ये क्या है?!”
non-static initialization, एक लाइन के logger call से महंगी serialization होना, जैसी कई समस्याएँ मिलीं
मैं आम तौर पर async-profiler का HTML generator इस्तेमाल करता हूँ, लेकिन इस बार single SVG के लिए Brendan का tool इस्तेमाल किया
/procपढ़ते समय होने वाले memory overhead, eBPF profiling, और कम-documented user-space ABI के इतिहास पर लिखा हैविवरण मेरे blog post में है
source tweet
Jaromir का blog भी वाकई शानदार था
ऐसी चीज़ें प्रोजेक्ट में कैसे खोजी जा सकती हैं? सिर्फ AI चलाने से इसका पता चलना मुश्किल लगता है..
ऐसे उदाहरणों को देखकर मुझे भी लगता है कि सीखकर इसे ज़रूर खुद अनुभव करना चाहिए।
वाकई कमाल है।
मुझे नहीं लगता कि यह पूरी तरह गलत बात है, लेकिन जब मामला kernel से जुड़ा हो, तो मुझे लगता है कि सिर्फ यह पहचान पाना ही कि चीज़ें धीमी हैं, अपने-आप में बेहद मुश्किल रहा होगा।