ओवरहेड के लिए कुछ अजगर ORM का विश्लेषणपरिचय
अजगर django में एक एप्लिकेशन विकसित करते समय, मैं इसकी अपर्याप्त ब्रेकिंग में आ गया।
काफी जटिल गणना एल्गोरिदम में सुधार करने के कई प्रयासों के बाद, मैंने देखा कि इन एल्गोरिदम में महत्वपूर्ण सुधारों के परिणामस्वरूप बहुत मामूली परिणाम आए - जिससे मैंने निष्कर्ष निकाला कि टोंटी एल्गोरिदम में बिल्कुल भी नहीं थी।
बाद के विश्लेषण से पता चला कि वास्तव में, django ORM, जिसका उपयोग गणनाओं के लिए आवश्यक डेटा तक पहुंचने के लिए किया गया था, प्रोसेसर संसाधनों का मुख्य अनुत्पादक उपभोक्ता निकला।
इस सवाल में दिलचस्पी होने के बाद, मैंने यह जांचने का फैसला किया कि ORM का उपयोग करते समय अनुत्पादक व्यय क्या हैं। परिणाम प्राप्त करने के लिए, मैंने सबसे बुनियादी ऑपरेशन का उपयोग किया: नए बनाए गए नए डेटाबेस में पहले और एकमात्र उपयोगकर्ता का उपयोगकर्ता नाम प्राप्त करना।
डेटाबेस के रूप में, हमने लोकलहोस्ट (MyISAM टेबल) पर स्थित MySQL का उपयोग किया।
एक प्रारंभिक "रोल मॉडल" के रूप में, मैंने उस कोड का उपयोग किया जो न्यूनतम रूप से django विनिर्देश का उपयोग करता था और लगभग अपेक्षित रूप से आवश्यक मूल्य प्राप्त करता था:
def test_native(): from django.db import connection, transaction cursor = connection.cursor() t1 = datetime.datetime.now() for i in range(10000): cursor.execute("select username from auth_user limit 1") f = cursor.fetchone() u = f[0][0] t2 = datetime.datetime.now() print "native req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. t1 = datetime.datetime.now() for i in range(10000): cursor.execute("select username,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined from auth_user limit 1") f = cursor.fetchone() u = f[0][0] t2 = datetime.datetime.now() print "native (1) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. t1 = datetime.datetime.now() for i in range(10000): cursor = connection.cursor() cursor.execute("select username,first_name,last_name,email,password,is_staff,is_active,is_superuser,last_login,date_joined from auth_user limit 1") f = cursor.fetchone() u = f[0][0] t2 = datetime.datetime.now() print "native (2) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.
इस कोड को निष्पादित करने का परिणाम:
>>> test_native ()
देशी रीक / सेक्: 8873.05935101 रिक् टाइम (एमएस): 0.1127007
मूल (1) req / seq: 5655.73751948 req समय (ms): 0.1768116
मूल (2) req / seq: 3815.78751558 req समय (ms): 0.2620691
इस प्रकार, इष्टतम "नमूना" डेटाबेस को प्रति सेकंड लगभग 8 और डेढ़ हजार हिट देता है।
आमतौर पर, django और अन्य ORM को डेटाबेस से प्राप्त होने पर ऑब्जेक्ट के अन्य गुण प्राप्त होते हैं। जैसा कि आप आसानी से देख सकते हैं, टेबल के बाकी हिस्सों से "लोकोमोटिव" प्राप्त करने से परिणाम बहुत खराब हो गया: प्रति सेकंड 5 से डेढ़ हजार अनुरोध। हालांकि, यह गिरावट सापेक्ष है, क्योंकि अक्सर गणना परिणाम प्राप्त करने के लिए एक से अधिक डेटा फ़ील्ड की आवश्यकता होती है।
एक नया कर्सर प्राप्त करने का संचालन मुश्किल से मुश्किल हो गया - इसमें लगभग 0.1ms लगते हैं और कोड निष्पादन की गति लगभग 1.5 गुना कम हो जाती है।
उदाहरण के लिए नमूना के लिए दूसरा परिणाम लें और देखें कि प्राप्त संकेतकों से ओआरएम क्या नुकसान उठाता है।
Django ऑरम
आइए कई क्वेरी विकल्पों को निष्पादित करें, सरलतम से शुरू करें, और प्रश्नों को अनुकूलित करने के लिए लगातार django टूल का उपयोग करने का प्रयास करें।
सबसे पहले, हम उपयोगकर्ता प्रकार के साथ शुरू करते हुए, वांछित विशेषता प्राप्त करने के लिए सबसे सरल कोड निष्पादित करेंगे।
फिर हम अनुरोध ऑब्जेक्ट को पहले सहेज कर परिणाम को बेहतर बनाने का प्रयास करेंगे।
फिर, एकमात्र () विधि के उपयोग को याद करें और परिणाम को और बेहतर बनाने का प्रयास करें।
और अंत में, स्थिति को सुधारने के प्रयास का एक और संस्करण मूल्यों () पद्धति का उपयोग करना है, जो लक्ष्य ऑब्जेक्ट बनाने की आवश्यकता को समाप्त करता है।
यहाँ अंतिम कोड है जो हमारे प्रयासों के परिणाम की जाँच करता है:
def test_django(): t1 = datetime.datetime.now() for i in range(10000): u = User.objects.all()[0].username t2 = datetime.datetime.now() print "django req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. t1 = datetime.datetime.now() q = User.objects.all() for i in range(10000): u = q[0].username t2 = datetime.datetime.now() print "django (1) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. t1 = datetime.datetime.now() q = User.objects.all().only('username') for i in range(10000): u = q[0].username t2 = datetime.datetime.now() print "django (2) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. t1 = datetime.datetime.now() q = User.objects.all().values('username') for i in range(10000): u = q[0]['username'] t2 = datetime.datetime.now() print "django (3) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.
निष्पादन परिणाम हतोत्साहित कर रहे हैं:
>>> test_django ()
django req / seq: 1106.3929598 req time (ms): 0.903838
django (1) req / seq: 1173.20686476 req समय (ms): 0.8523646
django (2) req / seq: 695.949871009 req समय (ms): 1.4368851
django (3) req / seq: 1383.74156246 req समय (ms): 0.7226783
सबसे पहले, अपने आप में ओआरएम का उपयोग 5 से अधिक बार (!) से अधिक गैर-इष्टतम "नमूना" की तुलना में प्रदर्शन खराब हो गया। चक्र के बाहर अनुरोध की तैयारी को आगे बढ़ाने से (10% से कम) परिणाम में सुधार नहीं हुआ। लेकिन केवल () के उपयोग ने तस्वीर को पूरी तरह से खराब कर दिया - हमें अपेक्षित सुधार के बजाय लगभग 2 गुना परिणाम में गिरावट दिखाई देती है। इसी समय, दिलचस्प रूप से, वस्तु के निर्माण के बहिष्करण ने उत्पादकता को 20% तक बढ़ाने में मदद की।
इस प्रकार, django ORM एक वस्तु प्राप्त करने के लिए लगभग 0.7226783-0.1768116 = 0.5458667ms द्वारा अनुत्पादक खर्चों में वृद्धि देता है।
अतिरिक्त प्रयोगों और तालिकाओं के निर्माण की आवश्यकता को देखते हुए, मैं आपको सूचित करता हूं कि ये परिणाम वस्तुओं की एक सूची प्राप्त करने के लिए भी सही हैं:
प्रत्येक व्यक्तिगत वस्तु को वस्तुओं के संग्रह
में पुनः प्राप्त करना, जो कि एक एकल क्वेरी का परिणाम है, जिसके परिणामस्वरूप प्रत्येक वस्तु पर
आधा मिलीसेकंड या उससे अधिक के
ऑर्डर का नुकसान होता है।
MySQL का उपयोग करने के मामले में, ये नुकसान
5 बार से अधिक कोड निष्पादन में मंदी की राशि है।
SQLAlchemy
SQLAlchemy के लिए, मैंने एक AUser वर्ग बनाया, जो मानक django.contrib.auth.models.User वर्ग के अनुरूप डेटा संरचना की घोषणा करता है।
अधिकतम प्रदर्शन प्राप्त करने के लिए, प्रलेखन और कुछ प्रयोगों को ध्यान से पढ़ने के बाद, एक सरल क्वेरी कैश का उपयोग किया गया था:
query_cache = {} engine = create_engine('mysql://testalchemy:testalchemy@127.0.0.1/testalchemy', execution_options={'compiled_cache':query_cache})
प्रदर्शन परीक्षण पहले ऑब्जेक्ट के उपयोग के "ललाट" संस्करण पर किया जाता है।
फिर लूप के बाहर अनुरोध की तैयारी करके अनुकूलन करने का प्रयास किया जाता है।
फिर लक्ष्य ऑब्जेक्ट के निर्माण को समाप्त करके अनुकूलन करने का प्रयास किया जाता है।
फिर हम अनुरोधित फ़ील्ड के सेट को सीमित करके क्वेरी को और अनुकूलित करते हैं।
def test_alchemy(): t1 = datetime.datetime.now() for i in range(10000): u = session.query(AUser)[0].username t2 = datetime.datetime.now() print "alchemy req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. q = session.query(AUser) t1 = datetime.datetime.now() for i in range(10000): u = q[0].username t2 = datetime.datetime.now() print "alchemy (2) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. from sqlalchemy.sql import select table = AUser.__table__ sel = select([table],limit=1) t1 = datetime.datetime.now() for i in range(10000): u = sel.execute().first()['username'] t2 = datetime.datetime.now() print "alchemy (3) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. table = AUser.__table__ sel = select(['username'],from_obj=table,limit=1) t1 = datetime.datetime.now() for i in range(10000): u = sel.execute().first()['username'] t2 = datetime.datetime.now() print "alchemy (4) req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.
यहाँ परीक्षा परिणाम हैं:
>>> test_alchemy ()
कीमिया रीक् / seq: 512.719730527 req time (ms): 1.9503833
कीमिया (2) req / seq: 526.34332554 req समय (ms): 1.8999006
कीमिया (3) req / seq: 1341.40897306 req समय (ms): 0.7454848
कीमिया (4) req / seq: 1995.34167532 req समय (एमएस): 0.5011673
पहले दो मामलों में, कीमिया ने अपनी पहचान के बावजूद कैश अनुरोध नहीं किया (मैंने पाया कि क्यों, लेकिन डेवलपर्स ने अब तक इसे किसी तरह के प्लग के साथ डूबने का सुझाव दिया है, जो तब वे कोड में छड़ी करने का वादा करते हैं, मैंने ऐसा नहीं किया)। कैश्ड क्वेरीज़ कीमिया को प्रदर्शन में 30% -35% तक django ORM से आगे निकलने की अनुमति देती हैं।
मैं तुरंत ध्यान देता हूं कि django ORM और SQLAlchemy द्वारा उत्पन्न SQL लगभग समान है और परीक्षण में विकृति का न्यूनतम परिचय देता है।
घुटने पर ORM
स्वाभाविक रूप से, इस तरह के परिणामों के बाद, हम अपने सभी कोड को रीडायरेक्ट करते हैं, जो प्रोसेसिंग एल्गोरिदम में डेटा को सीधे अनुरोधों तक पहुंचाते हैं। प्रत्यक्ष क्वेरी कोड के साथ काम करना असुविधाजनक है - इसलिए, हमने ORM के समान कार्य करने वाले एक साधारण वर्ग में सबसे अधिक बार किए गए कार्यों को लपेटा:
class S: def __init__(self,**kw): self.__dict__.update(kw) @classmethod def list_from_cursor(cls,cursor): return [cls(**dict(zip([col[0] for col in cursor.description],row))) for row in cursor.fetchall()] @classmethod def from_cursor(cls,cursor): row = cursor.fetchone() if row: return cls(**dict(zip([col[0] for col in cursor.description],row))) def __str__(self): return str(self.__dict__) def __repr__(self): return str(self) def __getitem__(self,ind): return getattr(self,ind)
हम इस वर्ग का उपयोग करके शुरू किए गए प्रदर्शन के नुकसान को मापते हैं।
def test_S(): from django.db import connection, transaction import util cursor = connection.cursor() t1 = datetime.datetime.now() for i in range(10000): cursor.execute("select * from auth_user limit 1") u = util.S.from_cursor(cursor).username t2 = datetime.datetime.now() print "S req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10. t1 = datetime.datetime.now() for i in range(10000): cursor.execute("select username from auth_user limit 1") u = util.S.from_cursor(cursor).username t2 = datetime.datetime.now() print "S opt req/seq:",10000/(t2-t1).total_seconds(),'req time (ms):',(t2-t1).total_seconds()/10.
परीक्षा परिणाम:
>>> test_S ()
S req / seq: 4714.92835902 req time (ms): 0.2120923
एस ऑप्ट रीक / seq: 7473.3388636 रिक् समय (ms): 0.133809
जैसा कि आप देख सकते हैं, नुकसान बहुत मामूली हैं: गैर-इष्टतम मामले में 0.2120923-0.1768116 = 0.0352807ms और इष्टतम में 0.133809-0.1127007 = 0.0211083ms। मैं ध्यान देता हूं कि हमारे ओआरएम में, घुटने पर किया जाता है, एक पूर्ण विकसित अजगर वस्तु बनाई जाती है।
सामान्य निष्कर्ष
शक्तिशाली सार्वभौमिक ओआरएम का उपयोग करने से
बहुत अधिक ध्यान देने योग्य प्रदर्शन
हानि होती है । फास्ट DBMS इंजन, जैसे MySQL का उपयोग करने के मामले में, डेटा एक्सेस प्रदर्शन
3-5 गुना से अधिक घट जाता
है । प्रदर्शन का नुकसान इंटेल पेंटियम डुअल सीपीयू ई 2200 @ 2.20GHz प्लेटफ़ॉर्म पर एकल ऑब्जेक्ट तक पहुंचने के लिए लगभग 0.5ms या उससे अधिक है।
नुकसान का एक महत्वपूर्ण हिस्सा डेटाबेस से प्राप्त डेटा की एक स्ट्रिंग से एक वस्तु का निर्माण है: लगभग 0.1ms। एक और 0.1ms कर्सर के निर्माण को खा जाता है, जो ORM से छुटकारा पाना काफी मुश्किल है।
शेष घाटे की उत्पत्ति अज्ञात रही। हम केवल यह मान सकते हैं कि परिणाम को संसाधित करते समय कॉल की संख्या के कारण पर्याप्त मात्रा में नुकसान हो सकता है - डेटा प्रसंस्करण परतों के अमूर्त होने के कारण।
पर्याप्त प्रदर्शन प्राप्त करने के लिए, ओआरएम डेवलपर्स को ओबीएम के लिए विशेष रूप से अमूर्त, क्वेरी डिज़ाइन और अन्य संचालन की परतों से गुजरने वाले कोड के नुकसान को ध्यान में रखना चाहिए। सही मायने में उत्पादक ORM को इस ORM का उपयोग करने वाले डेवलपर के लिए एक
बार एक पैरामीटर की गई क्वेरी तैयार करने की अनुमति देनी चाहिए और फिर विभिन्न मापदंडों के साथ इसका उपयोग समग्र प्रदर्शन पर न्यूनतम प्रभाव के साथ करना चाहिए। इस दृष्टिकोण को लागू करने के तरीकों में से एक उत्पन्न SQL अभिव्यक्तियों के लिए कुछ कैश का उपयोग करना है और तैयार किया गया क्वेरी अंतर्निहित DBMS के लिए विशिष्ट है। मेरे आश्चर्य करने के लिए, इस तथ्य के बावजूद कि इस तरह के अनुकूलन को SQLAlchemy में किया गया था, प्रदर्शन अभी भी ग्रस्त है, हालांकि यह कुछ हद तक कम गंभीर है।
मेरे लिए व्यक्तिगत रूप से, यह एक रहस्य बना हुआ है जहां दोनों ओरमों पर एक वस्तु को पढ़ने से एक और 0.3-0.4ms का नुकसान होता है। यह विशेषता है कि दोनों ओआरएम प्रोसेसर संसाधनों को
लगभग समान रूप से उत्पादक
रूप से खर्च करते हैं। इससे हमें लगता है कि नुकसान कुछ स्थानीय ORM समस्याओं (जैसे django से तैयार प्रश्नों की कैश की कमी) के कारण नहीं हुआ था, लेकिन
वास्तुशिल्प तत्वों द्वारा संभवत: दोनों ORM में समान हैं। मैं पेशेवर टिप्पणियों के लिए समुदाय का आभारी रहूंगा।