พาธไปยังการพิมพ์โค้ด Python 4 ล้านบรรทัด ส่วนที่ 2

วันนี้เรากำลังเผยแพร่ส่วนที่สองของการแปลเนื้อหาเกี่ยวกับวิธีที่ Dropbox จัดระเบียบการควบคุมประเภทสำหรับโค้ด Python หลายล้านบรรทัด

พาธไปยังการพิมพ์โค้ด Python 4 ล้านบรรทัด ส่วนที่ 2

อ่านส่วนแรก

การสนับสนุนประเภทอย่างเป็นทางการ (PEP 484)

เราทำการทดลองอย่างจริงจังครั้งแรกกับ mypy ที่ Dropbox ในช่วง Hack Week 2014 Hack Week เป็นกิจกรรมหนึ่งสัปดาห์ที่จัดโดย Dropbox ในช่วงเวลานี้ พนักงานสามารถทำงานอะไรก็ได้ที่พวกเขาต้องการ! โครงการเทคโนโลยีที่มีชื่อเสียงที่สุดของ Dropbox บางโครงการเริ่มต้นจากกิจกรรมลักษณะนี้ จากการทดลองนี้ เราสรุปได้ว่า mypy ดูมีแนวโน้มดี แม้ว่าโครงการยังไม่พร้อมสำหรับการใช้งานอย่างแพร่หลายก็ตาม

ในเวลานั้น แนวคิดในการกำหนดมาตรฐานระบบคำใบ้ประเภท Python อยู่ในอากาศ อย่างที่ฉันบอกไปแล้ว เนื่องจาก Python 3.0 มันเป็นไปได้ที่จะใช้คำอธิบายประกอบประเภทสำหรับฟังก์ชัน แต่สิ่งเหล่านี้เป็นเพียงการแสดงออกตามอำเภอใจ โดยไม่มีการกำหนดไวยากรณ์และความหมาย ในระหว่างการทำงานของโปรแกรม คำอธิบายประกอบเหล่านี้โดยส่วนใหญ่แล้วจะถูกละเลยไป หลังจาก Hack Week เราก็เริ่มทำงานกับการกำหนดมาตรฐานความหมาย งานนี้นำไปสู่การเกิดขึ้น สพป.484 (Guido van Rossum, Łukasz Langa และฉันร่วมมือกันในเอกสารนี้)

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

ไวยากรณ์คำใบ้ประเภทที่ถูกนำมาใช้ในที่สุดนั้นคล้ายคลึงกับสิ่งที่ mypy รองรับในขณะนั้นมาก PEP 484 เปิดตัวพร้อมกับ Python 3.5 ในปี 2015 Python ไม่ใช่ภาษาที่พิมพ์แบบไดนามิกอีกต่อไป ฉันชอบคิดว่าเหตุการณ์นี้เป็นเหตุการณ์สำคัญในประวัติศาสตร์ของ Python

จุดเริ่มต้นของการโยกย้าย

เมื่อปลายปี 2015 Dropbox ได้สร้างทีมงานสามคนเพื่อทำงานกับ mypy รวมถึงกุยโด ฟาน รอสซุม, เกร็ก ไพรซ์ และเดวิด ฟิชเชอร์ ตั้งแต่นั้นเป็นต้นมา สถานการณ์ก็เริ่มพัฒนาอย่างรวดเร็วมาก อุปสรรคแรกในการเติบโตของ mypy คือประสิทธิภาพการทำงาน ตามที่ผมบอกไปข้างต้น ในช่วงแรกๆ ของโปรเจ็กต์ ผมคิดที่จะแปลการใช้งาน mypy เป็นภาษา C แต่ตอนนี้แนวคิดนี้ถูกตัดออกจากรายการไปแล้ว เราติดอยู่กับการรันระบบโดยใช้ล่าม CPython ซึ่งไม่เร็วพอสำหรับเครื่องมืออย่าง mypy (โปรเจ็กต์ PyPy ซึ่งเป็นการนำ Python ทางเลือกไปใช้ด้วยคอมไพเลอร์ JIT ไม่ได้ช่วยเราเช่นกัน)

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

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

นี่เป็นช่วงเวลาแห่งการนำการตรวจสอบประเภทมาใช้อย่างรวดเร็วและเป็นธรรมชาติที่ Dropbox ภายในสิ้นปี 2016 เรามีโค้ด Python พร้อมคำอธิบายประกอบประเภทแล้วประมาณ 420000 บรรทัด ผู้ใช้หลายคนกระตือรือร้นที่จะตรวจสอบประเภท ทีมพัฒนาจำนวนมากขึ้นเรื่อยๆ ใช้ Dropbox mypy

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

ผลผลิตเพิ่มมากขึ้น!

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

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

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

ผลผลิตเพิ่มมากขึ้น!

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

เราตัดสินใจกลับไปสู่แนวคิดก่อนหน้านี้เกี่ยวกับ mypy กล่าวคือการแปลงโค้ด Python เป็นโค้ด C การทดลองกับ Cython (ระบบที่ให้คุณแปลโค้ดที่เขียนด้วย Python เป็นโค้ด C) ไม่ได้ทำให้เรามองเห็นการเร่งความเร็วได้ ดังนั้นเราจึงตัดสินใจรื้อฟื้นแนวคิดในการเขียนคอมไพเลอร์ของเราเอง เนื่องจากโค้ดเบส mypy (เขียนด้วยภาษา Python) มีคำอธิบายประกอบประเภทที่จำเป็นทั้งหมดอยู่แล้ว เราจึงคิดว่าคงจะคุ้มค่าที่จะลองใช้คำอธิบายประกอบเหล่านี้เพื่อเร่งความเร็วของระบบ ฉันรีบสร้างต้นแบบเพื่อทดสอบแนวคิดนี้ แสดงให้เห็นประสิทธิภาพที่เพิ่มขึ้นมากกว่า 10 เท่าบนไมโครเบนช์มาร์กต่างๆ แนวคิดของเราคือการรวบรวมโมดูล Python ให้เป็นโมดูล C โดยใช้ Cython และเปลี่ยนคำอธิบายประกอบประเภทเป็นการตรวจสอบประเภทรันไทม์ (โดยปกติคำอธิบายประกอบประเภทจะถูกละเว้น ณ รันไทม์และใช้โดยระบบการตรวจสอบประเภทเท่านั้น) จริงๆ แล้วเราวางแผนที่จะแปลการใช้งาน mypy จาก Python เป็นภาษาที่ได้รับการออกแบบมาให้พิมพ์แบบคงที่ ซึ่งจะมีลักษณะ (และโดยส่วนใหญ่จะใช้งานได้) เหมือนกับ Python ทุกประการ (การโยกย้ายข้ามภาษาประเภทนี้ได้กลายเป็นประเพณีของโปรเจ็กต์ mypy การใช้งาน mypy ดั้งเดิมนั้นเขียนด้วย Alore จากนั้นก็มีลูกผสมทางวากยสัมพันธ์ของ Java และ Python)

การมุ่งเน้นไปที่ API ส่วนขยาย CPython เป็นกุญแจสำคัญในการไม่สูญเสียความสามารถในการจัดการโครงการ เราไม่จำเป็นต้องใช้เครื่องเสมือนหรือไลบรารีใดๆ ที่ mypy ต้องการ นอกจากนี้ เรายังคงสามารถเข้าถึงระบบนิเวศ Python ทั้งหมดและเครื่องมือทั้งหมด (เช่น pytest) ซึ่งหมายความว่าเราสามารถใช้โค้ด Python ที่ตีความได้ต่อไปในระหว่างการพัฒนา ทำให้เราสามารถทำงานต่อไปด้วยรูปแบบที่รวดเร็วมากในการเปลี่ยนแปลงโค้ดและทดสอบ แทนที่จะรอให้โค้ดคอมไพล์ ดูเหมือนว่าเรากำลังทำหน้าที่ได้ดีมากในการนั่งบนเก้าอี้สองตัว พูดได้เลยว่าเราชอบมันมาก

คอมไพเลอร์ซึ่งเราเรียกว่า mypyc (เนื่องจากใช้ mypy เป็นส่วนหน้าสำหรับการวิเคราะห์ประเภท) กลายเป็นโปรเจ็กต์ที่ประสบความสำเร็จอย่างมาก โดยรวมแล้ว เราเร่งความเร็วได้ประมาณ 4 เท่าสำหรับการรัน mypy บ่อยครั้งโดยไม่ต้องแคช การพัฒนาแกนกลางของโครงการ mypyc ใช้ทีมงานเล็กๆ ซึ่งประกอบด้วย Michael Sullivan, Ivan Levkivsky, Hugh Hahn และตัวฉันเองใช้เวลาประมาณ 4 เดือนตามปฏิทิน งานจำนวนนี้น้อยกว่าที่จำเป็นในการเขียน mypy ใหม่มาก เช่น ใน C++ หรือ Go และเราต้องทำการเปลี่ยนแปลงโปรเจ็กต์น้อยกว่าที่เราต้องทำเมื่อเขียนใหม่ในภาษาอื่น นอกจากนี้เรายังหวังว่าเราจะสามารถนำ mypyc ไปสู่ระดับที่โปรแกรมเมอร์ Dropbox คนอื่นๆ สามารถใช้เพื่อคอมไพล์และเพิ่มความเร็วโค้ดของพวกเขาได้

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

เพื่อระบุการดำเนินการ “ช้า” ที่พบบ่อยที่สุด เราได้ดำเนินการสร้างโปรไฟล์โค้ด ด้วยข้อมูลนี้ เราพยายามปรับแต่ง mypyc เพื่อให้สร้างโค้ด C ที่เร็วขึ้นสำหรับการดำเนินการดังกล่าว หรือเขียนโค้ด Python ที่เกี่ยวข้องใหม่โดยใช้การดำเนินการที่เร็วขึ้น (และบางครั้งเราก็ไม่มีวิธีแก้ปัญหาที่ง่ายเพียงพอสำหรับปัญหานั้นหรือปัญหาอื่น ๆ ) . การเขียนโค้ด Python ใหม่มักเป็นวิธีแก้ปัญหาที่ง่ายกว่าการให้คอมไพเลอร์ทำการแปลงแบบเดียวกันโดยอัตโนมัติ ในระยะยาว เราต้องการทำให้การเปลี่ยนแปลงหลายๆ อย่างเป็นไปโดยอัตโนมัติ แต่ ณ เวลานั้น เรามุ่งเน้นไปที่การเร่งความเร็วของ Mypy โดยใช้ความพยายามเพียงเล็กน้อย และในการก้าวไปสู่เป้าหมายนี้ เราได้ตัดมุมหลายประการ

จะยังคง ...

เรียนผู้อ่าน! คุณรู้สึกอย่างไรกับโครงการ mypy เมื่อได้รู้ว่ามีอยู่จริง?

พาธไปยังการพิมพ์โค้ด Python 4 ล้านบรรทัด ส่วนที่ 2
พาธไปยังการพิมพ์โค้ด Python 4 ล้านบรรทัด ส่วนที่ 2

ที่มา: will.com

เพิ่มความคิดเห็น