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

Tăng tốc mã: Đừng truyền struct lớn hơn 16 byte trên AMD64

  • Để cải thiện hiệu năng của ngôn ngữ Neat, cách truyền mảng đã được thay đổi từ một tham số struct duy nhất sang ba tham số con trỏ.
  • Lý do mảng của Neat chậm hơn mảng của ngôn ngữ D là vì mảng có kích thước 24 byte, vượt quá 16 byte nên tham số được truyền theo cách khác.
  • Theo đặc tả SystemV AMD64 ABI, mọi struct vượt quá 16 byte đều được truyền thông qua con trỏ.

Xác nhận vấn đề bằng benchmark

  • Thông qua benchmark, có thể xác nhận sự khác biệt hiệu năng giữa cách truyền struct và cách truyền từng trường riêng lẻ.
  • Khi truyền struct, cần có quá trình cấp phát trên stack và sao chép, trong khi khi truyền từng trường riêng lẻ thì chúng được chuyển trực tiếp qua các thanh ghi SSE.
  • Cách truyền từng trường riêng lẻ cho hiệu năng nhanh hơn khoảng 2 lần so với cách truyền struct.

Lựa chọn của nhà thiết kế ngôn ngữ

  • Khi gọi C API thì phải tuân theo C ABI, nhưng các kiểu cấp cao được sử dụng nội bộ không nhất thiết phải được biểu diễn dưới dạng struct.
  • Nhà thiết kế ngôn ngữ có thể quyết định cách mảng, tuple, union type, v.v. sẽ được truyền đi.
  • Truyền các kiểu vượt quá 16 byte dưới dạng từng trường riêng lẻ có thể giúp cải thiện hiệu năng.

Ý kiến của GN⁺

  • Bài viết này rất hữu ích cho các lập trình viên quan tâm đến tối ưu hóa phần mềm.
  • Đặc biệt, bài viết cho thấy rằng khi phát triển các ứng dụng nhạy cảm về hiệu năng, kích thước struct và cách truyền tham số có thể tạo ra ảnh hưởng quan trọng.
  • Các nhà thiết kế ngôn ngữ hoặc nhà phát triển API có thể tận dụng thông tin này để tìm cơ hội cải thiện hiệu năng.

1 bình luận

 
GN⁺ 2024-01-06
Ý kiến Hacker News
  • Liên quan đến vấn đề của SysV amd64 ABI, có thể đặt ABI nội bộ của ngôn ngữ thành thứ gì đó khác ngoài SysV. Miễn là nó không bị lộ ra cho trình gọi C theo SysV, bạn có thể dùng bất kỳ quy ước gọi hàm nào mình muốn. Điểm khác biệt của NeatLang có vẻ phức tạp hơn nhiều so với việc thay đổi quy ước gọi của LLVM, và tác giả cũng có thể muốn phơi bày các kiểu cho chương trình C bằng một quy ước gọi nhất quán.
  • Nhiều người thường thiếu hiểu biết về chi phí truyền đối số, và bài viết về chủ đề này khá hữu ích. Ví dụ, ở Google, việc truyền một đối tượng 24 byte theo giá trị không hiện lên trong profiler nhưng vẫn phát sinh chi phí ở mọi hàm.
  • Khi chuyển sang x64, có người đã benchmark engine đồ họa vì lo ngại đối tượng vec3 (3xfloat) bị mở rộng từ 12 byte lên 16 byte. Họ phát hiện dùng 16 byte lại nhanh hơn vì phù hợp với các lần đọc 8 byte. Kết quả là vec3 được dùng giống như vec4. Luôn nên benchmark toàn cục.
  • Các đối số được nạp sẵn vào thanh ghi có hiệu năng tốt hơn việc ghi lên stack, và thao tác trên stack lại nhanh hơn thứ được cấp phát trên heap. Đây là lý do mã phức tạp với nhiều biến toàn cục có thể chạy nhanh, còn các hàm đệ quy thanh lịch hoặc các đối số tuple/struct/liste thì chậm hơn. Loại đầu tiên dễ tối ưu thành các vòng lặp assembly dày đặc hơn.
  • Trong MSVC, struct lớn hơn 8 byte sẽ được truyền trên stack. Đây là chi tiết ABI mà mã portable không nên phụ thuộc vào. Tuy nhiên, với các hàm không được gọi thường xuyên thì cũng không cần quá căng thẳng; còn với các hàm nhỏ được gọi nhiều, hãy để compiler có thể inline mã để kích hoạt những tối ưu hữu ích hơn cả việc truyền đối số qua thanh ghi.
  • Trên Windows, khi dùng quy ước gọi cdecl mặc định, các struct lớn hơn 8 byte sẽ không được truyền qua thanh ghi.
  • Trên amd64, dùng sysv amd64 ABI để truyền và trả về theo giá trị các struct lớn hơn 16 byte là chậm, nhưng thường vẫn đáng giá vì giúp mã rõ ràng hơn. Tất nhiên điều này không áp dụng cho trường hợp ở đây, nhưng chẳng hạn mỗi compiler C++, Golang, OCaml, SBCL đều có thể dùng ABI tùy biến bên trong ngôn ngữ riêng của mình.
  • Trong C++, có một quy tắc kinh nghiệm là các kiểu không nguyên thủy nên được truyền bằng tham chiếu (hoặc con trỏ nếu thật sự cần), trừ khi có lý do chính đáng để không làm vậy. Điều này một phần là vì ABI, một phần để tránh copy constructor hoặc move constructor. Nếu muốn tối ưu hiệu năng, đây là những chi tiết low-level nhàm chán mà bạn phải để ý trong C++.
  • Bài viết đưa ra liên kết đến một benchmark rất đặc thù, nơi Java (JIT) nhanh hơn C++ và thậm chí nhanh hơn cả Scala. Điều này làm dấy lên câu hỏi Julia HO là gì và vì sao nó lại nhanh đến vậy, vì sao chênh lệch tốc độ giữa Python và Pypy lại lớn như thế, liệu có lý do gì để không dùng Pypy hay không, và liệu nó có nên trở thành tiêu chuẩn hay không.
  • Trong ví dụ được đưa ra, có thể sửa bằng cách đổi kiểu tham số struct Vector sang truyền bằng tham chiếu const struct Vector & mà không ảnh hưởng đến phía trình gọi. Nhiều đoạn mã C++ đầy lỗi con trỏ là do dùng con trỏ một cách không cần thiết, trong khi truyền bằng tham chiếu có thể dễ dùng và an toàn hơn.