Đường dẫn để đánh máy 4 triệu dòng mã Python. Phần 2

Hôm nay chúng tôi sẽ xuất bản phần thứ hai của bản dịch tài liệu về cách tổ chức kiểm soát kiểu của Dropbox đối với hàng triệu dòng mã Python.

Đường dẫn để đánh máy 4 triệu dòng mã Python. Phần 2

Đọc phần một

Hỗ trợ loại chính thức (PEP 484)

Chúng tôi đã tiến hành các thử nghiệm nghiêm túc đầu tiên với mypy tại Dropbox trong Hack Week 2014. Hack Week là sự kiện kéo dài một tuần do Dropbox tổ chức. Trong thời gian này, nhân viên có thể làm bất cứ điều gì họ muốn! Một số dự án công nghệ nổi tiếng nhất của Dropbox đã bắt đầu tại những sự kiện như thế này. Kết quả của thử nghiệm này là chúng tôi kết luận rằng mypy có vẻ đầy hứa hẹn, mặc dù dự án vẫn chưa sẵn sàng để sử dụng rộng rãi.

Vào thời điểm đó, ý tưởng tiêu chuẩn hóa các hệ thống gợi ý kiểu Python đang được triển khai. Như tôi đã nói, kể từ Python 3.0, người ta có thể sử dụng chú thích kiểu cho các hàm, nhưng đây chỉ là những biểu thức tùy ý, không có cú pháp và ngữ nghĩa xác định. Trong quá trình thực hiện chương trình, phần lớn các chú thích này bị bỏ qua. Sau Hack Week, chúng tôi bắt đầu chuẩn hóa ngữ nghĩa. Công việc này đã dẫn đến sự xuất hiện PP 484 (Guido van Rossum, Łukasz Langa và tôi đã cộng tác thực hiện tài liệu này).

Động cơ của chúng tôi có thể được nhìn từ hai phía. Đầu tiên, chúng tôi hy vọng rằng toàn bộ hệ sinh thái Python có thể áp dụng một cách tiếp cận chung để sử dụng gợi ý kiểu (một thuật ngữ được sử dụng trong Python tương đương với "chú thích kiểu"). Điều này, xét đến những rủi ro có thể xảy ra, sẽ tốt hơn so với việc sử dụng nhiều phương pháp tiếp cận không tương thích lẫn nhau. Thứ hai, chúng tôi muốn thảo luận cởi mở về cơ chế chú thích kiểu với nhiều thành viên của cộng đồng Python. Mong muốn này một phần được quyết định bởi thực tế là chúng tôi không muốn trông giống như những “kẻ bội đạo” về những ý tưởng cơ bản của ngôn ngữ trong mắt đông đảo các lập trình viên Python. Đó là một ngôn ngữ được gõ động, được gọi là "gõ vịt". Trong cộng đồng, ngay từ đầu, không thể không nảy sinh thái độ có phần nghi ngờ đối với ý tưởng gõ tĩnh. Nhưng tình cảm đó cuối cùng đã suy yếu sau khi rõ ràng rằng việc gõ tĩnh sẽ không bắt buộc (và sau khi mọi người nhận ra rằng nó thực sự hữu ích).

Cú pháp gợi ý loại cuối cùng đã được áp dụng rất giống với những gì mypy hỗ trợ vào thời điểm đó. PEP 484 được phát hành cùng với Python 3.5 vào năm 2015. Python không còn là ngôn ngữ được gõ động nữa. Tôi thích coi sự kiện này như một cột mốc quan trọng trong lịch sử Python.

Bắt đầu di cư

Vào cuối năm 2015, Dropbox đã thành lập một nhóm gồm ba người để làm việc trên mypy. Họ bao gồm Guido van Rossum, Greg Price và David Fisher. Kể từ lúc đó, tình hình bắt đầu phát triển cực kỳ nhanh chóng. Trở ngại đầu tiên cho sự phát triển của mypy là hiệu suất. Như tôi đã gợi ý ở trên, trong những ngày đầu của dự án, tôi đã nghĩ đến việc dịch triển khai mypy sang C, nhưng hiện tại ý tưởng này đã bị loại khỏi danh sách. Chúng tôi gặp khó khăn khi chạy hệ thống bằng trình thông dịch CPython, trình thông dịch này không đủ nhanh đối với các công cụ như mypy. (Dự án PyPy, một triển khai Python thay thế bằng trình biên dịch JIT, cũng không giúp được gì cho chúng tôi.)

May mắn thay, một số cải tiến về thuật toán đã hỗ trợ chúng tôi ở đây. “Công cụ tăng tốc” mạnh mẽ đầu tiên là việc thực hiện kiểm tra gia tăng. Ý tưởng đằng sau sự cải tiến này rất đơn giản: nếu tất cả các phần phụ thuộc của mô-đun không thay đổi kể từ lần chạy mypy trước đó, thì chúng tôi có thể sử dụng dữ liệu được lưu trong bộ nhớ đệm trong lần chạy trước trong khi làm việc với các phần phụ thuộc. Chúng tôi chỉ cần thực hiện kiểm tra kiểu trên các tệp đã sửa đổi và trên các tệp phụ thuộc vào chúng. Mypy thậm chí còn đi xa hơn một chút: nếu giao diện bên ngoài của một mô-đun không thay đổi, mypy cho rằng các mô-đun khác đã nhập mô-đun này không cần phải kiểm tra lại.

Kiểm tra gia tăng đã giúp chúng tôi rất nhiều khi chú thích số lượng lớn mã hiện có. Vấn đề là quá trình này thường bao gồm nhiều lần chạy mypy lặp đi lặp lại khi các chú thích được thêm dần vào mã và dần dần được cải thiện. Lần chạy đầu tiên của mypy vẫn rất chậm vì nó có rất nhiều phần phụ thuộc cần kiểm tra. Sau đó, để cải thiện tình hình, chúng tôi đã triển khai cơ chế lưu vào bộ nhớ đệm từ xa. Nếu mypy phát hiện bộ đệm cục bộ có thể đã lỗi thời, nó sẽ tải xuống ảnh chụp nhanh bộ đệm hiện tại cho toàn bộ cơ sở mã từ kho lưu trữ tập trung. Sau đó, nó thực hiện kiểm tra gia tăng bằng cách sử dụng ảnh chụp nhanh này. Điều này đã đưa chúng tôi thêm một bước tiến lớn trong việc tăng hiệu suất của mypy.

Đây là thời kỳ áp dụng kiểm tra loại nhanh chóng và tự nhiên tại Dropbox. Đến cuối năm 2016, chúng tôi đã có khoảng 420000 dòng mã Python có chú thích kiểu. Nhiều người dùng rất nhiệt tình với việc kiểm tra kiểu. Ngày càng có nhiều nhóm phát triển sử dụng Dropbox mypy.

Lúc đó mọi thứ có vẻ tốt, nhưng chúng tôi vẫn còn nhiều việc phải làm. Chúng tôi bắt đầu thực hiện khảo sát người dùng nội bộ định kỳ để xác định các khu vực có vấn đề của dự án và hiểu những vấn đề nào cần được giải quyết trước (phương pháp này vẫn được sử dụng trong công ty cho đến ngày nay). Điều quan trọng nhất, như đã rõ ràng, là hai nhiệm vụ. Đầu tiên, chúng tôi cần phạm vi loại mã nhiều hơn, thứ hai, chúng tôi cần mypy hoạt động nhanh hơn. Rõ ràng là công việc của chúng tôi nhằm tăng tốc mypy và triển khai nó vào các dự án của công ty vẫn chưa hoàn thành. Chúng tôi, nhận thức đầy đủ về tầm quan trọng của hai nhiệm vụ này, bắt tay vào giải quyết chúng.

Năng suất cao hơn!

Kiểm tra tăng dần làm cho mypy nhanh hơn, nhưng công cụ này vẫn chưa đủ nhanh. Nhiều lần kiểm tra gia tăng kéo dài khoảng một phút. Lý do cho điều này là nhập khẩu theo chu kỳ. Điều này có lẽ sẽ không gây ngạc nhiên cho những ai đã từng làm việc với các cơ sở mã lớn được viết bằng Python. Chúng tôi có hàng trăm mô-đun, mỗi mô-đun được nhập gián tiếp vào tất cả các mô-đun khác. Nếu bất kỳ tệp nào trong vòng lặp nhập bị thay đổi, mypy phải xử lý tất cả các tệp trong vòng lặp đó và thường là bất kỳ mô-đun nào đã nhập mô-đun từ vòng lặp đó. Một chu kỳ như vậy là “mớ rắc rối phụ thuộc” khét tiếng đã gây ra rất nhiều rắc rối tại Dropbox. Cấu trúc này từng chứa hàng trăm mô-đun, mặc dù được nhập trực tiếp hoặc gián tiếp, nhiều thử nghiệm, nhưng nó cũng được sử dụng trong mã sản xuất.

Chúng tôi đã xem xét khả năng "gỡ rối" sự phụ thuộc vòng tròn, nhưng chúng tôi không có đủ nguồn lực để làm điều đó. Có quá nhiều mã mà chúng tôi không quen thuộc. Kết quả là chúng tôi đã nghĩ ra một cách tiếp cận khác. Chúng tôi quyết định làm cho mypy hoạt động nhanh chóng ngay cả khi có “các vấn đề phụ thuộc”. Chúng tôi đã đạt được mục tiêu này bằng cách sử dụng daemon mypy. Daemon là một tiến trình máy chủ thực hiện hai tính năng thú vị. Đầu tiên, nó lưu trữ thông tin về toàn bộ codebase trong bộ nhớ. Điều này có nghĩa là mỗi khi chạy mypy, bạn không phải tải dữ liệu được lưu trong bộ nhớ đệm liên quan đến hàng nghìn phần phụ thuộc đã được nhập. Thứ hai, ông cẩn thận phân tích sự phụ thuộc giữa các chức năng và các thực thể khác ở cấp độ đơn vị cấu trúc nhỏ. Ví dụ, nếu hàm foo gọi một hàm bar, khi đó có sự phụ thuộc foo từ bar. Khi một tập tin thay đổi, daemon trước tiên sẽ cô lập chỉ xử lý tập tin đã thay đổi. Sau đó, nó xem xét các thay đổi có thể nhìn thấy bên ngoài đối với tệp đó, chẳng hạn như chữ ký hàm đã thay đổi. Trình nền chỉ sử dụng thông tin chi tiết về việc nhập để kiểm tra kỹ các chức năng thực sự sử dụng chức năng đã sửa đổi. Thông thường, với phương pháp này, bạn phải kiểm tra rất ít chức năng.

Việc thực hiện tất cả những điều này không hề dễ dàng, vì việc triển khai mypy ban đầu tập trung nhiều vào việc xử lý từng tệp một. Chúng tôi đã phải đối mặt với nhiều tình huống ranh giới, việc xảy ra chúng đòi hỏi phải kiểm tra nhiều lần trong trường hợp có gì đó thay đổi trong mã. Ví dụ, điều này xảy ra khi một lớp được gán một lớp cơ sở mới. Sau khi thực hiện những gì mình muốn, chúng tôi có thể giảm thời gian thực hiện hầu hết các bước kiểm tra gia tăng xuống chỉ còn vài giây. Đây dường như là một chiến thắng lớn đối với chúng tôi.

Năng suất thậm chí còn cao hơn!

Cùng với bộ nhớ đệm từ xa mà tôi đã thảo luận ở trên, daemon mypy gần như đã giải quyết hoàn toàn các vấn đề nảy sinh khi lập trình viên thường xuyên chạy kiểm tra kiểu, thực hiện các thay đổi đối với một số lượng nhỏ tệp. Tuy nhiên, hiệu suất hệ thống trong trường hợp sử dụng ít thuận lợi nhất vẫn chưa đạt mức tối ưu. Quá trình khởi động sạch mypy có thể mất hơn 15 phút. Và điều này còn hơn cả những gì chúng tôi mong đợi. Mỗi tuần tình hình lại trở nên tồi tệ hơn khi các lập trình viên tiếp tục viết mã mới và thêm chú thích vào mã hiện có. Người dùng của chúng tôi vẫn mong muốn có được hiệu suất cao hơn nhưng chúng tôi rất vui khi được đáp ứng được nửa chặng đường của họ.

Chúng tôi quyết định quay lại một trong những ý tưởng trước đó liên quan đến mypy. Cụ thể là chuyển đổi mã Python thành mã C. Việc thử nghiệm với Cython (một hệ thống cho phép bạn dịch mã viết bằng Python sang mã C) không mang lại cho chúng tôi bất kỳ sự tăng tốc rõ rệt nào, vì vậy chúng tôi quyết định làm sống lại ý tưởng viết trình biên dịch của riêng mình. Vì cơ sở mã mypy (được viết bằng Python) đã chứa tất cả các chú thích kiểu cần thiết nên chúng tôi nghĩ rằng sẽ đáng để thử sử dụng các chú thích này để tăng tốc hệ thống. Tôi nhanh chóng tạo ra một nguyên mẫu để thử nghiệm ý tưởng này. Nó cho thấy hiệu suất tăng hơn 10 lần trên các tiêu chuẩn vi mô khác nhau. Ý tưởng của chúng tôi là biên dịch các mô-đun Python thành các mô-đun C bằng cách sử dụng Cython và biến chú thích kiểu thành kiểm tra kiểu thời gian chạy (thường các chú thích kiểu bị bỏ qua trong thời gian chạy và chỉ được sử dụng bởi các hệ thống kiểm tra kiểu). Chúng tôi thực sự đã lên kế hoạch dịch việc triển khai mypy từ Python sang một ngôn ngữ được thiết kế để gõ tĩnh, ngôn ngữ này sẽ trông (và phần lớn, hoạt động) giống hệt Python. (Loại di chuyển đa ngôn ngữ này đã trở thành một truyền thống của dự án mypy. Việc triển khai mypy ban đầu được viết bằng Alore, sau đó có sự kết hợp cú pháp giữa Java và Python).

Tập trung vào API tiện ích mở rộng CPython là chìa khóa để không làm mất khả năng quản lý dự án. Chúng tôi không cần triển khai máy ảo hoặc bất kỳ thư viện nào mà mypy cần. Ngoài ra, chúng tôi vẫn có quyền truy cập vào toàn bộ hệ sinh thái Python và tất cả các công cụ (chẳng hạn như pytest). Điều này có nghĩa là chúng tôi có thể tiếp tục sử dụng mã Python được thông dịch trong quá trình phát triển, cho phép chúng tôi tiếp tục làm việc với mô hình thực hiện thay đổi và kiểm tra mã rất nhanh, thay vì chờ mã biên dịch. Có thể nói, có vẻ như chúng tôi đã làm rất tốt việc ngồi trên hai chiếc ghế và chúng tôi yêu thích điều đó.

Trình biên dịch mà chúng tôi gọi là mypyc (vì nó sử dụng mypy làm giao diện người dùng để phân tích các loại), hóa ra lại là một dự án rất thành công. Nhìn chung, chúng tôi đã đạt được tốc độ tăng gấp 4 lần khi chạy mypy thường xuyên mà không cần lưu vào bộ nhớ đệm. Việc phát triển cốt lõi của dự án mypyc mất khoảng 4 tháng theo lịch với một nhóm nhỏ gồm Michael Sullivan, Ivan Levkivsky, Hugh Hahn và tôi. Lượng công việc này nhỏ hơn nhiều so với những gì cần thiết để viết lại mypy, chẳng hạn như trong C++ hoặc Go. Và chúng tôi phải thực hiện ít thay đổi hơn đối với dự án so với những gì chúng tôi phải thực hiện khi viết lại nó bằng ngôn ngữ khác. Chúng tôi cũng hy vọng rằng chúng tôi có thể đưa mypyc lên một tầm cao mới để các lập trình viên Dropbox khác có thể sử dụng nó để biên dịch và tăng tốc mã của họ.

Để đạt được mức hiệu suất này, chúng tôi đã phải áp dụng một số giải pháp kỹ thuật thú vị. Do đó, trình biên dịch có thể tăng tốc nhiều thao tác bằng cách sử dụng các cấu trúc C cấp thấp, nhanh. Ví dụ: một lệnh gọi hàm đã biên dịch sẽ được dịch thành lệnh gọi hàm C. Và cuộc gọi như vậy nhanh hơn nhiều so với việc gọi một hàm được thông dịch. Một số thao tác, chẳng hạn như tra cứu từ điển, vẫn liên quan đến việc sử dụng các lệnh gọi C-API thông thường từ CPython, chỉ nhanh hơn một chút khi biên dịch. Chúng tôi có thể loại bỏ tải bổ sung lên hệ thống do phiên dịch tạo ra, nhưng trong trường hợp này, điều này chỉ mang lại lợi ích nhỏ về mặt hiệu suất.

Để xác định các thao tác “chậm” phổ biến nhất, chúng tôi đã thực hiện lập hồ sơ mã. Được trang bị dữ liệu này, chúng tôi đã cố gắng điều chỉnh mypyc để nó tạo mã C nhanh hơn cho các hoạt động như vậy hoặc viết lại mã Python tương ứng bằng các hoạt động nhanh hơn (và đôi khi chúng tôi đơn giản là không có giải pháp đủ đơn giản cho vấn đề đó hoặc vấn đề khác) . Viết lại mã Python thường là một giải pháp dễ dàng hơn cho vấn đề này so với việc để trình biên dịch tự động thực hiện cùng một phép biến đổi. Về lâu dài, chúng tôi muốn tự động hóa nhiều chuyển đổi này, nhưng tại thời điểm đó, chúng tôi đang tập trung vào việc tăng tốc mypy với nỗ lực tối thiểu. Và để hướng tới mục tiêu này, chúng tôi đã cắt giảm một số bước.

Để được tiếp tục ...

Gởi bạn đọc! Ấn tượng của bạn về dự án mypy là gì khi biết đến sự tồn tại của nó?

Đường dẫn để đánh máy 4 triệu dòng mã Python. Phần 2
Đường dẫn để đánh máy 4 triệu dòng mã Python. Phần 2

Nguồn: www.habr.com

Thêm một lời nhận xét