คู่มือการทดสอบประสิทธิภาพและการเพิ่มประสิทธิภาพด้วย Python และ Django

เผยแพร่แล้ว: 2022-03-11

Donald Knuth กล่าวว่า "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากเหง้าของความชั่วร้ายทั้งหมด" แต่มีบางครั้งที่มักจะเกิดขึ้นในโครงการที่โตเต็มที่ซึ่งมีภาระงานสูง เมื่อมีความจำเป็นที่หลีกเลี่ยงไม่ได้ในการปรับให้เหมาะสม ในบทความนี้ ฉันอยากจะพูดถึง วิธีการทั่วไป 5 วิธีในการเพิ่มประสิทธิภาพโค้ดของโครงการเว็บของคุณ ฉันจะใช้ Django แต่หลักการควรจะคล้ายกันสำหรับกรอบงานและภาษาอื่นๆ ในบทความนี้ ฉันจะใช้วิธีเหล่านี้เพื่อลดเวลาตอบสนองของแบบสอบถามจาก 77 เป็น 3.7 วินาที

คู่มือการเพิ่มประสิทธิภาพและการทดสอบประสิทธิภาพด้วย Python และ Django

โค้ดตัวอย่างนี้ดัดแปลงมาจากโปรเจ็กต์จริงที่ฉันทำงานด้วยและเป็นการสาธิตเทคนิคการเพิ่มประสิทธิภาพการทำงาน ในกรณีที่คุณต้องการติดตามและเห็นผลด้วยตัวเอง คุณสามารถคว้าโค้ดที่สถานะเริ่มต้นบน GitHub และทำการเปลี่ยนแปลงที่เกี่ยวข้องในขณะที่ติดตามบทความ ฉันจะใช้ Python 2 เนื่องจากแพ็คเกจของบุคคลที่สามบางแพ็คเกจยังไม่พร้อมใช้งานสำหรับ Python 3

แนะนำแอปพลิเคชันของเรา

โครงการเว็บของเราเพียงแค่ติดตามข้อเสนอด้านอสังหาริมทรัพย์ตามประเทศ ดังนั้นจึงมีเพียงสองรุ่น:

 # houses/models.py from utils.hash import Hasher class HashableModel(models.Model): """Provide a hash property for models.""" class Meta: abstract = True @property def hash(self): return Hasher.from_model(self) class Country(HashableModel): """Represent a country in which the house is positioned.""" name = models.CharField(max_length=30) def __unicode__(self): return self.name class House(HashableModel): """Represent a house with its characteristics.""" # Relations country = models.ForeignKey(Country, related_name='houses') # Attributes address = models.CharField(max_length=255) sq_meters = models.PositiveIntegerField() kitchen_sq_meters = models.PositiveSmallIntegerField() nr_bedrooms = models.PositiveSmallIntegerField() nr_bathrooms = models.PositiveSmallIntegerField() nr_floors = models.PositiveSmallIntegerField(default=1) year_built = models.PositiveIntegerField(null=True, blank=True) house_color_outside = models.CharField(max_length=20) distance_to_nearest_kindergarten = models.PositiveIntegerField(null=True, blank=True) distance_to_nearest_school = models.PositiveIntegerField(null=True, blank=True) distance_to_nearest_hospital = models.PositiveIntegerField(null=True, blank=True) has_cellar = models.BooleanField(default=False) has_pool = models.BooleanField(default=False) has_garage = models.BooleanField(default=False) price = models.PositiveIntegerField() def __unicode__(self): return '{} {}'.format(self.country, self.address)

บทคัดย่อ HashableModel จัดเตรียมโมเดลที่สืบทอดจากคุณสมบัติ hash ที่มีคีย์หลักของอินสแตนซ์และประเภทเนื้อหาของโมเดล ซึ่งจะซ่อนข้อมูลที่ละเอียดอ่อน เช่น รหัสอินสแตนซ์ โดยการแทนที่ด้วยแฮช นอกจากนี้ยังอาจมีประโยชน์ในกรณีที่โปรเจ็กต์ของคุณมีหลายโมเดล และคุณต้องการที่ที่รวมศูนย์ที่เลิกแฮชและตัดสินใจว่าจะทำอย่างไรกับอินสแตนซ์โมเดลต่างๆ ของคลาสต่างๆ โปรดทราบว่าสำหรับโครงการขนาดเล็กของเรา การแฮชไม่จำเป็นจริงๆ เนื่องจากเราสามารถจัดการได้โดยไม่ต้องใช้มัน แต่จะช่วยสาธิตเทคนิคการเพิ่มประสิทธิภาพบางอย่าง ดังนั้นฉันจะเก็บไว้ที่นั่น

นี่คือคลาส Hasher :

 # utils/hash.py import basehash class Hasher(object): @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): base36 = basehash.base36() content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): base36 = basehash.base36() unhashed = '%09d' % base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]

เนื่องจากเราต้องการให้บริการข้อมูลนี้ผ่านจุดปลาย API เราติดตั้ง Django REST Framework และกำหนดซีเรียลไลเซอร์และมุมมองต่อไปนี้:

 # houses/serializers.py class HouseSerializer(serializers.ModelSerializer): """Serialize a `houses.House` instance.""" id = serializers.ReadOnlyField(source="hash") country = serializers.ReadOnlyField(source="country.hash") class Meta: model = House fields = ( 'id', 'address', 'country', 'sq_meters', 'price' )
 # houses/views.py class HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer country = None def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) return queryset def list(self, request, *args, **kwargs): # Skipping validation code for brevity country = self.request.GET.get("country") self.country = Hasher.to_object_pk(country) queryset = self.get_queryset() serializer = self.serializer_class(queryset, many=True) return Response(serializer.data)

ตอนนี้ เราเติมฐานข้อมูลของเราด้วยข้อมูลบางส่วน (อินสแตนซ์บ้านทั้งหมด 100,000 รายการที่สร้างขึ้นโดยใช้ factory-boy : 50,000 สำหรับประเทศหนึ่ง 40,000 สำหรับอีกประเทศหนึ่ง และ 10,000 สำหรับประเทศที่สาม) และพร้อมที่จะทดสอบประสิทธิภาพของแอปของเรา

การเพิ่มประสิทธิภาพการทำงานเป็นเรื่องของการวัดเท่านั้น

มีหลายสิ่งที่เราวัดได้ในโครงการ:

  • เวลาดำเนินการ
  • จำนวนบรรทัดของรหัส
  • จำนวนการเรียกใช้ฟังก์ชัน
  • หน่วยความจำที่จัดสรร
  • เป็นต้น

แต่ไม่ใช่ทั้งหมดที่เกี่ยวข้องในการวัดว่าโครงการของเราทำงานได้ดีเพียงใด โดยทั่วไป มีสองตัวชี้วัดหลักที่สำคัญที่สุด: ระยะเวลาที่บางสิ่งดำเนินการและจำนวนหน่วยความจำที่ต้องการ

ในโครงการเว็บ เวลาตอบสนอง (เวลาที่เซิร์ฟเวอร์ต้องการเพื่อรับคำขอที่สร้างขึ้นจากการกระทำของผู้ใช้บางราย ประมวลผลและส่งผลลัพธ์กลับ) มักจะเป็นตัวชี้วัดที่สำคัญที่สุด เนื่องจากจะไม่ทำให้ผู้ใช้เบื่อระหว่างรอ เพื่อตอบกลับและเปลี่ยนไปใช้แท็บอื่นในเบราว์เซอร์

ในการเขียนโปรแกรม การวิเคราะห์ประสิทธิภาพของโครงการเรียกว่าการทำโปรไฟล์ เพื่อกำหนดโปรไฟล์ประสิทธิภาพของจุดปลาย API ของเรา เราจะใช้แพ็คเกจ Silk หลังจากติดตั้งและทำการเรียก /api/v1/houses/?country=5T22RI (แฮชที่สอดคล้องกับประเทศที่มีรายการบ้าน 50,000 รายการ) เราได้รับสิ่งนี้:

200 รับ
/api/v1/บ้าน/

โดยรวม 77292ms
15854ms กับข้อความค้นหา
50004 แบบสอบถาม

เวลาตอบสนองโดยรวมคือ 77 วินาที โดยใช้เวลา 16 วินาทีในการสืบค้นข้อมูลในฐานข้อมูล ซึ่งมีการสืบค้นข้อมูลทั้งหมด 50,000 รายการ ด้วยจำนวนที่มหาศาลเช่นนี้ ยังมีพื้นที่ให้ปรับปรุงอีกมาก เรามาเริ่มกันเลยดีกว่า

1. การเพิ่มประสิทธิภาพการสืบค้นฐานข้อมูล

เคล็ดลับที่พบบ่อยที่สุดประการหนึ่งในการเพิ่มประสิทธิภาพการทำงานคือการทำให้แน่ใจว่าการสืบค้นฐานข้อมูลของคุณได้รับการปรับให้เหมาะสม กรณีนี้ไม่มีข้อยกเว้น นอกจากนี้ เราสามารถดำเนินการหลายอย่างเกี่ยวกับการสืบค้นข้อมูลของเราเพื่อปรับเวลาตอบสนองให้เหมาะสมที่สุด

1.1 ให้ข้อมูลทั้งหมดพร้อมกัน

เมื่อพิจารณาให้ละเอียดยิ่งขึ้นว่า 50,000 ข้อความค้นหาเหล่านั้นคืออะไร คุณจะเห็นว่าสิ่งเหล่านี้เป็นข้อความค้นหาที่ซ้ำซ้อนทั้งหมดสำหรับตาราง houses_country :

200 รับ
/api/v1/บ้าน/

โดยรวม 77292ms
15854ms กับข้อความค้นหา
50004 แบบสอบถาม

ที่ โต๊ะ เข้าร่วม เวลาดำเนินการ (มิลลิวินาที)
+0:01:15.874374 "บ้าน_ประเทศ" 0 0.176
+0:01:15.873304 "บ้าน_ประเทศ" 0 0.218
+0:01:15.872225 "บ้าน_ประเทศ" 0 0.218
+0:01:15.871155 "บ้าน_ประเทศ" 0 0.198
+0:01:15.870099 "บ้าน_ประเทศ" 0 0.173
+0:01:15.869050 "บ้าน_ประเทศ" 0 0.197
+0:01:15.867877 "บ้าน_ประเทศ" 0 0.221
+0:01:15.866807 "บ้าน_ประเทศ" 0 0.203
+0:01:15.865646 "บ้าน_ประเทศ" 0 0.211
+0:01:15.864562 "บ้าน_ประเทศ" 0 0.209
+0:01:15.863511 "บ้าน_ประเทศ" 0 0.181
+0:01:15.862435 "บ้าน_ประเทศ" 0 0.228
+0:01:15.861413 "บ้าน_ประเทศ" 0 0.174

สาเหตุของปัญหานี้คือใน Django ชุดแบบสอบถามนั้น ขี้เกียจ ซึ่งหมายความว่าชุดคิวรีจะไม่ได้รับการประเมินและจะไม่กระทบกับฐานข้อมูลจนกว่าคุณจะต้องการรับข้อมูลจริงๆ ในเวลาเดียวกัน จะได้รับเฉพาะข้อมูลที่คุณบอกให้ทำ และทำการร้องขอในภายหลังหากต้องการข้อมูลเพิ่มเติม

นั่นคือสิ่งที่เกิดขึ้นในกรณีของเรา เมื่อตั้งค่าการสืบค้นผ่าน House.objects.filter(country=country) Django จะได้รับรายชื่อบ้านทั้งหมดในประเทศที่กำหนด อย่างไรก็ตาม เมื่อทำซีเรียลไลซ์อินสแตนซ์ house HouseSerializer ต้องการอินสแตนซ์ country ของเฮาส์เพื่อคำนวณฟิลด์ country ของซีเรียลไลเซอร์ เนื่องจากข้อมูลประเทศไม่มีอยู่ในชุดการสืบค้น django จึงส่งคำขอเพิ่มเติมเพื่อรับข้อมูลนั้น และมันก็เป็นเช่นนั้นสำหรับบ้านทุกหลังในชุดข้อความค้นหา นั่นคือทั้งหมด 50,000 ครั้ง

วิธีแก้ปัญหานั้นง่ายมาก ในการดึงข้อมูลที่จำเป็นทั้งหมดสำหรับการทำให้เป็นอนุกรม คุณสามารถใช้ select_related() ในชุดการสืบค้น ดังนั้น get_queryset ของเราจะมีลักษณะดังนี้:

 def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country).select_related('country') return queryset

มาดูกันว่าสิ่งนี้ส่งผลต่อประสิทธิภาพอย่างไร:

200 รับ
/api/v1/บ้าน/

โดยรวม 35979ms
102ms ในการสืบค้น
4 คำถาม

เวลาตอบสนองโดยรวมลดลงเหลือ 36 วินาที และเวลาที่ใช้ในฐานข้อมูลคือ ~100ms โดยมีเพียง 4 ข้อความค้นหาเท่านั้น! นั่นเป็นข่าวดี แต่เราสามารถทำได้มากกว่านี้

1.2 ระบุเฉพาะข้อมูลที่เกี่ยวข้อง

โดยค่าเริ่มต้น Django จะแยกฟิลด์ทั้งหมดออกจากฐานข้อมูล อย่างไรก็ตาม เมื่อคุณมีตารางขนาดใหญ่ที่มีคอลัมน์และแถวจำนวนมาก คุณควรบอก Django ว่าควรแยกฟิลด์ใดเป็นพิเศษ เพื่อจะได้ไม่ต้องใช้เวลาในการรับข้อมูลที่ไม่ได้ใช้เลย ในกรณีของเรา เราต้องการเพียงห้าฟิลด์สำหรับซีเรียลไลซ์เซชั่น แต่เรามี 17 ฟิลด์ การระบุฟิลด์ที่จะดึงข้อมูลจากฐานข้อมูลนั้นสมเหตุสมผล เหมาะสม เพื่อที่เราจะลดเวลาตอบสนองลงต่อไป

Django มีชุดคำสั่ง defer() และชุดคำสั่ง only() สำหรับทำสิ่งนี้ อันแรกระบุว่าฟิลด์ใดที่จะ ไม่ โหลด และอันที่สองระบุว่าฟิลด์ใดที่จะโหลด เท่านั้น

 def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country)\ .select_related('country')\ .only('id', 'address', 'country', 'sq_meters', 'price') return queryset

ซึ่งช่วยลดเวลาที่ใช้ในการสืบค้นลงครึ่งหนึ่ง ซึ่งถือว่าดี แต่ 50ms นั้นไม่มากนัก เวลาโดยรวมก็ลดลงเล็กน้อยเช่นกัน แต่ยังมีพื้นที่ให้ตัดมากขึ้น

200 รับ
/api/v1/บ้าน/

โดยรวม 33111ms
52ms ในการสืบค้น
4 คำถาม

2. เพิ่มประสิทธิภาพโค้ดของคุณ

คุณไม่สามารถเพิ่มประสิทธิภาพการสืบค้นฐานข้อมูลได้อย่างไม่จำกัด และผลลัพธ์ล่าสุดของเราก็แสดงให้เห็นว่า แม้ว่าเราจะลดเวลาที่ใช้ในการสืบค้นลงโดยสมมุติฐานเป็น 0 เราก็ยังคงเผชิญกับความเป็นจริงในการรอครึ่งนาทีเพื่อรับคำตอบ ถึงเวลาเปลี่ยนไปใช้การเพิ่มประสิทธิภาพอีกระดับหนึ่ง: ตรรกะทางธุรกิจ

2.1 ลดความซับซ้อนของรหัสของคุณ

บางครั้ง แพ็คเกจของบริษัทอื่นอาจมีค่าใช้จ่ายมากมายสำหรับงานง่ายๆ ตัวอย่างหนึ่งคืองานของเราในการส่งคืนอินสแตนซ์เฮาส์ที่ซีเรียลไลซ์

Django REST Framework นั้นยอดเยี่ยม โดยมีคุณสมบัติที่มีประโยชน์มากมายที่พร้อมใช้งานทันที อย่างไรก็ตาม เป้าหมายหลักของเราในตอนนี้คือการลดเวลาตอบสนอง ดังนั้นจึงเป็นตัวเลือกที่ดีสำหรับการเพิ่มประสิทธิภาพ โดยเฉพาะอย่างยิ่งวัตถุที่ต่อเนื่องกันนั้นค่อนข้างง่าย

มาเขียนซีเรียลไลเซอร์แบบกำหนดเองเพื่อจุดประสงค์นี้กัน เพื่อให้ง่าย เราจะมีวิธีการแบบคงที่เดียวที่ใช้งานได้ ในความเป็นจริง คุณอาจต้องการมีคลาสและลายเซ็นเมธอดเดียวกันเพื่อให้สามารถใช้ซีเรียลไลเซอร์แทนกันได้:

 # houses/serializers.py class HousePlainSerializer(object): """ Serializes a House queryset consisting of dicts with the following keys: 'id', 'address', 'country', 'sq_meters', 'price'. """ @staticmethod def serialize_data(queryset): """ Return a list of hashed objects from the given queryset. """ return [ { 'id': Hasher.from_pk_and_class(entry['id'], House), 'address': entry['address'], 'country': Hasher.from_pk_and_class(entry['country'], Country), 'sq_meters': entry['sq_meters'], 'price': entry['price'] } for entry in queryset ] # houses/views.py class HouseListAPIView(ListAPIView): model = House serializer_class = HouseSerializer plain_serializer_class = HousePlainSerializer # <-- added custom serializer country = None def get_queryset(self): country = get_object_or_404(Country, pk=self.country) queryset = self.model.objects.filter(country=country) return queryset def list(self, request, *args, **kwargs): # Skipping validation code for brevity country = self.request.GET.get("country") self.country = Hasher.to_object_pk(country) queryset = self.get_queryset() data = self.plain_serializer_class.serialize_data(queryset) # <-- serialize return Response(data)

200 รับ
/api/v1/บ้าน/

โดยรวม 17312ms
38ms ในการสืบค้น
4 คำถาม

นี่ดูดีขึ้นแล้ว เวลาตอบสนองลดลงเกือบครึ่งหนึ่งเนื่องจากเราไม่ได้ใช้รหัส DRF serializers

ผลลัพธ์ที่วัดได้อีกอย่างหนึ่ง—จำนวนการเรียกใช้ฟังก์ชันทั้งหมดระหว่างรอบคำขอ/การตอบสนอง—ลดลงจากการโทร 15,859,427 ครั้ง (จากคำขอในหัวข้อ 1.2 ด้านบน) เป็น 9,257,469 ครั้ง ซึ่งหมายความว่าประมาณ 1/3 ของการเรียกใช้ฟังก์ชันทั้งหมดทำโดย Django REST Framework

2.2 อัปเดต/เปลี่ยนแพ็คเกจของบุคคลที่สาม

เทคนิคการเพิ่มประสิทธิภาพที่อธิบายข้างต้นเป็นเทคนิคทั่วไป ซึ่งคุณสามารถทำได้โดยไม่ต้องวิเคราะห์และคิดอย่างละเอียด อย่างไรก็ตาม 17 วินาทียังคงรู้สึกค่อนข้างยาว เพื่อลดจำนวนนี้ เราจะต้องเจาะลึกลงไปในโค้ดของเราและวิเคราะห์ว่าเกิดอะไรขึ้นภายใต้ประทุน กล่าวอีกนัยหนึ่ง เราจะต้องสร้างโปรไฟล์โค้ดของเรา

คุณสามารถทำโปรไฟล์ได้ด้วยตัวเอง โดยใช้ตัวสร้างโปรไฟล์ Python ในตัว หรือคุณสามารถใช้แพ็คเกจของบริษัทอื่น (ที่ใช้ตัวสร้างโปรไฟล์ Python ในตัว) เนื่องจากเราใช้ silk อยู่แล้ว มันสามารถสร้างโปรไฟล์โค้ดและสร้างไฟล์โปรไฟล์ไบนารี ที่เราสามารถมองเห็นเพิ่มเติมได้ มีแพ็คเกจการแสดงภาพหลายชุดที่เปลี่ยนโปรไฟล์ไบนารีเป็นการแสดงข้อมูลเชิงลึก ฉันจะใช้แพ็คเกจ snakeviz

นี่คือการแสดงภาพโปรไฟล์ไบนารีของคำขอล่าสุดจากด้านบน โดยเชื่อมต่อกับวิธีการจัดส่งของมุมมอง:

รูปภาพของวิธีการจัดส่งของมุมมอง

จากบนลงล่างคือ call stack โดยแสดงชื่อไฟล์ ชื่อเมธอด/ฟังก์ชันพร้อมหมายเลขบรรทัด และเวลาสะสมที่เกี่ยวข้องในเมธอดนั้น ตอนนี้มันง่ายกว่าที่จะเห็นว่าการแบ่งเวลาของสิงโตนั้นทุ่มเทให้กับการคำนวณแฮช (สี่เหลี่ยม __init__.py และ primes.py ที่มีสีม่วง)

ในปัจจุบัน นี่คือปัญหาคอขวดด้านประสิทธิภาพหลักในโค้ดของเรา แต่ในขณะเดียวกัน โค้ดดังกล่าวก็ไม่ใช่โค้ด ของเรา จริงๆ แต่เป็นแพ็กเกจของบุคคลที่สาม

ในสถานการณ์เช่นนี้ มีบางสิ่งที่เราสามารถทำได้อย่างจำกัด:

  • ตรวจสอบแพ็คเกจเวอร์ชันใหม่ (ซึ่งหวังว่าจะมีประสิทธิภาพที่ดีขึ้น)
  • ค้นหาแพ็คเกจอื่นที่ทำงานได้ดีกว่ากับงานที่เราต้องการ
  • เขียนการใช้งานของเราเองที่จะเอาชนะประสิทธิภาพของแพ็คเกจที่เราใช้อยู่ในปัจจุบัน

โชคดีสำหรับฉัน มีแพ็คเกจ basehash เวอร์ชันใหม่กว่าซึ่งมีหน้าที่ในการแฮช รหัสใช้ v.2.1.0 แต่มี v.3.0.4 สถานการณ์ดังกล่าว เมื่อคุณสามารถอัปเดตเป็นเวอร์ชันที่ใหม่กว่าของแพ็คเกจ จะมีโอกาสมากขึ้นเมื่อคุณกำลังทำงานกับโปรเจ็กต์ที่มีอยู่

เมื่อตรวจสอบบันทึกประจำรุ่นสำหรับ v.3 มีประโยคเฉพาะที่ฟังดูมีแนวโน้มมาก:

การยกเครื่องครั้งใหญ่เสร็จสิ้นด้วยอัลกอริธึมพื้นฐาน รวมถึง (sic) รองรับ gmpy2 หากมี (sic) ในระบบสำหรับการเพิ่มขึ้นนั้นมากขึ้น

ลองหานี้ออก!

 pip install -U basehash gmpy2

200 รับ
/api/v1/บ้าน/

7738ms โดยรวม
59ms สำหรับข้อความค้นหา
4 คำถาม

เราลดเวลาตอบสนองจาก 17 เหลือน้อยกว่า 8 วินาที ผลลัพธ์ที่ยอดเยี่ยม แต่มีอีกสิ่งหนึ่งที่เราควรดู

2.3 Refactor รหัสของคุณเอง

จนถึงตอนนี้ เราได้ปรับปรุงการสืบค้นข้อมูล แทนที่โค้ดที่ซับซ้อนและทั่วไปของบุคคลที่สามด้วยฟังก์ชันเฉพาะของเราเอง และแพ็คเกจของบุคคลที่สามที่อัปเดตแล้ว แต่เรายังคงไม่แตะต้องโค้ดที่มีอยู่ของเรา แต่บางครั้งการปรับโครงสร้างโค้ดที่มีอยู่ใหม่เพียงเล็กน้อยก็สามารถสร้างผลลัพธ์ที่น่าประทับใจได้ แต่สำหรับสิ่งนี้ เราต้องวิเคราะห์ผลลัพธ์ของโปรไฟล์อีกครั้ง

รูปภาพของผลลัพธ์โปรไฟล์

เมื่อพิจารณาให้ละเอียดยิ่งขึ้น คุณจะเห็นว่าการแฮชยังคงเป็นปัญหาอยู่ (ไม่น่าแปลกใจที่มันเป็นสิ่งเดียวที่เราทำกับข้อมูลของเรา) แม้ว่าเราจะปรับปรุงไปในทิศทางนั้นแล้วก็ตาม อย่างไรก็ตาม สี่เหลี่ยมสีเขียวที่บอกว่า __init__.py ใช้เวลา 2.14 วินาทีรบกวนจิตใจฉัน ตามด้วย __init__.py:54(hash) สีเทาที่อยู่ถัดจากมัน ซึ่งหมายความว่าการเริ่มต้นบางอย่างใช้เวลานาน

มาดูซอร์สโค้ดของแพ็คเกจ basehash กัน

 # basehash/__init__.py # Initialization of `base36` class initializes the parent, `base` class. class base36(base): def __init__(self, length=HASH_LENGTH, generator=GENERATOR): super(base36, self).__init__(BASE36, length, generator) class base(object): def __init__(self, alphabet, length=HASH_LENGTH, generator=GENERATOR): if len(set(alphabet)) != len(alphabet): raise ValueError('Supplied alphabet cannot contain duplicates.') self.alphabet = tuple(alphabet) self.base = len(alphabet) self.length = length self.generator = generator self.maximum = self.base ** self.length - 1 self.prime = next_prime(int((self.maximum + 1) * self.generator)) # `next_prime` call on each initialized instance

อย่างที่คุณเห็น การเริ่มต้นของอินสแตนซ์ base จำเป็นต้องมีการเรียกใช้ฟังก์ชัน next_prime ซึ่งค่อนข้างหนักอย่างที่เราเห็นในสี่เหลี่ยมด้านล่างซ้ายของการแสดงภาพด้านบน

มาดูคลาส Hash ของฉันกันอีกครั้ง:

 class Hasher(object): @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): base36 = basehash.base36() # <-- initializing on each method call content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): base36 = basehash.base36() # <-- initializing on each method call unhashed = '%09d' % base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]

อย่างที่คุณเห็น ฉันได้ระบุสองวิธีที่กำลังเริ่มต้นอินสแตนซ์ base36 ในการเรียกแต่ละเมธอด ซึ่งไม่จำเป็นจริงๆ

เนื่องจากการแฮชเป็นขั้นตอนที่กำหนด หมายความว่าสำหรับค่าอินพุตที่กำหนด จะต้องสร้างค่าแฮชเดียวกันเสมอ เราสามารถทำให้มันเป็นแอตทริบิวต์คลาสโดยไม่ต้องกลัวว่ามันจะทำลายบางสิ่ง มาดูกันว่ามันทำงานอย่างไร:

 class Hasher(object): base36 = basehash.base36() # <-- initialize hasher only once @classmethod def from_model(cls, obj, klass=None): if obj.pk is None: return None return cls.make_hash(obj.pk, klass if klass is not None else obj) @classmethod def make_hash(cls, object_pk, klass): content_type = ContentType.objects.get_for_model(klass, for_concrete_model=False) return cls.base36.hash('%(contenttype_pk)03d%(object_pk)06d' % { 'contenttype_pk': content_type.pk, 'object_pk': object_pk }) @classmethod def parse_hash(cls, obj_hash): unhashed = '%09d' % cls.base36.unhash(obj_hash) contenttype_pk = int(unhashed[:-6]) object_pk = int(unhashed[-6:]) return contenttype_pk, object_pk @classmethod def to_object_pk(cls, obj_hash): return cls.parse_hash(obj_hash)[1]

200 รับ
/api/v1/บ้าน/

โดยรวม 3766ms
38ms ในการสืบค้น
4 คำถาม

ผลลัพธ์สุดท้ายใช้เวลาน้อยกว่า 4 วินาที ซึ่งน้อยกว่าที่เราเริ่มต้นมาก การเพิ่มประสิทธิภาพของเวลาตอบสนองสามารถทำได้โดยใช้การแคช แต่ฉันจะไม่แก้ไขปัญหานี้ในบทความนี้

บทสรุป

การเพิ่มประสิทธิภาพการทำงานเป็นกระบวนการของการวิเคราะห์และการค้นพบ ไม่มีกฎเกณฑ์ตายตัวที่ใช้กับทุกกรณี เนื่องจากแต่ละโครงการมีขั้นตอนและปัญหาคอขวดเป็นของตัวเอง อย่างไรก็ตาม สิ่งแรกที่คุณควรทำคือโปรไฟล์โค้ดของคุณ และหากในตัวอย่างสั้นๆ เช่นนี้ ฉันสามารถลดเวลาตอบสนองจาก 77 วินาทีเป็น 3.7 วินาที โปรเจ็กต์ขนาดใหญ่ก็มีศักยภาพในการปรับให้เหมาะสมมากยิ่งขึ้น

หากคุณสนใจที่จะอ่านบทความที่เกี่ยวข้องกับ Django เพิ่มเติม ลองดูข้อผิดพลาด 10 อันดับแรกที่นักพัฒนา Django ทำโดย Alexandr Shurigin ผู้พัฒนา Toptal Django