วัดก่อนปรับแต่ง: timeit และ cProfile, สิ่งที่ GIL บล็อกจริง ๆ, ความประหยัดของ generator และกับดักหน่วยความจำของสตริงกับสไลซ์
เปิดบทเรียนนี้ใน Kodokonอย่าเดา จงวัด timeit แยกไมโครเบนช์มาร์กที่เชื่อถือได้ออกมา มันปิดการทำงานของ garbage collector และรันซ้ำหลายครั้งเพื่อกลบสัญญาณรบกวน ส่วน cProfile วาดแผนที่การเรียกของทั้งโปรแกรม เรียงลำดับตาม cumtime (เวลาสะสม รวมการเรียกย่อยด้วย) เพื่อระบุตัวการที่แท้จริง กฎทองของงานด้านประสิทธิภาพทุกครั้งคือ บรรทัดที่ช้าแทบไม่เคยเป็นบรรทัดที่คุณสงสัย
python -m timeit "sum(x * x for x in range(1000))"
python -m timeit -s "s = 'abc' * 100" "s.upper()"
python -m cProfile -s cumtime -m timeit "min(range(50))"GIL (Global Interpreter Lock) รับประกันว่ามีเพียงเธรดเดียวเท่านั้นที่รันไบต์โค้ดของ Python ได้ในแต่ละขณะภายในโปรเซสหนึ่ง ผลที่ตามมาคือ เธรดไม่ได้ทำให้การคำนวณล้วน ๆ เร็วขึ้น อย่างไรก็ตาม พวกมันยังเหมาะอย่างยิ่งกับงาน I/O เพราะ GIL จะถูก ปล่อย ระหว่างการอ่านข้อมูลจากเครือข่ายหรือดิสก์ รวมถึงถูกปล่อยโดยไลบรารีภาษา C จำนวนมาก เช่น NumPy หากต้องการใช้งานหลายคอร์ให้เต็มที่ด้วยการคำนวณ ให้ไปทาง multiprocessing หรือ concurrent.futures.ProcessPoolExecutor ส่วน Python 3.13 มีเวอร์ชันทดลองแบบ "free-threaded" ที่ไม่มี GIL ซึ่งยังพบได้ยากในโปรดักชัน
import sys
squares_list = [x * x for x in range(1_000_000)]
squares_gen = (x * x for x in range(1_000_000))
print(sys.getsizeof(squares_list), "bytes")
print(sys.getsizeof(squares_gen), "bytes")
print(sum(squares_gen))generator หนักเพียงไม่กี่ร้อยไบต์ ไม่ว่าลำดับจะยาวเท่าไรก็ตาม มันเก็บเพียงสถานะการทำงานของตัวเอง และผลิตค่าแต่ละค่าเมื่อมีการร้องขอ จงต่อพวกมันเข้าเป็น pipeline - sum(x * x for x in data) - เพื่อประมวลผลสตรีมโดยไม่ต้องสร้างลิสต์ตัวกลางขึ้นมาจริง ๆ เลย ข้อแลกเปลี่ยนที่ต้องรู้ไว้คือ generator ใช้ได้ครั้งเดียว เมื่อหมดสิ้นแล้ว มันจะไม่ผลิตอะไรอีกเลยอย่างเงียบ ๆ - เป็นต้นตอคลาสสิกของบั๊กที่การวนซ้ำรอบที่สองดู "ว่างเปล่า"
import timeit
def concat(n: int) -> str:
out = ""
for i in range(n):
out += str(i)
return out
def join(n: int) -> str:
return "".join(str(i) for i in range(n))
t1 = timeit.timeit(lambda: concat(20_000), number=10)
t2 = timeit.timeit(lambda: join(20_000), number=10)
print(f"concat: {t1:.3f} s / join: {t2:.3f} s")sys.getsizeof ของ generator จึงเล็กจิ๋วเมื่อเทียบกับลิสต์ที่เทียบเท่ากัน?''.join(parts) จึงชนะ out += part ที่ทำซ้ำในลูป?