- ตัวจัดการแพ็กเกจ Python uv มีความเร็วในการติดตั้ง เร็วกว่า pip มากกว่า 10 เท่า และไม่ได้เร็วเพียงเพราะเขียนด้วย Rust แต่เกิดจาก การเลือกเชิงออกแบบ
- หัวใจสำคัญที่ทำให้เร็วคือ มาตรฐานเมทาดาทาแบบคงที่ (PEP 518, 517, 621, 658) ซึ่งทำให้สามารถทราบ dependency ได้โดยไม่ต้องรันโค้ด
- uv ตัด ฟีเจอร์แบบ legacy (.egg, pip.conf, การติดตั้งลงระบบ Python ฯลฯ) ที่ pip ยังรองรับออกอย่างเด็ดขาด เพื่อ กำจัด code path ที่ไม่จำเป็น
- ส่วนที่ Rust ช่วยได้คือ zero-copy deserialization, lock-free concurrency, โครงสร้างแบบ single binary เป็นต้น ซึ่งเป็นเพียงส่วนหนึ่งของการเพิ่มความเร็วทั้งหมด
- โดยรวมแล้ว กรณีของ uv แสดงให้เห็นว่า เมทาดาทาที่เป็นมาตรฐานและการตัดความเข้ากันได้ที่ไม่จำเป็นออก คือหัวใจของนวัตกรรมด้านประสิทธิภาพ
มาตรฐานที่ทำให้ uv เร็วได้
- ความช้าของ pip ไม่ได้เป็นปัญหาจาก implementation แต่เกิดจากโครงสร้างแบบ setup.py-based ในอดีต ที่ทำให้ต้องรันโค้ดเพื่อจะรู้ dependency
- การรัน
setup.py ต้องติดตั้ง build dependency ก่อน ซึ่งก่อให้เกิด “ปัญหาไก่กับไข่”
- ในกระบวนการติดตั้งอาจเกิด การรันโค้ดตามอำเภอใจและความล้มเหลวซ้ำ ๆ ทำให้การติดตั้งช้าลง
- PEP 518 (2016) นำ
pyproject.toml เข้ามา ทำให้ประกาศ build dependency ได้โดยไม่ต้องรันโค้ด
- PEP 517 (2017) แยก build frontend กับ backend ออกจากกัน ทำให้ pip ไม่จำเป็นต้องเข้าใจภายในของ setuptools
- PEP 621 (2020) ทำให้ตาราง
[project] เป็นมาตรฐาน จึงตรวจสอบ dependency ได้ด้วยการ parse TOML เท่านั้น
- PEP 658 (2022) ใส่เมทาดาทาของแพ็กเกจไว้ใน Simple Repository API โดยตรง ทำให้ดึงข้อมูล dependency ได้โดยไม่ต้องดาวน์โหลด wheel
- PyPI เริ่มใช้ PEP 658 ในเดือนพฤษภาคม 2023 และ uv เปิดตัวในเดือนกุมภาพันธ์ 2024 จึงกลายเป็น เครื่องมือตัวแรกที่ใช้ประโยชน์จากโครงสร้างมาตรฐานใหม่นี้ได้อย่างเต็มที่
- เช่นเดียวกับ Cargo ของ Rust หรือ npm ระบบนิเวศ Python ก็กำลังเปลี่ยนผ่านสู่ แพ็กเกจจิงที่อิงเมทาดาทาแบบคงที่
ฟีเจอร์ที่ uv ตัดออก
- ความเร็วของ uv มาจาก การตัดฟีเจอร์ที่ไม่จำเป็นออก
- ไม่รองรับ .egg: pip ยังจัดการอยู่ แต่ uv ตัดออกทั้งหมด
- ไม่สนใจ pip.conf: ตัดทั้งไฟล์ตั้งค่า ตัวแปรสภาพแวดล้อม และตรรกะการสืบทอดค่าออกไป
- ปิดการคอมไพล์ bytecode: ไม่แปลง
.py เป็น .pyc จึงลดเวลาในการติดตั้ง
- บังคับใช้ virtual environment: ไม่ติดตั้งลง system Python โดยตรง จึงไม่ต้องมีโค้ดตรวจสอบสิทธิ์และความปลอดภัยส่วนนี้
- ปฏิบัติตามสเปกอย่างเข้มงวด: ปฏิเสธแพ็กเกจที่ไม่ถูกต้อง เพื่อลดตรรกะการจัดการข้อยกเว้น
- เมิน upper bound ของ requires-python: ละเลยข้อจำกัดเชิงป้องกันอย่าง
python<4.0 เพื่อลด การแก้ dependency (backtracking)
- ให้ความสำคัญกับ index แรก: หากพบแพ็กเกจในหลาย index จะหยุดที่ตัวแรกทันที ช่วย ป้องกันการโจมตีแบบ dependency confusion และลดการร้องขอเครือข่าย
- ทั้งหมดนี้คือตัวอย่างของ code path ที่ pip ต้องมี แต่ uv เลือกตัดทิ้ง
การปรับแต่งที่ทำได้แม้ไม่มี Rust
- ความเร็วของ uv ส่วนใหญ่เกิดจาก การปรับแต่งด้านการออกแบบที่ไม่ขึ้นกับภาษา
- ใช้ HTTP Range request เพื่อดาวน์โหลดเฉพาะ central directory ของไฟล์ wheel บางส่วน แทนการดาวน์โหลดทั้งไฟล์
- ใช้ การดาวน์โหลดแบบขนาน เพื่อดึงหลายแพ็กเกจพร้อมกัน
- ใช้ global cache และ hardlink ทำให้แม้ติดตั้งแพ็กเกจเดียวกันในหลาย virtual environment ก็ ไม่ใช้พื้นที่ดิสก์เพิ่ม
- การแก้ dependency โดยไม่พึ่ง Python: parse TOML และเมทาดาทาของ wheel โดยตรง และจะรัน Python เฉพาะกรณีที่มีแค่
setup.py เท่านั้น
- ใช้อัลกอริทึมแก้ dependency แบบ PubGrub ซึ่งเร็วกว่าแนวทาง backtracking ของ pip และอธิบายข้อผิดพลาดได้ชัดเจนกว่า
ส่วนที่ Rust มีส่วนช่วยจริง
- Rust มีบทบาทสำคัญใน การปรับแต่งระดับล่าง บางส่วน
- ใช้ zero-copy deserialization บนพื้นฐานของ rkyv เพื่อใช้งานข้อมูลในแคชได้โดยตรงโดยไม่ต้องคัดลอก
- ใช้ โครงสร้างข้อมูลแบบ lock-free concurrency เพื่อให้เข้าถึงแบบขนานได้อย่างปลอดภัย
- ไม่มีค่าใช้จ่ายในการเริ่มต้น interpreter: uv เป็น static binary เดี่ยว จึงตัดต้นทุนการสร้าง Python process ของ pip ออกไป
- บีบอัดข้อมูลเวอร์ชันเป็นจำนวนเต็ม u64 ทำให้การเปรียบเทียบและแฮชทำได้เร็วขึ้น
- องค์ประกอบเหล่านี้ช่วยเพิ่มประสิทธิภาพ แต่ ไม่ใช่สาเหตุหลักของความเร็วที่เพิ่มขึ้นทั้งหมด
บทเรียนสำคัญ
- ความเร็วของ uv ไม่ได้มาจาก Rust แต่มาจากสิ่งที่มันเลือกจะไม่ทำ
- มาตรฐาน PEP 518·517·621·658 วางรากฐานให้ การจัดการแพ็กเกจที่รวดเร็ว และ uv ก็นำสิ่งนี้ไปทำให้เกิดขึ้นจริงด้วย การตัด legacy ออกและตั้งสมมติฐานแบบสมัยใหม่
- pip ก็สามารถทำ parallel download, global cache และการแก้ dependency แบบอิงเมทาดาทาได้ แต่ การต้องรักษา backward compatibility มา 15 ปี คืออุปสรรคสำคัญ
- ผลลัพธ์คือ pip จึงแทบเลี่ยงไม่ได้ที่จะช้า และ มีเพียงเครื่องมือที่เริ่มต้นจากสมมติฐานใหม่เท่านั้นที่สามารถเพิ่มความเร็วได้อย่างถึงราก
- บทเรียนสำหรับตัวจัดการแพ็กเกจอื่น ๆ คือ เมทาดาทาแบบคงที่, การสำรวจ dependency โดยไม่รันโค้ด, และโครงสร้างที่แก้ dependency ล่วงหน้าได้ เป็นสิ่งจำเป็น
- Cargo และ npm ใช้แนวทางนี้อยู่แล้ว และ ระบบนิเวศที่ต้องรันโค้ดเพื่อตรวจสอบ dependency นั้นช้าโดยพื้นฐาน
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
คิดว่าบทความนี้อธิบาย ประสิทธิภาพของ uv ได้ดีจากหลายมุม
การที่เขียนด้วย Rust ก็ช่วยอยู่ แต่สิ่งที่มีบทบาทมากกว่ามากคือ ความพยายามในการทำให้เป็นมาตรฐาน ตลอด 10 ปีที่ผ่านมา ที่ทำให้ ecosystem ของ Python หลุดพ้นจากการพึ่งพา
setup.pyRust เองก็เลือกใช้ได้ด้วยเหตุผลคล้ายกัน คือช่วยยกระดับศักยภาพของชุมชน
สามารถย้อนดูบทเรียนจากความผิดพลาดเดิม ๆ แล้วออกแบบใหม่ให้ดีขึ้น พร้อมได้ข้อดีของ Rust เพิ่มมา กลายเป็นหมัดชุดแบบ ‘one-two punch’
เห็นด้วยกับข้อสรุปที่ว่า “uv ไม่ได้เร็วเพราะเป็น Rust แต่เร็วเพราะมี สิ่งที่ไม่ต้องทำ เยอะกว่า”
แต่ก็ยังมองว่าเร็วเกินไปที่จะฟันธงปัจจัยเรื่องความเร็วโดยไม่มี benchmark
PEP 518, 517, 621, 658 มีผลมากแน่ แต่ก็ยังสงสัยว่า การตัดความเข้ากันได้ย้อนหลัง มีส่วนแค่ไหน
อีกทั้งยังไม่ได้พูดถึงว่าการเลือกภาษามีผลต่อการ optimize อย่างไรบ้าง
เรื่องที่ TOML parser ของ Cargo เร็วกว่า Python มากก็น่าสนใจ
จริง ๆ แล้ว TOML ถูกอ่านเฉพาะตอน build จึงไม่ได้มีสัดส่วนมากนัก แต่การ แพร่หลายของ wheel มีส่วนช่วยให้เร็วขึ้น
บทความอ้างอิงที่เกี่ยวข้อง: setup.py deprecated, wheels are faster
การทำ zero-copy deserialization ด้วย
rkyvไม่ใช่เทคนิคที่มีเฉพาะ Rustภาษาระดับล่างอย่าง C/C++ ก็ทำได้เหมือนกัน
ประเด็น “ไม่มีการเริ่มต้น interpreter” ก็อยู่ในบริบทเดียวกัน
เนื้อหาของบทความดี แต่ สำนวนที่ถูกขัดเกลาด้วย LLM ให้ความรู้สึกประดิษฐ์เกินไป
สักวันหนึ่งอาจจะมียุคที่เราต้องคอยกู้บทความที่โดน LLM ทำให้เสียทรงกลับมาให้เป็นมนุษย์อีกครั้ง
ดูเป็นผู้เชี่ยวชาญด้าน supply chain security แต่บทความนั้นก็ยังให้ความรู้สึกว่าโดนสำนวน คลุมเครือแบบ LLM กลบไปเหมือนกัน
เพราะ prompt แบบตายตัวทำให้ทุกบทความคล้ายกันไปหมด จนน่าเสียดายที่อินเทอร์เน็ตเหมือนพูดด้วยเสียงเดียวกันทั้งหมด
ไม่ค่อยเข้าใจบรรยากาศที่ตื่นเต้นกับความเร็วของ uv กันมากขนาดนี้
ผู้ใช้ Python ส่วนใหญ่น่าจะไม่ได้จัดความเร็วในการติดตั้งแพ็กเกจไว้แม้แต่ใน 10 เรื่องที่กังวลที่สุด
ผมเองก็ใช้ Python ทุกวัน แต่ไม่ได้รู้สึกมากนัก
poetryใช้เวลา 5–30 นาทีถ้าล้มเหลวก็ต้องรออีก 30 นาที และ uv ให้ประสบการณ์ที่ ลื่นไหลน่าใช้มาก จริง ๆ
pip installกินเวลาส่วนใหญ่ของการ deploy อยู่บ่อยครั้งใช้เวลาไปมากกับการพยายามทำ caching เพื่อเร่งความเร็ว
poetry installใช้ 2 นาที แต่uv syncเสร็จในไม่กี่วินาทีประหยัดได้ครั้งละ 2 นาทีในทุก CI พอสะสมแล้วมีผลมาก
uvx sometoolก็ใช้เวลาแค่ไม่กี่วินาทีในการ สร้าง virtual environment และติดตั้ง dependency จน workflow เปลี่ยนไปเลยตอนนี้กลับไปทำโปรเจกต์ที่ไม่มี uv ได้ยากแล้ว
เทคนิคการปรับแต่งความเร็ว บางอย่างของ uv ดูเหมือนจะย้ายไปใส่ใน pip ได้เหมือนกัน
เช่น การดาวน์โหลดแบบขนาน, การสร้าง
.pycแบบหน่วงเวลา, การไม่สนใจ egg, การเช็กเวอร์ชัน เป็นต้นแต่ uv จัดการ venv ได้ดีมากจนดูเหมือนไม่มีเหตุผลนักที่จะไปปรับ pip แล้ว
สุดท้ายประเด็นก็คือ “ไม่ได้เร็วเพราะ Rust อย่างเดียว” ดังนั้น pip เองก็ยังมีช่องให้เร็วขึ้นได้
โปรแกรมเมอร์ที่เลือกใช้ภาษาที่เร็ว มักจะมี mindset เรื่อง performance optimization อยู่แล้ว
ทัศนคติแบบนั้นต่างหากที่กำหนดประสิทธิภาพได้มากกว่าตัวภาษาเอง
น่าสนใจที่ uv เพิกเฉยต่อเพดาน
python<4.0แพ็กเกจส่วนใหญ่ตั้งค่านี้แบบ ป้องกันไว้ก่อน เพราะกลัวว่าจะพังกับ Python 4 ทั้งที่ในความเป็นจริงอาจไม่มีปัญหา
การตั้งเพดานแบบนี้จึงเป็นความพยายามแก้ปัญหาสมมุติมากกว่าปัญหาที่เกิดขึ้นจริง
ข้อจำกัดอย่าง
python<3.0ยังสำคัญอยู่ ดังนั้นกรณีแบบนั้นควรถูกบล็อกไว้การที่ PEP 658 ถูกนำมาใช้ในปี 2023 และ uv โผล่มาในปี 2024 ไม่ใช่เรื่องบังเอิญ
เพราะ ecosystem พร้อมแล้ว เครื่องมืออย่าง uv จึงเกิดขึ้นได้
แต่ก็ยังสงสัยว่าทำไมผู้ดูแลแพ็กเกจถึงยอมรับการเปลี่ยนแปลงนี้
คนที่ใช้
setup.pyได้ดีอยู่แล้วก็น่าจะมี แล้วอะไรคือแรงจูงใจให้ย้ายไปpyproject.tomlsetup.pyสร้างความลำบากให้หลายคนยกตัวอย่างเช่นแม้แต่ Requests ก็ยังไม่เข้ากันกับ PEP 517/518/621 อย่างสมบูรณ์
minor release ถูกเลื่อนมาแล้วปีครึ่ง และระหว่างนั้นก็เกิด ปัญหาในการ build ด้วย
แต่ก็ยังสงสัยว่าเหตุใด pip ถึงยังใช้ประโยชน์จากสิ่งนี้ได้ไม่เต็มที่
ประโยคที่ว่า “เส้นทางโค้ดที่ไม่ต้องทำ ก็ไม่ต้องรอ” นั้นไม่ค่อยแม่นนัก
มีเพียงโค้ดที่ไม่ถูกรันเท่านั้นที่ช่วยประหยัดเวลาได้
ตัวอย่างเช่น ต่อให้ไม่มีการรองรับ
.eggถ้ามันเป็น ฟอร์แมตที่เลิกใช้ไปแล้ว ก็อาจไม่กระทบความเร็วอยู่ดีถ้ามี ข้อมูลเชิงปริมาณ ว่าแต่ละอย่างช่วยประหยัดเวลาไปเท่าไรจริงก็คงจะดีกว่านี้
ถึงอย่างนั้น โดยรวมก็ยังเป็นบทความที่เรียบเรียงมาได้ดี