คู่มือการทดสอบประสิทธิภาพและการเพิ่มประสิทธิภาพด้วย Python และ Django
เผยแพร่แล้ว: 2022-03-11Donald Knuth กล่าวว่า "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากเหง้าของความชั่วร้ายทั้งหมด" แต่มีบางครั้งที่มักจะเกิดขึ้นในโครงการที่โตเต็มที่ซึ่งมีภาระงานสูง เมื่อมีความจำเป็นที่หลีกเลี่ยงไม่ได้ในการปรับให้เหมาะสม ในบทความนี้ ฉันอยากจะพูดถึง วิธีการทั่วไป 5 วิธีในการเพิ่มประสิทธิภาพโค้ดของโครงการเว็บของคุณ ฉันจะใช้ Django แต่หลักการควรจะคล้ายกันสำหรับกรอบงานและภาษาอื่นๆ ในบทความนี้ ฉันจะใช้วิธีเหล่านี้เพื่อลดเวลาตอบสนองของแบบสอบถามจาก 77 เป็น 3.7 วินาที
โค้ดตัวอย่างนี้ดัดแปลงมาจากโปรเจ็กต์จริงที่ฉันทำงานด้วยและเป็นการสาธิตเทคนิคการเพิ่มประสิทธิภาพการทำงาน ในกรณีที่คุณต้องการติดตามและเห็นผลด้วยตัวเอง คุณสามารถคว้าโค้ดที่สถานะเริ่มต้นบน 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
