2 điểm bởi GN⁺ 2023-07-13 | 1 bình luận | Chia sẻ qua WhatsApp
  • Nguyên mẫu mượn vùng của Vale lần đầu tiên biên dịch thành công, mở đường kiểm chứng một cách tiếp cận an toàn bộ nhớ kết hợp tham chiếu thế hệ và vùng trên các chương trình thực tế
  • Lập trình viên có thể viết mã theo cách gần với C/C++, rồi chỉ áp dụng pure và mượn vùng ở những nơi cần thiết để giảm overhead của kiểm tra thế hệ
  • Chương trình Vale zero-check đầu tiên là ví dụ Cellular Automata để tạo màn chơi roguelike, và assembly đầu ra đã gần như ngang với chế độ unsafe_with_bounds
  • Trong benchmark, safe_fastest không cho thấy slowdown quan sát được so với unsafe_with_bounds, còn unsafe_no_bounds nhanh hơn 1.18 ± 0.01 lần so với cả hai chế độ kia
  • Đây vẫn chưa phải so sánh trực tiếp với C/Rust, và vẫn còn việc phải làm quanh nhiễu tối ưu hóa LLVM, độ hoàn thiện của nguyên mẫu, cũng như việc chưa hỗ trợ inline data, cùng với một pre-optimizer riêng cho Vale và việc hoàn thiện tính năng vùng

Kết hợp tham chiếu thế hệ và mượn vùng

  • Cách tiếp cận an toàn bộ nhớ của Vale hướng tới việc không dùng reference counting, tracing garbage collection hay borrow checking
  • Cấu trúc cơ bản là lập trình viên viết chương trình theo phong cách gần với C hoặc C++, còn generational references của Vale sẽ duy trì tính an toàn bộ nhớ
  • Sau đó, khi áp dụng pure và region borrowing, phần lớn overhead của kiểm tra thế hệ có thể được loại bỏ
  • Nếu thêm cả linear style, Vale cho rằng có thể hạ số lần kiểm tra thế hệ trong mã Vale xuống zero
  • Mượn vùng hoàn toàn là cơ chế opt-in, nên có thể viết trước cho thoải mái rồi chỉ thêm vào những phần cần tối ưu sau này
  • Mục tiêu là trong cùng một chương trình, một số phần có thể linh hoạt như Java, một số phần nhanh như Rust, hoặc ở bất kỳ điểm nào giữa hai thái cực đó

Những gì cần làm để tạo ra nguyên mẫu

  • Trong vài năm qua, nền tảng compiler đã được xây dựng để hỗ trợ đồng thời hệ thống mượn theo vùng và generational references
  • Bản thân hệ thống mượn đã phức tạp, và còn cần full generics mạnh hơn so với template trước đây
  • Để vùng và tham chiếu thế hệ vận hành tự nhiên cùng nhau, cũng cần thêm các pha compiler mới
    • Nội bộ, vùng được rút gọn thành số nguyên “pure height”
    • Tham số generic của vùng được biểu diễn bằng số âm, vùng mặc định là 0, còn mỗi khối pure là một số dương tăng dần
  • Vài tháng trước, nguyên mẫu vùng đã hoàn thành; dù vẫn còn thô, nó đã lần đầu biên dịch thành công một thứ gì đó
  • Kết quả là chương trình Vale zero-check đầu tiên đã ra đời
    • Có thể dùng cờ compiler --print_mem_overhead true để đếm số lần kiểm tra thế hệ của chương trình

Chương trình zero-check đầu tiên và so sánh assembly

  • Chương trình đầu tiên là ví dụ Cellular Automata để tạo màn chơi cho game roguelike
  • Chỉ một sai sót nhỏ trong mã compiler cũng có thể chèn thêm lệnh vào assembly đầu ra và tạo ra overhead nhân tạo cho chương trình cuối
  • Để lần ra vấn đề, assembly đầu ra liên tục được so sánh với các chế độ unsafe của Vale
    • unsafe_no_bounds: tương tự C, tắt toàn bộ bảo vệ an toàn bộ nhớ và dùng raw pointer thay cho generational references
    • unsafe_with_bounds: giống Rust ở chỗ thêm bounds checking cho truy cập mảng
  • Sau vài tháng lần theo khác biệt, assembly đầu ra đã gần như giống hệt chế độ unsafe_with_bounds
  • Khác biệt duy nhất được dự đoán là mỗi allocation có thêm một generation number giả ngẫu nhiên ở đầu, nhưng trong kiểm tra thế hệ thực tế thì giá trị này không bị đọc
    • Nội bộ, một thanh ghi tăng đơn điệu được dùng để giữ cho việc này nhanh
    • Nếu bổ sung isolates hoặc unique references, khác biệt này có thể bị loại bỏ

Kết quả benchmark và điều kiện đo

  • Tóm tắt benchmark như sau
Summary
  './build_unsafe_no_bounds/main' ran
    1.18 ± 0.01 times faster than './build_unsafe_with_bounds/main'
    1.18 ± 0.01 times faster than './build_safe_fastest/main'
  • safe_fastest, chế độ normal của Vale, không cho thấy slowdown khi so với chế độ chỉ có bounds checking
  • Trong phép đo này, cách tiếp cận đó không có overhead quan sát được
  • Nếu muốn tự chạy thử, có thể build nhánh regions, xem các script benchmark, và đặt câu hỏi trên server Discord
  • Điều kiện đo có những giới hạn rõ ràng
    • Đây không phải benchmark so sánh trực tiếp với các ngôn ngữ như C hay Rust
    • Những compiler đó có nhiều năm tối ưu hóa riêng, có thể làm nhiễu biến số của thí nghiệm
    • Để tách riêng khác biệt trong cách tiếp cận an toàn bộ nhớ, bài đo so sánh với unsafe_no_boundsunsafe_with_bounds
    • Môi trường đo là Razer Blade 15" 2018, SSD 512GB, chạy Ubuntu 22.04
    • Công cụ đo là hyperfine và được chạy bên trong cset shield

Nhiễu tối ưu hóa bộc lộ trên các chương trình lớn hơn

  • Trên các chương trình lớn hơn, hiện tượng optimizer noise được quan sát khá rõ
    • Nó khác với benchmark noise; thiết lập đo cho ra thời gian chạy rất nhất quán, chẳng hạn ± 0.01
    • Một thay đổi nhỏ ở một vùng có thể làm kết quả đo lệch hẳn về một phía
  • Thậm chí có trường hợp khi đổi kích thước generation number lại cho ra kết quả âm overhead một cách nhất quán là 1.13 ± 0.01
    • Đây là kết quả kỳ lạ vì trong chương trình không có nhiều generation number
    • Có thể thay đổi ở register allocation đã lấn át khác biệt hiệu năng đến từ thay đổi về mặt ngữ nghĩa
  • Trong một chương trình lớn hơn là game roguelike nhỏ, optimizer không gộp được hai nhánh giống hệt nhau bên trong câu lệnh if, và cũng bỏ lỡ các tối ưu rõ ràng khác
  • Chưa rõ sự hiện diện của một integer không bị đọc ảnh hưởng thế nào, và cũng có thể đây là lỗi của LLVM
  • Kết quả này cho thấy Vale có thể cần một pre-optimizer riêng tương tự MIR của Rust
    • LLVM được thiết kế với C trong đầu nhiều hơn
    • Nếu LLVM hiểu generational references là mẫu truy cập vào vùng nhớ đã bị giải phóng một cách có chủ ý, nó có thể xem đó là undefined behavior

Khả năng áp dụng và công việc tiếp theo

  • Tham chiếu thế hệ và vùng, khi kết hợp với nhau, có thể tạo ra một cách tiếp cận an toàn bộ nhớ rất nhanh
  • Những miền phần mềm mà cách tiếp cận này có thể phù hợp thường có các đặc điểm sau
    • Muốn latency dễ dự đoán hơn so với tracing garbage collection
    • Muốn hiệu năng và tính thân thiện với cache tốt hơn so với reference counting
    • Muốn prototype và lặp nhanh dễ hơn so với borrow checking
  • Trước khi Vale có thể được so sánh trực diện với C hoặc C++, vẫn còn việc phải làm
    • LLVM optimizer hiện gặp vấn đề khi suy luận về generation và immutability, nên cần một pre-optimizer riêng cho Vale
    • Vale hiện cần hỗ trợ inline data thay cho giải pháp tạm thời là đặt mọi struct lên heap
    • Benchmark ở trên không dùng struct, nên việc chưa hỗ trợ inline data không ảnh hưởng tới kết quả này
    • Tính năng vùng vẫn đang ở giai đoạn nguyên mẫu, cần được mài giũa các phần còn thô và giảm nợ kỹ thuật trước khi merge vào nhánh main
  • Sau khi merge, kế hoạch là làm cho thư viện chuẩn dùng vùng, để ngay cả khi mã chương trình chính của người dùng không trực tiếp dùng vùng thì vẫn có thể hưởng lợi
  • Các phép đo hiện tại cho thấy chương trình zero-check là khả thi và có thể đạt được tốc độ như kỳ vọng

1 bình luận

 
GN⁺ 2023-07-13
Ý kiến trên Hacker News
  • Tôi đã tải Vale xuống để thử, nhưng ấn tượng ban đầu rất tệ khi chạy trình biên dịch valec lần đầu mà không truyền tham số thì nó lập tức in ra "(panic)"
    panic là một cách diễn đạt rất nặng, và theo tôi nên tránh trong các tình huống xử lý lỗi thông thường. Khi một chương trình báo panic, nó tạo cảm giác như có điều gì đó vượt ngoài tầm kiểm soát, nên để lại dư vị không tốt
    Sau đó tôi định xem trợ giúp tham số dòng lệnh, nhưng hiện tại gần như không có; rồi tôi lưu ví dụ Hello World trên website vào hello.vl và chạy valec hello.vl thì nhận được Unknown subcommand
    Vì vậy tôi chạy valec build hello.vl, thì sau Unrecognized input: hello.vl lại hiện tiếp (panic), còn valec help cũng không giúp ích gì nên cuối cùng tôi bỏ cuộc. Tôi không biết phải dùng cái này như thế nào

    • Xin lỗi. Có vẻ tệp trợ giúp không còn hiển thị đúng nữa. Nếu bạn cat trực tiếp valec-help-build.txt đi kèm trong bản tải xuống, bạn sẽ thấy phần giải thích nội dung mình đang tìm
      Trình biên dịch hiện vẫn còn khá thô ráp ở nhiều chỗ. Từ tháng 8 đến tháng 5, chúng tôi tập trung 100% vào việc tạo nguyên mẫu region, và những gì bạn đang gặp là khoản nợ kỹ thuật tích tụ trong quá trình đó. Bao gồm cả việc không có kiểm thử tích hợp cho hệ thống trợ giúp
      Trong 1~2 tháng qua, tôi đang trả dần khoản nợ đó, nhưng vẫn chưa quay lại được mức của bản phát hành 0.2. Nếu cần thêm trợ giúp, cứ cho tôi biết hoặc ghé vào máy chủ Discord, sẽ có nhiều người sẵn lòng giúp
    • Tôi nghĩ Vale về cơ bản vẫn đang ở giai đoạn R&D. Kiểu như chỉ có thể kỳ vọng một commit cụ thể trên một nhánh cụ thể sẽ hoạt động, chứ chưa phải giai đoạn mà bất kỳ ai tải trình biên dịch về cũng có thể xây thứ gì đó
      Tuy vậy, README không thể hiện rõ điều này mà lại ghi “Try Vale”, nên hơi mơ hồ. Dù sao thì hiện tại nó vẫn có vẻ gần với nghiên cứu/phát triển hoặc proof of concept hơn
    • Đây có vẻ gần với vấn đề giao diện người dùng hơn là bug. Nếu là thứ mang tính thử nghiệm, tôi nghĩ cũng có thể rộng lượng phần nào ngay cả với bug thực sự
      Xét về giao diện người dùng hay bug, trải nghiệm debug C++ 40 năm tuổi bằng gdb 35 năm tuổi cũng đủ sức cạnh tranh với bất kỳ ngôn ngữ thử nghiệm nào. Ví dụ, việc in funcname()::staticvarname là một giao diện kỳ quặc và thất bại khoảng một nửa số lần. Còn hệ thống build của C++ thì khỏi phải nói
      Nếu là công nghệ thử nghiệm, ta có thể phê bình khái niệm của nó, nhưng một giao diện người dùng thô ráp thì theo tôi vẫn có thể chấp nhận phần nào
    • Cách dùng trình biên dịch có trong README trên GitHub
      https://github.com/ValeLang/Vale#building-a-vale-program
    • Nếu là phần mềm vẫn còn ở giai đoạn alpha thì đây là trạng thái khá dễ đoán trước
  • Độ trễ dễ dự đoán hơn GC truy vết, hiệu năng và tính thân thiện với cache tốt hơn reference counting, lại còn dễ tạo nguyên mẫu và lặp lại hơn borrow checking — nghe đến mức không chỉ tò mò mà còn thực sự quan tâm
    Tôi cũng đã bắt đầu theo dõi RSS feed: https://verdagon.dev/rss.xml

    • Cuối cùng cũng có một ý tưởng mới trong ngôn ngữ biên dịch AOT mà không kết thúc bằng kiểu “thôi cứ để lỗi bộ nhớ thỉnh thoảng xảy ra đi”
  • Vale cần thêm nhà tài trợ
    https://github.com/sponsors/ValeLang
    Tôi hy vọng trong lúc bài này còn đang ở trang nhất, dự án sẽ đạt được mục tiêu 3.000 USD mỗi tháng
    Tôi muốn giúp Evan có thể làm việc này toàn thời gian. Tôi cũng là một nhà tài trợ. Một ngôn ngữ vừa nhanh vừa an toàn lại còn thú vị để tạo nguyên mẫu thì đáng để ủng hộ

    • Tôi tò mò không biết giữa tài trợ qua GitHub và Patreon thì chia sẻ doanh thu khác nhau thế nào
  • “Trình tối ưu hóa trước dành riêng cho Vale, tương tự Cranelift của Rust” có lẽ đang nói đến MIR, tức middle-level intermediate representation. Có một bài blog hay liên quan đến chủ đề này: https://blog.rust-lang.org/2016/04/19/MIR.html
    Cranelift chủ yếu là backend trình biên dịch tập trung vào JIT, nhưng về lý thuyết cũng có thể thay thế LLVM. Công việc về backend thay thế cũng đang được tiến hành, nhưng có những ràng buộc: https://github.com/bjorn3/rustc_codegen_cranelift

    • Có lẽ đúng vậy. Cranelift được xem như trình tối ưu hóa Rust cho WebAssembly
  • Cách tiếp cận đưa ra lựa chọn để trong phần lớn mã nguồn không phải bận tâm về quản lý bộ nhớ, trong khi chỉ tối ưu các đường mã nóng bằng trừu tượng hóa không chi phí, nghe như đang có được ưu điểm của cả hai phía
    Điều đó càng đúng nếu cái phải đánh đổi vì sự tiện lợi không phải là an toàn mà chỉ là hiệu năng

    • Tôi chỉ dùng C++ nhưng hoàn toàn không lo về quản lý bộ nhớ
      Shared ownership là một khái niệm tệ nên tôi cũng không dùng smart pointer
      Tôi cho rằng các vấn đề quản lý bộ nhớ nhìn chung đều là chuyện nhỏ
  • Vẫn luôn thắc mắc từ an toàn trong ngữ cảnh tham chiếu thế hệ (generational references) thực sự có nghĩa là gì
    Nếu tôi hiểu đúng thì có phải là ngăn được use-after-free và double-free không? Nếu vậy thì khi thế hệ dự kiến và thế hệ thực tế không khớp, chương trình vẫn có thể thất bại khi truy cập bộ nhớ
    Xét ở điểm đó thì có vẻ kém an toàn hơn đếm tham chiếu, garbage collection truy vết và kiểm tra borrow

    • double-free được ngăn bởi quyền sở hữu đơn của Vale, tức quyền sở hữu đơn theo nghĩa của C++, còn tham chiếu thế hệ giúp phát hiện use-after-free một cách an toàn
      Nếu cố truy cập bộ nhớ đã được giải phóng thông qua một tham chiếu thì điều đó phải dẫn đến lỗi phân đoạn hoặc assertion failure theo cách có thể dự đoán và an toàn. Về sau, nếu có cải tiến ánh xạ lại không gian địa chỉ ảo thì có lẽ còn có thể loại bỏ cả lỗi phân đoạn, điều này khá đáng mong đợi
    • Nó an toàn theo nghĩa là sẽ gây lỗi phân đoạn thay vì để một con trỏ treo đọc hoặc ghi vào bộ nhớ tùy ý
      Tuy vậy, nếu là chỉ mục thế hệ thì về lý thuyết runtime cũng phải có thể kiểm tra tính hợp lệ của truy cập trước khi thực sự thử truy cập. Tôi không rõ Vale có làm được vậy không
    • Kém an toàn hơn GC, kiểm tra borrow và đếm tham chiếu. Dù vậy vẫn an toàn hơn malloc/free, và còn có những ưu điểm khác
    • Tôi cũng đang thắc mắc. Tôi không hiểu cơ chế này ngăn use-after-free và double-free như thế nào
      Hàm check cần số thế hệ của allocation, nên nó phải truy cập vào allocation đó. Tức là để kiểm tra liệu tham chiếu có thể truy cập allocation đó hay không, trước hết nó phải truy cập allocation đó đã
      Tất nhiên, nếu allocation đã bị giải phóng rồi thì việc truy cập chính allocation đó và số thế hệ của nó đã là hành vi không xác định, nên sẽ không hoạt động
      Có vẻ quá hiển nhiên nên tôi không biết mình đang bỏ sót điều gì lớn, hay ở đây “an toàn bộ nhớ” được dùng theo một nghĩa hoàn toàn khác
    • Nếu hỏi nó có kém an toàn hơn khái niệm “an toàn” mà chính tác giả giới hạn lại hay không, thì có lẽ là vậy
  • Vale không phải là ngôn ngữ như V. V từng nhận một bài review rất nặng lời tại https://mawfig.github.io/2022/06/18/v-lang-in-2022.html, và vì tên khá giống nhau nên tôi đã nhớ nhầm sang Vale
    Để lại ở đây phòng khi có ai khác cũng nhầm như tôi

    • “Bài review rất nặng lời” đó thực ra chỉ là danh sách các lỗi nhỏ đã được sửa từ một năm trước
      Nội dung bài viết giờ không còn liên quan nữa nhưng vẫn còn tồn tại, và đó cũng là bài viết duy nhất trên blog đó
    • “Bài review chỉ trích” đó có vẻ gần với một mẩu spam cũ mà phe phản đối hay troll cứ tiếp tục dùng hơn. Một “bài review” về phiên bản alpha của ngôn ngữ, thực chất là một bài công kích nên ngoài chuyện đó ra không thấy có giá trị gì khác
      Bây giờ đã là năm 2023 và V cũng đang ở beta (0.4). Hơn nữa, người viết bài đó dùng một tài khoản GitHub dùng một lần để đăng bài review/công kích, tạo tranh cãi rồi biến mất
      Bài duy nhất trên blog đó cũng chỉ là bài công kích V, không có review nào khác. Những phần từng có nội dung thực chất thì nay cũng đã được sửa rồi[1]
      Nếu tìm mawfig.github thì cũng sẽ thấy nó bị phát tán lặp đi lặp lại trên HN và thường được dùng để bôi nhọ
      [1]: https://github.com/vlang/v/issues/14803
      [1]: https://github.com/vlang/v/issues/14787
      [1]: https://github.com/vlang/v/issues/14786
  • Chúc mừng Evan đã đạt được cột mốc này. Tôi không có kinh nghiệm về thiết kế ngôn ngữ lập trình hay compiler, nhưng rất thích đọc các bài viết về Vale

    • Tôi cũng vậy, nhưng giá mà cái tên khác đi thì tốt hơn
      Giờ đã có Vale của Evan và Val của Adobe Software Technology Lab, nên tìm tài liệu liên quan chắc sẽ khá khó
      https://www.val-lang.dev
    • Tôi cũng không có nền tảng để hiểu bài viết trong đa số trường hợp, nhưng vẫn thấy thú vị
    • Tôi cũng thấy các bài viết rất hay và mong chờ tương lai của Vale
  • “Vale nhanh: Vale được biên dịch AOT bằng LLVM, dùng kiểu tĩnh, sử dụng kỹ thuật tham chiếu thế hệ mới để có an toàn bộ nhớ đi kèm tốc độ và tính linh hoạt, và sắp bổ sung kiểm tra borrow theo vùng để còn nhanh hơn nữa”
    https://vale.dev/

  • Cảm giác như đang nghe lỏm một cuộc tranh luận kéo dài 5 năm giữa hai người
    Có ai giải thích giúp chuyện gì đang diễn ra ở đây không? Bài viết khó hiểu quá