- ผลลัพธ์เบนช์มาร์กที่วัด ตัวเลขด้านประสิทธิภาพของการคำนวณ หน่วยความจำ และอินพุต/เอาต์พุตของ Python อย่างเป็นระบบ โดยทำให้เวลาและการใช้หน่วยความจำของแต่ละการทำงานเป็นค่าที่วัดได้
- ในด้าน ความเร็ว มีการแสดงเวลาแฝงสัมพัทธ์ของการทำงานหลากหลายประเภท เช่น การเข้าถึงแอตทริบิวต์ 14ns, การเพิ่มข้อมูลลงลิสต์ 29ns, การเปิดไฟล์ 9μs, การตอบสนองของ FastAPI 8.6μs
- ในด้าน หน่วยความจำ มีการให้ตัวเลขเฉพาะเจาะจง เช่น สตริงว่าง 41 ไบต์, จำนวนเต็ม 28 ไบต์, ลิสต์ว่าง 56 ไบต์, ดิกชันนารีว่าง 64 ไบต์, โปรเซสว่าง 16MB
- มีการเปรียบเทียบความต่างด้านประสิทธิภาพของไลบรารีมาตรฐานและไลบรารีทางเลือกในแต่ละด้าน เช่น โครงสร้างข้อมูล การซีเรียลไลซ์ และการประมวลผลแบบอะซิงก์ (
orjson, msgspec เป็นต้น)
- บทเรียนสำคัญ ที่เน้นคือ โอเวอร์เฮดหน่วยความจำของออบเจ็กต์ Python ที่สูง, การค้นหาที่รวดเร็วของ dict/set, ผลของ
__slots__ ในการลดการใช้หน่วยความจำ, และการตระหนักถึงโอเวอร์เฮดของการประมวลผลแบบอะซิงก์
ภาพรวม
- เอกสารที่รวบรวม ตัวชี้วัดด้านประสิทธิภาพ ที่นักพัฒนา Python ควรรู้ โดยแสดงค่าจริงที่วัดได้ของความเร็วในการทำงานและการใช้หน่วยความจำ
- เบนช์มาร์กดำเนินการบนสภาพแวดล้อม CPython 3.14.2, Mac Mini M4 Pro (ARM, 14 คอร์, RAM 24GB)
- ผลลัพธ์เน้นที่ การเปรียบเทียบเชิงสัมพัทธ์ และมีการเปิดเผยโค้ดกับข้อมูลไว้ในคลัง GitHub
การใช้หน่วยความจำ (Memory Costs)
- โปรเซส Python ว่าง ใช้หน่วยความจำ 15.73MB
- สตริง มีขนาดพื้นฐาน 41 ไบต์ และเพิ่มอีก 1 ไบต์ต่ออักขระ 1 ตัว
- ตัวอย่าง: สตริงว่าง 41B, สตริงยาว 100 ตัวอักษร 141B
- ชนิดตัวเลข: จำนวนเต็มขนาดเล็ก (0–256) 28B, จำนวนเต็มขนาดใหญ่ (1000) ก็ 28B, จำนวนเต็มขนาดใหญ่มาก (10ⁱ⁰⁰) 72B, จำนวนทศนิยมแบบลอยตัว 24B
- ขนาดพื้นฐานของ คอลเลกชัน: ลิสต์ 56B, ดิกชันนารี 64B, เซต 216B
- เมื่อมี 1,000 รายการ: ลิสต์ 35.2KB, ดิกชันนารี 63.4KB, เซต 59.6KB
- อินสแตนซ์ของคลาส: คลาสทั่วไป (5 แอตทริบิวต์) 694B, คลาส
__slots__ 212B
- เมื่อมีอินสแตนซ์ 1,000 ตัว: คลาสทั่วไป 165.2KB, คลาส
__slots__ 79.1KB
การทำงานพื้นฐาน (Basic Operations)
- การคำนวณเลขคณิต: การบวกจำนวนเต็ม 19ns, การบวกจำนวนจริง 18.4ns, การคูณจำนวนเต็ม 19.4ns
- การทำงานกับสตริง: การต่อสตริง 39.1ns, f-string 64.9ns,
.format() 103ns, การฟอร์แมตแบบ % 89.8ns
- การทำงานกับลิสต์:
append() 28.7ns, list comprehension (1,000 รายการ) 9.45μs, for loop แบบเดียวกัน 11.9μs
- list comprehension เร็วกว่า for loop ราว 26%
การเข้าถึงและการวนซ้ำคอลเลกชัน (Collection Access and Iteration)
- การเข้าถึงด้วยคีย์/ดัชนี: การค้นหาในดิกชันนารี 21.9ns, การตรวจสมาชิกในเซต 19ns, การเข้าถึงดัชนีในลิสต์ 17.6ns
- การตรวจสมาชิกในลิสต์ (1,000 รายการ) ใช้เวลา 3.85μs ซึ่งช้ากว่าเซต/ดิกชันนารีประมาณ 200 เท่า
- การตรวจความยาว:
len() สำหรับลิสต์ 18.8ns, ดิกชันนารี 17.6ns, เซต 18ns
- การวนซ้ำ: ลิสต์ (1,000 รายการ) 7.87μs, ดิกชันนารี 8.74μs,
sum() 1.87μs
คลาสและแอตทริบิวต์ (Class and Object Attributes)
- ความเร็วในการเข้าถึงแอตทริบิวต์: ทั้งคลาสทั่วไปและคลาส
__slots__ อ่านได้ที่ 14.1ns, เขียนประมาณ 16ns
- การทำงานอื่น ๆ: การอ่าน
@property 19ns, getattr() 13.8ns, hasattr() 23.8ns
- เมื่อใช้
__slots__ จะได้ ผลในการประหยัดหน่วยความจำมากกว่า 2 เท่า โดยความเร็วในการเข้าถึงยังอยู่ในระดับเดียวกัน
JSON และการซีเรียลไลซ์ (JSON and Serialization)
- ประสิทธิภาพของไลบรารีทางเลือกเทียบกับไลบรารีมาตรฐาน
orjson ซีเรียลไลซ์ออบเจ็กต์ซับซ้อนได้ใน 310ns ซึ่งเร็วกว่า json ที่ใช้ 2.65μs มากกว่า 8 เท่า
msgspec อยู่ที่ 445ns, ujson อยู่ที่ 1.64μs
- ในการ ดีซีเรียลไลซ์
orjson ก็เร็วที่สุดที่ 839ns
- Pydantic:
model_dump_json() 1.54μs, model_validate_json() 2.99μs
เว็บเฟรมเวิร์ก (Web Frameworks)
- สำหรับการตอบกลับ JSON แบบเดียวกัน: FastAPI 8.63μs, Starlette 8.01μs, Litestar 8.19μs, Flask 16.5μs, Django 18.1μs
- FastAPI ตอบสนองได้เร็วกว่า Django ราว 2 เท่า
การอ่านเขียนไฟล์ (File I/O)
- การเปิดและปิดไฟล์ 9.05μs, การอ่าน 1KB 10μs, การอ่าน 1MB 33.6μs
- การเขียน: 1KB 35.1μs, 1MB 207μs
- Pickle เร็วกว่า
json ทั้งในการซีเรียลไลซ์และดีซีเรียลไลซ์ประมาณ 2 เท่า (pickle.dumps() 1.3μs, json.dumps() 2.72μs)
ฐานข้อมูลและการคงอยู่ของข้อมูล (Database and Persistence)
- SQLite: insert 192μs, select 3.57μs, update 5.22μs
- diskcache: set 23.9μs, get 4.25μs
- MongoDB: insert 119μs, find_one 121μs
- SQLite เร็วที่สุดในด้านการอ่าน และ diskcache เด่นในด้านประสิทธิภาพการเขียน
การเรียกฟังก์ชันและข้อยกเว้น (Function and Call Overhead)
- การเรียกฟังก์ชัน: ฟังก์ชันว่าง 22.4ns, เมธอด 23.3ns, lambda 19.7ns
- การจัดการข้อยกเว้น: try/except (กรณีปกติ) 21.5ns, เมื่อเกิดข้อยกเว้น 139ns
- การตรวจชนิด:
isinstance() 18.3ns, การเทียบ type() 21.8ns
โอเวอร์เฮดของอะซิงก์ (Async Overhead)
- การสร้างคอรูทีน 47ns,
run_until_complete 27.6μs
asyncio.sleep(0) 39.4μs, gather(10 coroutines) 55μs
- เมื่อเทียบกับ การเรียกฟังก์ชันแบบซิงก์ (20ns) แล้ว การรันแบบอะซิงก์ (28μs) ช้ากว่าประมาณ 1,000 เท่า
บทเรียนสำคัญ (Key Takeaways)
- โอเวอร์เฮดหน่วยความจำของออบเจ็กต์ Python มีขนาดใหญ่ แม้แต่ลิสต์ว่างก็ใช้ 56 ไบต์
- การค้นหาในดิกชันนารีและเซต เร็วกว่าการค้นหาในลิสต์หลายร้อยเท่า
- ไลบรารี JSON ทางเลือกอย่าง
orjson, msgspec เร็วกว่ามาตรฐาน 3–8 เท่า
- การประมวลผลแบบอะซิงก์ มีโอเวอร์เฮดสูง จึงควรใช้เมื่อจำเป็นต้องมีการทำงานพร้อมกันจริง ๆ เท่านั้น
__slots__ ลดการใช้หน่วยความจำได้เหลือไม่ถึงครึ่ง โดยแทบไม่มีผลเสียด้านประสิทธิภาพ
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
หลายคนบอกว่า “ถ้าคุณต้องแคร์ตัวเลข latency ใน Python ก็ควรไปใช้ภาษาอื่น” แต่ฉันไม่เห็นด้วย
โค้ดเบสขนาดใหญ่ของบริษัทอย่าง Instagram, Dropbox, OpenAI ก็เติบโตมาด้วย Python เหมือนกัน สุดท้ายก็ต้องเจอปัญหาเรื่องประสิทธิภาพอยู่ดี และสิ่งสำคัญคือความสามารถในการแก้ปัญหานั้นภายใน Python โดยไม่ต้องย้ายภาษา
ปัญหาด้านประสิทธิภาพส่วนใหญ่มักไม่ได้มาจากข้อจำกัดของภาษา แต่เกิดจาก โค้ดที่ไม่มีประสิทธิภาพ มากกว่า เช่น ลูปที่เรียกฟังก์ชันซ้ำโดยไม่จำเป็นเป็นหมื่นครั้ง
ลองดู Python latency quiz ที่ฉันทำไว้ได้ด้วย
ในเชิงย้อนแย้ง พอถึงจุดที่ตัวเลขพวกนี้สำคัญขึ้นมา ก็แปลว่า Python ไม่ใช่เครื่องมือที่เหมาะกับงานนั้นแล้ว
ในทางปฏิบัติ สิ่งสำคัญคือการใส่ instrumentation ให้โค้ดและหา bottleneck ให้เจอ (ด้วยเครื่องมืออย่าง pyspy) ถ้าคุณกังวลถึงระดับความเร็วในการเพิ่มสมาชิกลง list งานนั้นก็คงไม่ควรทำใน Python อยู่แล้ว
แนวทางแบบนี้เป็นไปได้เพราะ การทำงานร่วมกันระหว่าง Python กับ C และ Zig ก็ดีขึ้นเรื่อย ๆ ฉันคงไม่ใช้ Python ควบคุมเครื่องบิน แต่การมีสำนึกเรื่องทรัพยากรก็ยังสำคัญอยู่ดี
การรู้ว่า string ว่างกินพื้นที่กี่ไบต์แทบไม่มีความหมาย สิ่งสำคัญคือเข้าใจ time·space complexity
การรู้ว่า int มีขนาด 28 ไบต์ ไม่สำคัญเท่ากับการตัดสินได้ว่าโปรแกรมของคุณผ่านข้อกำหนดด้านประสิทธิภาพหรือไม่ และถ้าไม่ผ่านก็ควรหาอัลกอริทึมที่ดีกว่า
ตัวอย่างเช่น การที่ string concatenation เป็น O(n²) ก็มีผลต่อการออกแบบ f-string ของ Python
หรือการที่ dictionary เร็ว จึงถูกใช้แพร่หลายทั่ว Python ก็เป็นบริบทเดียวกัน
ตัวเลขเหล่านี้มีหน้าที่ ทำให้ความรู้โดยนัยนั้นมีเหตุผลรองรับด้วยตัวเลข
ทำให้นึกถึงบทความ ที่พูดถึงปัญหาที่ Eric Raymond เจอระหว่างใช้ Reposurgeon เพื่อย้าย GCC
ชื่อเรื่องทำให้สับสนอยู่หน่อย เพราะจริง ๆ แล้วเป็นการล้อชื่อบทความของ Jeff Dean ปี 2012 ที่ชื่อ “Latency Numbers Every Programmer Should Know”
มุกเล่นกับชื่อแบบนี้พบได้บ่อยในงานวิจัยสาย CS
มันเป็นเอกสารภายในสำหรับออกแบบ RAM vs Disk ของเสิร์ชเอนจินยุคแรกของ Google
ต่อมาพอมี flash memory ตัวเลขก็เปลี่ยนไป และยังมีเกร็ดว่า Jeff เคยทำ อัลกอริทึมบีบอัด เพื่อเสิร์ฟข้อมูลจีโนมจากแฟลชโดยตรงด้วย
นักพัฒนา Python ส่วนใหญ่น่าจะควรโฟกัสกับสิ่งที่สำคัญกว่า รายละเอียดประสิทธิภาพระดับต่ำ เหล่านี้
ข้อมูลแบบนี้เหมาะจะมีไว้อ้างอิง แต่ในทางปฏิบัติแทบไม่ค่อยจำเป็น
คำอธิบายเรื่องขนาดของ string ไม่ถูกต้อง Python มี string อยู่สามชนิด ที่ใช้ 1, 2, 4 ไบต์ต่ออักขระ
ดูรายละเอียดได้ในบล็อกนี้
ชื่อเรื่องกับตัวอย่างค่อนข้างไม่แม่นยำอยู่บ้าง
เช่น “item in set เร็วกว่า item in list 200 เท่า” นั้นพูดถึง membership test ไม่ใช่การเปรียบเทียบความเร็วของการวนซ้ำ (iteration)
ถึงอย่างนั้น โดยรวมแล้ว รูปแบบและการจัดวางก็น่าสนใจ
ไม่มีการวัดเวลาในการสร้างอินสแตนซ์ของคลาส
ฉันเคยรีแฟกเตอร์โค้ดแล้วเปลี่ยนโครงสร้าง list ธรรมดาเป็นคลาส ปรากฏว่าเวลาการทำงานเพิ่มจาก ไม่กี่ไมโครวินาที → หลายวินาที
อยากให้มีการวัดกรณีแบบนี้ด้วย
ปัญหาอาจเป็นการใช้คลาสมากเกินไปก็ได้ บางครั้งโครงสร้าง list ธรรมดาอาจเหมาะกว่า
กลับกันอาจเป็นเพราะใช้แนวคิดเชิงวัตถุ ผิดวิธี มากกว่า
ลองเอาโค้ดไปลง StackOverflow หรือ CodeReview.SE เพื่อขอฟีดแบ็กน่าจะดีกว่า
ฉันอ่านบทความนี้ด้วยมุมมองว่า “Python ยุคใหม่มีอะไรผิดพลาดในระดับพื้นฐานหรือเปล่า” ซึ่งก็น่าสนใจดี
แต่ฉันไม่เห็นด้วยกับคำอ้างว่าทุกคน “ควรรู้ทั้งหมด” แค่รู้สึกคุ้นกับการทำงานหลัก ๆ สักไม่กี่อย่างก็พอแล้ว
ช่วงของ small int caching ใน Python ไม่ใช่ 0~256 แต่เป็น -5~256
เรื่องนี้ทำให้มือใหม่สับสนบ่อย เพราะมักแยกไม่ออกระหว่าง identity (
is) กับ equality (==)