Vấn đề của triển khai thay thế (Alternative Implementation Problem)
(pointersgonewild.com)- Các triển khai thay thế như runtime ngôn ngữ hoặc JIT, dù có hiệu năng tốt hơn, vẫn phải liên tục theo kịp thay đổi của triển khai chuẩn và kỳ vọng của người dùng, nên việc được chấp nhận có thể bị hạn chế
- PyPy, LuaJIT và TruffleRuby cho thấy hiệu năng thực thi rất nhanh, nhưng khoảng cách tương thích và gánh nặng bám theo các tính năng mới đã trở thành rào cản khi triển khai thực tế
- YJIT chọn con đường không phải là một triển khai Ruby riêng biệt mà là đi vào bên trong CRuby, được xây dựng để tương thích 100% với các tính năng của CRuby ngay từ đầu, và đã được triển khai tại Shopify, Discourse, GitHub...
- Lựa chọn như Crystal, rất giống ngôn ngữ hiện có nhưng không tương thích, có thể khiến người dùng liên tục phải đối mặt với sự khác biệt kiểu “gần như Ruby nhưng không phải Ruby”
- Ở những lĩnh vực có tiêu chuẩn công khai tách rời khỏi trình triển khai như trình phân tích JSON hay JavaScript, gánh nặng của triển khai thay thế giảm đi; nhưng trong các hệ sinh thái nơi triển khai chuẩn trên thực tế chính là tiêu chuẩn, cần một chiến lược khác
Cái bẫy lặp đi lặp lại của triển khai thay thế
- Trong thế giới phần mềm, nhiều dự án khởi đầu như một triển khai thay thế tốt hơn cho hệ thống hiện có rồi lại nhiều lần bị mắc kẹt dưới cái bóng của triển khai chuẩn
- Triển khai thay thế luôn bị so sánh với triển khai chuẩn (canonical implementation), thứ được xem như tiêu chuẩn về tính năng, hiệu năng, hệ sinh thái và kỳ vọng người dùng
- Khi triển khai chuẩn liên tục thay đổi, triển khai thay thế phải tiêu tốn rất nhiều năng lượng để chạy theo thay vì tự quyết định hướng đi của mình
- Khi gắn một triển khai JIT vào một ngôn ngữ vốn theo truyền thống là interpreter, các tính năng mới thường dễ đi vào interpreter nhanh hơn, khiến gánh nặng bám theo phía JIT lớn hơn
Mẫu hình thấy ở PyPy, LuaJIT và TruffleRuby
- PyPy là một trình biên dịch JIT tiên tiến cho Python, có thể mang lại cải thiện tốc độ lớn so với CPython, nhưng mức sử dụng thực tế lại rất thấp
- Python là một mục tiêu luôn di chuyển, với các phiên bản và tính năng CPython mới xuất hiện đều đặn
- PyPy gặp khó trong việc theo kịp, nên luôn ở trạng thái chậm hơn Python vài phiên bản
- Để phần mềm Python tương thích với PyPy, các tính năng Python có thể dùng sẽ bị hạn chế, và đa số lập trình viên Python không muốn bận tâm đến điều đó
- LuaJIT mang lại cải thiện hiệu năng lớn so với triển khai Lua kiểu interpreter mặc định, và nhận được đánh giá rất cao cùng một mức độ chấp nhận thực tế nhất định
- Tác giả LuaJIT, Mike Pall, được nhiều người xem là một lập trình viên xuất sắc
- Khi ngôn ngữ Lua tiếp tục thêm tính năng mới, LuaJIT cũng bị chậm lại nhiều phiên bản
- Một số người dùng Lua vì thế ngần ngại dùng LuaJIT
- Lua nổi tiếng là ngôn ngữ theo chủ nghĩa tối giản, nhưng đã không có nỗ lực nào để làm chậm việc bổ sung tính năng mới hoặc phối hợp với Mike Pall
- TruffleRuby đưa ra những con số hiệu năng ấn tượng nhất trong các JIT cho Ruby, nhưng việc triển khai vẫn bị giới hạn
- Một lý do thực tế là thời gian làm nóng của TruffleRuby dài hơn rất nhiều so với CRuby
- Khi CRuby tiếp tục bổ sung tính năng, các cộng tác viên TruffleRuby cũng phải nỗ lực để theo kịp
- Người dùng Ruby xem CRuby là triển khai chuẩn, và cho rằng một triển khai không hoàn toàn tương thích thì ít đáng để cân nhắc
Con đường khác mà YJIT chọn
- YJIT khởi đầu là một JIT Ruby khác, nhưng được xây dựng ngay bên trong bản thân CRuby chứ không phải như một triển khai riêng
- Lựa chọn này tạo ra nhiều đánh đổi trong thiết kế, nhưng giúp YJIT có thể tương thích 100% với mọi tính năng của CRuby ngay từ đầu
- Hiện nay YJIT là JIT “chính thức” của Ruby và đã được triển khai tại Shopify, Discourse, GitHub...
- Người dùng truy cập github.com hay các cửa hàng Shopify thực tế đã tương tác với YJIT
- Tính đến nay, YJIT là trình biên dịch JIT cho Ruby thành công nhất, và tính tương thích đóng vai trò cốt lõi trong thành công đó
Chỉ “không thắng được thì gia nhập” vẫn là chưa đủ
- Nếu tự định vị mình là một triển khai thay thế, bạn rất dễ rơi vào trò chơi đuổi theo mãi trong cái bóng của triển khai chuẩn
- Khi dự án chuẩn tiếp tục tiến hóa, triển khai thay thế chỉ còn quyền quyết định rất hạn chế với hướng đi của chính dự án mình và phải đi theo phía sau
- Gia nhập vào triển khai chuẩn có thể mang lại kết quả tốt hơn, nhưng chừng đó vẫn không giải quyết được mọi trường hợp
- Crystal trong hệ sinh thái Ruby là một ngôn ngữ biên dịch tĩnh với cú pháp giống Ruby và dùng suy luận kiểu
- Crystal cố ý không nhắm đến tương thích Ruby mà tách nhánh khỏi Ruby
- Với các Rubyist, nó trông như một ngôn ngữ “gần như Ruby nhưng không hoàn toàn là Ruby”, và trên thực tế có nhiều khác biệt tinh vi cùng các điểm không tương thích
- Sự tương đồng này làm lung lay kỳ vọng của người dùng và tạo ra sự bối rối
- Crystal có lẽ đã đạt kết quả tốt hơn nếu ngay từ đầu không được tiếp thị là giống Ruby
Lựa chọn tránh cạnh tranh và có hướng đi riêng
- Câu nói của Peter Thiel “competition is for losers” được dùng trong ngữ cảnh đừng tự đặt mình vào vị thế phải cạnh tranh một cách không cần thiết
- Từ đó dẫn đến lời khuyên rằng nếu tạo ra một ngôn ngữ lập trình mới, tốt hơn là đừng cố làm ra một tập con của Python hay thứ gì đó bề ngoài quá giống một ngôn ngữ hiện có
- Nếu xây dựng thứ của riêng mình, bạn có thể phát triển hệ thống theo tốc độ và phương hướng riêng mà không bị trói buộc bởi kỳ vọng phải bắt kịp hiệu năng, tập tính năng hay hệ sinh thái thư viện của một triển khai khác
- Lời khuyên này áp dụng trong những tình huống mà ngôn ngữ hay hệ thống có triển khai chuẩn
- Các lĩnh vực có tiêu chuẩn công khai có thể là ngoại lệ
- Trình phân tích JSON có một đặc tả rõ ràng, tương đối nhỏ và không thay đổi quá nhanh, nên có thể tự triển khai
- JavaScript có nhiều triển khai dựa trên trình duyệt cùng tồn tại, điều này khả thi vì có một tổ chức tiêu chuẩn bên ngoài quản lý đặc tả JS
- Những người làm tiêu chuẩn JS hiểu rằng các triển khai biên dịch JIT rất quan trọng với hiệu năng, và dẫn dắt sự tiến hóa của ngôn ngữ theo hướng đó
- Họ không tham gia vào trò chơi thêm thật nhiều tính năng mới càng nhanh càng tốt
1 bình luận
Ý kiến trên Hacker News
Còn một điểm quan trọng nữa mà OP đã bỏ lỡ. Khi tạo một cách triển khai thay thế, kiến trúc thường khác với triển khai tham chiếu, và những việc dễ ở triển khai gốc có thể lại rất khó trong triển khai của mình
Ví dụ, giả sử một phần mềm độc quyền dùng cho báo cáo tài chính lưu tài liệu ở một định dạng nhị phân kỳ quặc. Khi làm một lựa chọn miễn phí thay thế, bạn chọn kiến trúc đọc toàn bộ tài liệu vào bộ nhớ và khi lưu thì ghi lại toàn bộ tệp, còn bản gốc được tạo ra từ thời RAM còn ít nên chỉ đọc và ghi phần người dùng đang làm việc, thậm chí có thể chỉnh sửa tại chỗ
Sau đó nếu bản gốc thêm tính năng đính kèm tệp vào tài liệu, thì các tệp lớn như bản ghi âm cuộc gọi với nhà đầu tư hay PDF quét hàng trăm trang vẫn hoạt động tốt nhờ nạp theo từng phần. Trong khi đó, triển khai của tôi giải tuần tự hóa toàn bộ tài liệu, nên sẽ gặp vấn đề ngay khi tài liệu lớn hơn RAM của người dùng, và một thay đổi mà ở bản gốc chỉ cần một lập trình viên làm trong một tuần có thể buộc phải thiết kế lại toàn bộ phần mềm
Ngược lại, cũng có những tính năng khó hơn ở bản gốc nhưng lại thành chuyện vặt ở triển khai thứ hai. Chỉ có một lần phải tái thiết kế lớn, và đó là ở triển khai gốc vốn dựa trên một giả định dẫn tới bùng nổ theo cấp số nhân
Một ví dụ công khai là bài toán chạy ứng dụng Windows trên Linux. Nhân Linux là một triển khai hoàn toàn khác với NT và cũng không nhắm tới tương thích, nhưng không cần phải thiết kế lại toàn bộ kernel để chạy ứng dụng Windows. Chỉ cần một vài chức năng kernel khá phổ biến và một lớp tương thích ở user space. Wine tốn rất nhiều công sức để viết và bảo trì, nhưng vẫn ít hơn rất nhiều so với chính triển khai Windows, và nó chạy trên một nền tảng vốn không hề hướng tới tương thích Windows. Tuy vậy, như bài viết nói, nó vẫn phải liên tục đuổi theo Windows, và còn xuất hiện cả mã phụ thuộc vào lỗi của triển khai tham chiếu, nên để tương thích cả lỗi, trước hết phải tìm ra cần triển khai những lỗi nào
Vì thế bất kỳ đoạn mã nào cũng có thể sửa thứ khác trong lúc chạy. Trên thực tế điều này không được dùng quá nhiều, nhưng hễ muốn bỏ đi là mọi người phản đối dữ dội. Một triển khai thực sự biên dịch Python phải xử lý cả tình huống một thread bất ngờ thay đổi thứ gì đó bên dưới thread khác
Đó là một cách để công ty nhỏ làm gánh nặng của công ty lớn ngày càng nặng hơn, và các công ty ít bỏ mặc technical debt cũng có thể làm vậy. Đây cũng là một trong số hiếm hoi những lúc có thể cho ban lãnh đạo thấy rõ technical debt. Vì bạn có thể nói: “Mất nhiều thời gian hơn Acme để triển khai tính năng này”
Tôi đồng ý với nhận định “đừng cố tạo ra một tập con của Python”. Những dự án được tiếp thị kiểu “vẫn là Python nhưng X tốt hơn” luôn khó cạnh tranh với triển khai tham chiếu, đặc biệt nếu X là tốc độ. Người dùng ngôn ngữ kiểu động thường rốt cuộc không quá quan tâm đến tốc độ chạy
Tuy nhiên, triển khai thay thế không phải lúc nào cũng thất bại. MicroPython có vẻ khá thành công dù gần như không hỗ trợ vượt quá mức Python 3.4. Đó là vì nó được thiết kế để chạy trên vi điều khiển, nên không cạnh tranh với CPython mà cạnh tranh với các môi trường lập trình vi điều khiển khác
Dù vậy, có lẽ những người bảo trì MicroPython nhận rất nhiều yêu cầu về các tính năng Python mới hơn. Tôi từng nghĩ tới một triển khai Python thay thế, nhẹ, tập trung vào nhúng cho ứng dụng, và trong trường hợp đó ý tưởng cũng là cạnh tranh với Lua chứ không phải CPython. Nhưng yêu cầu tính năng số một lại là “Có hỗ trợ NumPy không?”
Đồng thời, phía CPython dường như cũng đang có những nỗ lực để chạy tốt hơn ở frontend, và điểm đau cốt lõi là kích thước gói
Tôi đã học được điều tương tự khi xây startup. Nếu làm lại, tôi sẽ chủ động tránh những tính năng vé vào cửa cơ bản trong lĩnh vực của chúng tôi
Thay vào đó, tôi chỉ nên làm mức tối thiểu đủ để tạo niềm tin rằng kiến trúc của chúng tôi có thể hỗ trợ các yêu cầu kiểu enterprise, rồi dồn toàn bộ sức lực vào những điểm khác biệt khiến người ta nghĩ “à, tôi thấy được thứ này có thể đi xa đến đâu”, chứ không phải những tính năng chỉ gợi ra phản ứng kiểu “à, chỉ là bản sao của X thôi mà”
Chiến lược bạn mô tả gần với việc triển khai các tính năng nền tảng bắt buộc, nhưng không đi quá sâu vào các tính năng mở rộng “thông thường” sau đó. Tức là làm đủ để có người liên hệ lại vì các tính năng thú vị, đồng thời vẫn có bộ tính năng cơ bản đủ để không bị loại vì thiếu thứ bắt buộc
Tôi cũng có cảm giác tương tự với mọi mã bọc. Đôi khi có người nói “chúng ta cần một phiên bản nội bộ của API này”
Lý do thì nhiều, nhưng thường kiểu như “không thể tin rằng người ta sẽ dùng đúng API chính thức”. Có thể đúng, nhưng phiên bản nội bộ thì lại kém tiêu chuẩn hơn và tài liệu cũng tệ hơn
Đôi khi lý do là “cần thêm tính năng”, nhưng nếu vậy thì không cần bọc cả API, chỉ cần thêm 3 hàm là được. Theo thời gian, 99% codebase có thể biến thành polyfill
Ý chính là nếu không dùng mặc định thì sau này sẽ gây khổ lớn cho người tiếp quản codebase
Ví dụ, thư viện ziggy-pydust để viết module native Python bằng Zig rõ ràng dễ nhìn hơn so với cách import Python.h thông thường. Dù vậy, nó vẫn có
.ffiđể truy cập trực tiếp các hàm có trong Python.h nhưng chưa được triển khai, dù tính năng này còn chưa hoàn thiệnNếu không có lựa chọn như vậy thì thường người ta sẽ bỏ thư viện đó và quay về dùng bản gốc. Tuy nhiên theo một nghĩa nào đó đây cũng là một wrapper, tức một module native, và để tăng tốc phát triển thì đôi khi dùng trực tiếp ctypes lại tốt hơn
Đây là một bài viết hay và có nhiều bài học rất tốt, nhưng vẫn thiếu một thành phần cốt lõi. Điều này cũng giống như các lựa chọn thay thế cạnh tranh cho một sản phẩm nào đó
Nó giống như nói Amazon thất bại vì không có các hiệu sách vật lý quen thuộc với mọi người, nhưng thực tế không phải vậy
Lý do những lựa chọn thay thế JIT kiểu này thất bại và chỉ mãi chạy theo là vì trên thực tế, phần lớn lập trình viên ngôn ngữ X không coi JIT quan trọng đến thế. Chính xác hơn, họ coi tính năng ngôn ngữ và khả năng tương tác quan trọng hơn JIT
Vì vậy, sản phẩm “tham gia cùng” thay vì cạnh tranh sẽ thắng. Đó là vì nó không thể mang lại độ ổn định cao hơn hay khả năng tương tác tốt hơn
Tôi đã làm việc với ngôn ngữ và trình biên dịch trong thời gian dài, và thấy bài này rất thú vị. Nói theo cách khác thì ngôn ngữ còn là nhiều thứ hơn rất nhiều so với chỉ tốc độ biên dịch
Tốc độ biên dịch rất quan trọng, chắc chắn nằm trong top 10 khía cạnh hàng đầu. Đặc biệt, tăng tốc độ biên dịch sẽ làm vòng phản hồi của lập trình viên nhanh hơn, và đội ngũ nòng cốt cũng có thể cải thiện mọi khía cạnh khác nhanh hơn
Nhưng ngay cả vậy, vẫn còn hơn 30 khía cạnh cực kỳ quan trọng khác của ngôn ngữ lập trình
Kết luận về cách để các dự án kiểu này thành công là rất hay, nhưng vẫn còn một yếu tố chưa được nói đủ về việc vì sao nhiều dự án không thể cất cánh: tính tương thích của các triển khai thay thế trên thực tế thường thấp hơn những gì họ tuyên bố, và điều đó đúng cả với các tính năng ngôn ngữ cũ
Ví dụ, ứng dụng Ruby và Python rất thường có extension C native ở đâu đó trong chuỗi phụ thuộc, và theo tôi biết thì các triển khai thay thế lớn chưa bao giờ hỗ trợ được điều này. Họ đã từng thử, nhưng vì những lý do kỹ thuật rõ ràng mà không thành công, còn phương án trông đợi thư viện cung cấp nhiều triển khai thì lịch sử cũng không mấy suôn sẻ
Cộng thêm việc các ngôn ngữ này thường được dùng cho những website CRUD nơi I/O mới là yếu tố hiệu năng lớn hơn CPU, thì sức hấp dẫn của một lựa chọn thay thế nhanh hơn càng giảm đi nhiều
Bài viết thực sự rất xuất sắc. Xã hội học của công nghệ thật sự rất thú vị
Những người duy trì triển khai ngôn ngữ muốn có mức linh hoạt tối đa khi thiết kế và phát hành các tính năng mới có lợi cho người dùng. Họ không muốn bị cản chân vì phải đạt đồng thuận giữa nhiều triển khai trước khi tung ra tính năng. Ví dụ điển hình là tốc độ phát triển của JavaScript từng có cảm giác chậm như băng trôi trong nhiều năm, trong khi TypeScript lại tiến hóa tương đối nhanh
Đồng thời, các triển khai thay thế cũng có mặt lợi, vì chúng có thể là tín hiệu cho thấy hệ sinh thái ngôn ngữ đủ vững mạnh. Nếu một triển khai thay thế thực sự tốt, thì ngay cả khi chỉ phù hợp với một nhóm nhu cầu ngách nào đó, nó vẫn có thể mang lại giá trị thực cho hệ sinh thái
Vì vậy, nếu là nhà thiết kế hay người bảo trì ngôn ngữ, bạn có lẽ sẽ không chủ động thù địch với các triển khai thay thế, nhưng chúng vẫn có mặt trái. Nhìn chung, phản hồi từ người dùng thường sẽ theo hướng hãy ra tính năng mới và tiếp tục phát triển ngôn ngữ. Sẽ không có nhiều người yêu cầu bạn chậm lại để PyPy, IronRuby hay LuaJIT có thể theo kịp
Khi người dùng ngôn ngữ chọn xem sẽ xây dựng trên triển khai nào, ưu tiên lớn nhất thường là độ an toàn và tính ổn định. Không ai muốn một codebase hàng triệu dòng của mình phụ thuộc vào các đặc tính hành vi tinh vi của một triển khai thay thế từng được tạo ra bởi một nghiên cứu sinh cực kỳ thông minh nhưng nay đã chuyển sang dự án khác. Vì thế, người dùng sẽ đổ về triển khai được dùng nhiều nhất, và điều đó lại tiếp tục hút thêm người dùng khác, tạo nên một vòng phản hồi tích cực mạnh mẽ
Kết quả là, trừ khi có một lực rất mạnh đẩy theo hướng ngược lại, phần lớn ngôn ngữ sẽ hội tụ về một triển khai chuẩn duy nhất. Cũng có thể lập luận rằng đây là điều tốt. Gần như toàn bộ nỗ lực kỹ thuật dành cho triển khai ngôn ngữ sẽ không bị chia nhỏ cho nhiều triển khai mà mang lại lợi ích cho mọi người dùng. Dĩ nhiên, nhược điểm là triển khai đó có thể bị mắc kẹt ở một điểm tối ưu cục bộ
LuaJIT có được nhắc đến, nhưng nó cũng là một ví dụ cho thấy mọi chuyện không phải lúc nào cũng đi theo kết luận của bài viết. Nhiều người và nhiều dự án đã cố ý chọn LuaJIT thay vì Lua
luajittexcũng có thể dễ dàng tìm thấy song song vớiluatexCó thể đây là một ý không mấy phổ biến, nhưng mọi người thỉnh thoảng nên xem lại cái tôi của mình, kể cả chính bản thân mình. Tạo một dự án song song là “của mình” có thể dễ hơn là đóng góp cho một dự án mã nguồn mở đã có, nhưng cần tự hỏi là đang làm điều đó vì ai
Vì người bảo trì dự án, vì bản thân dự án, vì người dùng, hay vì cái tôi của chính mình. Nếu bạn thấy bực với mục cuối cùng, thì có lẽ nó khá liên quan đấy
Việc thêm JIT vào một ngôn ngữ hiện có là công việc lớn, nên tiêu chuẩn để bản triển khai tham chiếu chấp nhận nó chắc chắn sẽ rất cao. Dù vậy, tôi vẫn nghĩ mục tiêu nên là hướng đó. Fork hoặc làm mới từ đầu cũng cho bạn tự do để làm việc lớn, nhưng thường chỉ nên được xem là giải pháp tạm thời
Nếu mục tiêu là chứng minh tôi có thể làm được gì, thì có lẽ sẽ không đi được xa. Nếu mục tiêu là tạo ra thứ gì đó tốt hơn, bạn sẽ học được cách làm việc trong những ràng buộc của người khác
Ngạc nhiên là tôi lại nghĩ ngược lại. Chính những người rao giảng rằng chỉ nên có một dự án duy nhất, chỉ đóng góp cho dự án chuẩn, và không nên có bản triển khai thay thế mới là những người cần xem lại cái tôi của mình, đồng thời học về sự tồn tại của nhiều cách tiếp cận và vì sao cạnh tranh tốt hơn độc quyền
Tôi cho rằng bài viết này cho thấy Python, Lua và Ruby đã chọn cách tiếp cận như vậy và làm nhiều người thất vọng. Kết quả là hàng nghìn nhà phát triển và hàng triệu người dùng phải chấp nhận quá trình phát triển chậm hơn và phần mềm chậm hơn. Không phải vì điều đó là bất khả thi, mà vì về mặt hành chính không có động lực để làm như vậy
Ừm, cũng có những trường hợp bản triển khai chính thức thật sự quá tệ, nên bạn buộc phải tạo khu vườn nhỏ của riêng mình. Dù nó có lớn dần thành 20 mẫu Anh cùng cả kế hoạch luân canh đi nữa, thì đôi khi vẫn tốt hơn núi lửa