4 điểm bởi GN⁺ 2024-10-19 | 1 bình luận | Chia sẻ qua WhatsApp

Tận dụng tính đồng thời và song song của Go

  • Giới thiệu một dự án nhằm cải thiện năng lực tính toán số bằng cách tận dụng tính đồng thời và song song của Go.
  • Có thể thực hiện tính toán song song ở cấp độ phần cứng bằng cách sử dụng các lệnh SIMD (Same Instruction Multiple Data).
  • Trình biên dịch của Go không tận dụng SIMD, và do không tìm được gói SIMD đa dụng phù hợp nên tác giả quyết định tự phát triển một gói.

Ngôn ngữ assembly Plan9

  • Go sử dụng ngôn ngữ assembly riêng là Plan9, được dùng bằng cách điều chỉnh đôi chút các lệnh và thanh ghi của từng nền tảng cụ thể.
  • x86 Plan9 và ARM Plan9 khác nhau.
  • Giải thích cách sử dụng cơ bản thông qua các ví dụ đơn giản của Plan9.

Ví dụ Plan9

  • Giải thích cách khai báo và sử dụng hàm cơ bản của Plan9 thông qua các tệp AddInts_amd64.smain.go.
  • Mô tả cách lưu tham số hàm và giá trị trả về trên stack theo quy ước gọi hàm của Go.

Kế hoạch thiết kế gói

  • Thiết kế một gói cung cấp lớp trừu tượng mỏng cho các tác vụ SIMD về số học và phép toán bit.
  • Tạo gói nội bộ chứa các triển khai Plan9 theo từng kiến trúc, rồi thiết lập chúng thông qua hàm khởi tạo.

Ví dụ SIMD

  • Giải thích cách dùng SIMD thông qua ví dụ về các hàm Plan9 SIMD trên x86.
  • Trình bày cách kiểm tra hỗ trợ SSE và thực hiện phép cộng float32 thông qua các tệp Supported_amd64.sAddFloat32_amd64.s.

Hiệu năng và tương lai

  • Biểu đồ cho thấy chênh lệch hiệu năng giữa triển khai phần mềm Go và triển khai Plan9 SIMD, xác nhận mức tăng tốc khoảng 200-450%.
  • Tác giả hy vọng ghi chú này sẽ truyền cảm hứng cho các dự án sử dụng Plan9 và SIMD.

# Tổng hợp của GN⁺

  • Bài viết này giới thiệu cách tận dụng tính đồng thời và song song của Go để tối đa hóa hiệu năng.
  • Giải thích cách thực hiện tính toán song song ở cấp độ phần cứng bằng ngôn ngữ assembly Plan9 và các lệnh SIMD.
  • Bài viết gợi mở cho các lập trình viên Go về khả năng ứng dụng Plan9 và SIMD, đồng thời có thể hữu ích khi khám phá các cách tiếp cận mới để cải thiện hiệu năng.
  • Các dự án có chức năng tương tự được gợi ý gồm thư viện hỗ trợ SIMD của Rust hoặc các thư viện liên quan đến SIMD trong C++.

1 bình luận

 
GN⁺ 2024-10-19
Các ý kiến trên Hacker News
  • Nói qua vài điểm về assembly của Go: trên amd64, các int đó thực ra là 64-bit.
    Nếu dùng int32 thì danh sách tham số sẽ được căn chỉnh theo word, nhưng có một cái bẫy. Trên hệ thống 64-bit, giá trị trả về luôn bắt đầu ở offset được căn chỉnh theo doubleword.
    NOSPLIT được định nghĩa trong textflag.h, do trình biên dịch Go tự động cung cấp. Tuy nhiên, theo những gì tôi đọc được thì có vẻ NOSPLIT chỉ được tôn trọng trong các hàm runtime.XX, nên ở đây nó không làm gì cả và cũng không cần thiết.
    NOSPLIT có nghĩa là đừng để trình biên dịch chèn mã kiểm tra xem stack có thể bị tràn và cần phân tách hay không. Với một hàm không cần không gian stack thì về mặt kỹ thuật là không cần thiết; về cơ bản nó tồn tại để ngăn đoạn mã kiểm tra đó bị chèn vào chính hàm thực hiện kiểm tra stack splitting.

  • Nói thêm cho ai thắc mắc vì sao 4 được ghi là biểu thị “NOSPLIT” và vì sao lại nói là cần thiết: thông thường sau kích thước frame (tham số đứng sau NOSPLIT) sẽ là kích thước đối số, và hai phần được phân tách bằng dấu trừ.
    Đây không phải phép trừ mà chỉ là một cú pháp đặc thù. Kích thước frame $24-8 nghĩa là hàm có frame 24 byte và được gọi với 8 byte đối số nằm trong frame của caller.
    Nếu không chỉ định NOSPLIT cho TEXT, bạn bắt buộc phải cung cấp kích thước đối số. Với các hàm assembly có prototype Go, go vet sẽ kiểm tra kích thước đối số có đúng hay không.
    Nguồn: https://go.dev/doc/asm

  • “Go dùng một ngôn ngữ assembly nội bộ riêng gọi là Plan9” — ngôn ngữ đó thật sự được gọi như vậy à?

    • Không. Nó chỉ là assembly của Go.
      Cú pháp đúng là có nguồn gốc từ Plan 9, nhưng chúng tôi gọi nó là assembly của Go.
      Xem https://go.dev/doc/asm
    • Câu hỏi hợp lý. Ban đầu tôi cũng cứ nghĩ là đúng. Tôi cho rằng người nghiên cứu chủ đề này hẳn sẽ không sai ở chỗ như vậy, và nếu biết chút ít về dự án thì cái tên đó cũng có vẻ phần nào hợp lý.
      Nhưng càng tìm hiểu thêm, tôi càng thấy đây giống một ảo giác của LLM.
      Tài liệu về định dạng assembly không đặt cho nó một tên riêng đúng nghĩa, mà chỉ gọi là go assembler.
      Nguồn gốc của ảo giác này có lẽ là đoạn đầu tiên: “Assembler dựa trên phong cách input của assembler Plan 9 và… tài liệu hiện tại mô tả tóm tắt cú pháp đó, các khác biệt, cũng như những điểm đặc thù khi viết mã assembly tương tác với Go.”
    • Không có tên riêng đâu. Plan 9 là một hệ điều hành, còn phong cách cú pháp assembly này bắt nguồn từ assembler từng được dùng trong hệ điều hành đó.
      Nó tương tự như nói “GNU Compiler Collection dùng một ngôn ngữ assembly nội bộ riêng gọi là Unix”.
  • Nếu tò mò vì sao đội ngũ Go chọn định dạng assembly chuyên biệt này, Rob Pike từng nói về thiết kế assembler của Go trong một bài trình bày năm 2016 [1][2].
    Ý chính có vẻ là quan sát rằng hầu hết các ngôn ngữ assembly nhìn chung khá giống nhau, nên có thể tạo ra một ngôn ngữ assembly chung giúp “trò chuyện với tầng thấp nhất của máy mà không cần học cú pháp mới”.
    Nó cũng cho phép tự động tạo assembler hoạt động bằng cách nhận PDF manual tập lệnh của kiến trúc mới làm đầu vào.
    [1]: https://www.youtube.com/watch?v=KINIAgRpkDA
    [2]: https://go.dev/talks/2016/asm.slide#1

    • Thực tế là nó đã có hiệu quả. Go đã tạo ra kỳ vọng rằng một ngôn ngữ lập trình mới nên có cross-compilation như một năng lực mặc định, trong khi vào thời điểm đó hầu như không có ngôn ngữ nào làm tốt việc này.
    • Nhân tiện, SpiderMonkey cũng đã làm việc gần như tương tự trong khoảng 25 năm, và tôi đoán các máy ảo JavaScript khác cũng vậy.
  • Bài viết nói “cần các hàm để thực hiện phép toán SIMD trên slice”, nên tôi tò mò thực tế dùng như thế nào.
    Tìm trong toàn bộ bài cũng không thấy phép toán nào được thực hiện trên slice.
    Sửa: đã tìm thấy trong tài liệu được liên kết: https://pkg.go.dev/github.com/pehringer/simd#pkg-index
    Về cơ bản, nếu có 2 slice muốn cộng lại, thay vì dùng vòng lặp for, bạn có thể xử lý song song bằng SIMD và dùng kiểu như simd.AddInt32(slice1, slice2, result).

  • Về các bộ xử lý mà Go hỗ trợ, phần này có liên quan (1). Hỗ trợ x64 cơ bản bao gồm SSE và SSE2.
    Tuy nhiên tôi không rõ trình biên dịch Go có thực sự sinh ra chúng hay không. Khác với những trình biên dịch rất phức tạp như gcc, vốn đặt hiệu năng lên hàng đầu, trình biên dịch Go thiên về kiểu Wirth (2): đơn giản và biên dịch nhanh.
    (1) https://go.dev/wiki/MinimumRequirements#amd64
    (2) https://irreal.org/blog/?p=7075
    https://smartgo.blog/2024/01/06/niklaus-wirth/

    • Thực tế thì tôi nghĩ mọi chip từ khoảng năm 2008 trở đi đều hỗ trợ SSE4.1.
  • Có vẻ tác giả đã nhầm ở phần này nên tôi để lại liên kết tham khảo: https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs

  • Tôi cũng đang định nói rằng chuyện này trông giống kết quả LLM diễn giải sai code
    Thật khó hình dung theo cách nào khác rằng một người biết thuật ngữ Plan 9 và đào sâu vào assembly, nhưng lại không biết mình đang bước vào vùng nước nào. Tôi nhận ra điều đó khi thấy những người khác cũng nghĩ như vậy
    Nếu đúng là vậy, mong tác giả đừng cảm thấy xấu hổ hay “bị bắt quả tang”, mà hãy thành thật. Như vậy chúng ta mới có thể học được. Tôi muốn có thêm sự chắc chắn về kiểu “lộ dấu vết LLM” này, nhưng dù trông có vẻ rõ ràng đến đâu thì hầu như hiếm khi thấy người ta thừa nhận
    Tất nhiên ở đây không phải là điều hiển nhiên, chỉ là một suy đoán rất vội vàng và mang tính phán xét

    • Nói thật à? Không. Nên thấy xấu hổ vì sự ngạo mạn khi nghĩ rằng LLM là lối tắt để khỏi phải làm công việc mà đáng ra nó phải làm cho đúng
      Những chuyện như thế này thật sự khiến tôi bực
  • Nội dung hơi vượt quá trình độ của tôi, nhưng tôi thích cách bài viết dẫn dắt người đọc đi cùng
    Chắc đây không thể là lần đầu có người thử làm chuyện này nhỉ? Hẳn phải có đến hàng chục Gopher đang khát SIMD theo đúng nghĩa đen. Mẫu phổ biến hơn có phải là dùng CGO không?

    • Vấn đề của cgo là overhead khi gọi hàm lớn. Tốt nhất chỉ dùng cho những khối công việc khá lớn
      Gọi hàm assembly trong Go rẻ hơn nhiều
      https://pkg.go.dev/github.com/grailbio/base/simd có một số thứ tôi đã làm theo hướng này
    • Tôi nghĩ chắc chắn từ lâu đã có người thử rồi. Tôi nhớ gần 10 năm trước có lần gọi điện với anh trai khi anh ấy đang cố viết một thư viện SIMD bằng Go (hình như là Skype?)
      Nếu tôi nhớ đúng, khi đó nhiều lệnh AVX còn không được assembler Plan 9 của Go mã hóa, nên phải tự mã hóa trực tiếp bằng byte [0]
      Thư viện hoàn chỉnh nhất tôi từng thấy, dù chưa dùng thử, sử dụng một mẹo khá gọn gàng: dùng CGO một phần nhưng tránh được overhead đó [1]
      [0]: https://github.com/slimsag/rand/blob/f1e8d464c0021a391d5cd64...
      [1]: https://github.com/alivanz/go-simd/
    • Đề xuất đưa những thứ như thế này vào thư viện chuẩn đã bị từ chối, nhưng phần thảo luận giúp hiểu bối cảnh: https://github.com/golang/go/issues/53171
  • Nếu muốn viết chương trình assembly cho Go, tôi khuyên nên xem Avo(https://github.com/mmcloughlin/avo)
    Nó cung cấp type safety và thực hiện một số kiểm tra để đảm bảo xuất ra assembly hợp lệ. Nó có thể cấp phát thanh ghi động, và giúp bạn không phải tự tính những thứ như stack và kích thước frame
    Nó cũng có thể xử lý các chi tiết của calling convention, nên bạn có thể dễ dàng load tham số vào thanh ghi hoặc vị trí mong muốn
    Gần đây tôi đã port toàn bộ assembly amd64 trong thư viện crypto của Go sang Avo, và đây là một thư viện rất hữu ích cho kiểu công việc này