2 điểm bởi GN⁺ 2024-10-03 | 1 bình luận | Chia sẻ qua WhatsApp
  • Trong điều kiện tranh chấp cao, khác biệt giữa các triển khai mutex bộc lộ rất rõ; pthread_mutex_t của Cosmopolitan Libc cho thấy thời gian chạy ngắn hơn và mức dùng CPU thấp hơn so với các triển khai chính trên Windows và Linux
  • Trong bài thử trên Windows với 24-core Threadripper 29070WX, Cosmopolitan nhanh hơn Microsoft SRWLOCK 2,75 lần và dùng ít tài nguyên CPU hơn 18 lần
  • Trên Linux với 96-core Threadripper Pro 7995WX, nó nhanh hơn glibc 3 lần và nhanh hơn musl libc 11 lần, còn khoảng cách về thời gian CPU thì lớn hơn nữa
  • Trên MacOS M2 Ultra, Apple Libc nhỉnh hơn một chút; Cosmopolitan dùng một thuật toán đơn giản phụ thuộc vào lời gọi hệ thống ulock của XNU trong môi trường ARM
  • Nền tảng cho hiệu năng này là việc tích hợp nsync của Google, với các điểm then chốt gồm đường đi nhanh CAS, hàng đợi waiter, futex/ulock/WaitOnAddress(), cơ chế tránh starvation và thiết kế designated waker

Cách benchmark mutex có tranh chấp

  • Bài test tạo 30 luồng, mỗi luồng tăng cùng một số nguyên toàn cục g_chores 100.000 lần
  • Mỗi phép tăng được thực hiện trong một vùng tới hạn rất nhỏ giữa pthread_mutex_lock()pthread_mutex_unlock()
  • Các số đo dùng đơn vị micro giây và được chia thành ba loại thời gian
    • wall time: thời gian thực để chạy chương trình, bao gồm cả overhead tạo luồng và join
    • user time: thời gian CPU tiêu tốn trong không gian người dùng
    • system time: thời gian CPU tiêu tốn trong kernel
  • Vì nhiều luồng chạy song song, tổng user time và system time có thể lớn hơn wall time
  • Trong tình huống không có tranh chấp, khác biệt hiệu năng giữa các triển khai thường nhỏ; nhưng trong tình huống có tranh chấp, khác biệt thiết kế mutex lộ ra rất rõ

Windows: Cosmopolitan nhanh hơn SRWLOCK

  • Bài test trên Windows được thực hiện trên 24-core Threadripper 29070WX
  • MutexShootout của Mark Waterman từng đánh giá SRWLOCK của Windows là triển khai mạnh nhất trong kịch bản tranh chấp cao
  • Trong cùng điều kiện, pthread_mutex_t của Cosmopolitan ghi nhận wall time ngắn hơn và mức dùng CPU thấp hơn SRWLOCK
Triển khai wall time user time system time
Cosmopolitan pthread_mutex_t 148,940µs 328,125µs 62,500µs
Microsoft SRWLOCK 410,416µs 5,515,625µs 1,640,625µs
Microsoft CRITICAL_SECTION 949,187µs 7,937,500µs 5,078,125µs
MSVC 2022 std::mutex 991,750µs 12,156,250µs 4,031,250µs
spin lock 1,165,435µs 24,515,000µs 15,000µs
Cygwin pthread_mutex_t 9,780,803µs 1,937,000µs 6,156,000µs
  • Mutex của Cosmopolitan nhanh hơn 2,75 lần so với Microsoft SRWLOCK và dùng ít tài nguyên CPU hơn 18 lần
  • So với Cygwin Mutex, một triển khai POSIX trên Windows, nó nhanh hơn 65 lần
  • Trong trường hợp sử dụng này, Cygwin Mutex còn chậm hơn cả spin lock

Linux: khoảng cách thời gian CPU còn lớn hơn wall time

  • Bài test trên Linux được thực hiện trên 96-core Threadripper Pro 7995WX
Triển khai wall time user time system time
Cosmopolitan pthread_mutex_t 36,905µs 44,511µs 23,492µs
glibc pthread_mutex_t 101,353µs 150,706µs 2,724,851µs
spin lock 202,423µs 4,694,749µs 2,000µs
Musl libc pthread_mutex_t 411,013µs 2,167,898µs 9,926,850µs
  • Mutex của Cosmopolitan nhanh hơn glibc 3 lầnnhanh hơn musl libc 11 lần
  • Tính theo thời gian CPU, nó dùng ít hơn glibc 42 lầnít hơn musl libc 178 lần
  • Trong workload mà mọi luồng đều phải thực hiện công việc bị tuần tự hóa, Cosmopolitan có thể trông như chỉ kích hoạt một lõi trong htop
  • Trong cùng tình huống, glibc và musl libc có thể làm mức dùng CPU tăng rất mạnh, khiến việc chạy nhiều tác vụ trên cùng máy chủ trở nên nặng nề hơn

MacOS: Apple Libc nhỉnh hơn một chút

  • Bài test trên MacOS được thực hiện trên M2 Ultra
Triển khai wall time user time system time
Apple Libc 52,263µs 43,202µs 911,009µs
Cosmopolitan pthread_mutex_t 54,700µs 63,055µs 1,003,674µs
  • Trên MacOS M2 ARM64, Apple Libc nhanh hơn một chút so với Cosmopolitan Mutex
  • Triển khai mutex tổng quát của Cosmopolitan không hoạt động tốt trên nền tảng này
  • Trên MacOS ARM, Cosmopolitan dùng một thuật toán đơn giản hơn dựa trên Futexes Are Tricky của Ulrich Drepper
  • Cách làm này giao phần lớn xử lý nặng cho lời gọi hệ thống ulock của XNU, nhờ đó đạt hiệu năng gần như tương đương triển khai của Apple

Nền tảng hiệu năng: tích hợp nsync

  • Trọng tâm hiệu năng của Cosmopolitan Mutex là việc tích hợp thư viện nsync của Google
  • nsync là một thư viện có 371 sao trên GitHub, do Mike Burrows của Google viết
  • Trong quá trình tích hợp vào Cosmopolitan, đã có các công việc sau
    • Tìm ra và sửa một lỗi tồn tại lâu mà chưa được phát hiện trong hàm unlock mutex của nsync
    • Port sang phép toán nguyên tử C11 trên AARCH64, giúp mutex nsync có tranh chấp nhanh hơn 30% so với nsync upstream
    • Viết lại tích hợp hệ thống như futex để hỗ trợ tính di động ở runtime
    • Làm cho nó hoạt động trơn tru với cơ chế hủy luồng POSIX

Cách nsync hoạt động

  • nsync trước tiên thử ngay một thao tác CAS (compare and swap) lạc quan để lấy khóa nhanh
  • Nếu không lấy được khóa, nó thêm luồng gọi vào danh sách liên kết kép của các waiter
    • Mỗi waiter có semaphore riêng trên một cache line độc lập
    • Một khi đã vào trạng thái chờ, luồng đó sẽ không còn đụng vào khóa chính nữa
    • Điều này rất quan trọng để giảm overhead giao tiếp phát sinh khi nhiều lõi cùng chạm vào một cache line
    • Tài liệu nền liên quan là What Every Programmer Should Know About Memory của Ulrich Drepper
  • nsync dùng futex của hệ điều hành để đưa luồng vào trạng thái ngủ
    • Trên MacOS, futex được gọi là ulock
    • Trên Windows, WaitOnAddress() đóng vai trò futex
    • Trong các hệ điều hành mà Cosmo hỗ trợ, chỉ NetBSD là không có futex; nó triển khai semaphore POSIX trong không gian kernel và cần một file descriptor mới cho mỗi semaphore
  • nsync tránh starvation bằng khái niệm “long wait”
    • Nếu một waiter đã bị đánh thức 30 lần nhưng lần nào cũng thất bại khi lấy khóa ở nội bộ, mutex sẽ thêm một bit để ngăn các luồng chưa phải chờ lấy được khóa
    • Khi bit này được bật, CAS ban đầu của các luồng mới vào sẽ thất bại cho đến khi hàng đợi được giải phóng bớt
  • Trường hợp tranh chấp trong vùng tới hạn nhỏ được tăng tốc bằng khái niệm designated waker
    • Khi một luồng thức dậy để cố lấy khóa, một bit sẽ được đặt trên khóa chính
    • Trong nsync, hàm unlock chịu trách nhiệm đánh thức luồng chờ tiếp theo
    • Nhờ bit này, luồng đang unlock không cần đánh thức waiter thứ hai nếu đã có một luồng thức sẵn
  • Mã nguồn liên quan nằm tại cosmopolitan/third_party/nsync/mu.ccosmopolitan/libc/intrin/pthread_mutex_lock.c

Dịch vụ thực tế và mã kiểm chứng

  • Có thể xem demo chạy thực tế dùng Cosmo Mutex tại máy chủ http://ipv4.games/
  • Dịch vụ này chạy trên VM GCE 2 lõi và đến nay đã chịu được DDoS botnet quy mô tối đa 49.131.669 IP
  • Nhờ nsync, tác giả có thể chuyển truy vấn SQL sang các luồng nền và dùng kiến trúc các luồng gửi thông điệp cho nhau
  • Có thể xem các chỉ số trạng thái tại /statusz
  • Mã benchmark dùng gettimeofday() để đo wall time và getrusage() để đo user time cùng system time
  • Cuối cùng, nó kiểm tra g_chores == THREADS * ITERATIONS để xác minh mọi phép tăng đều đã được thực hiện

Lưu ý khi nhìn vào spin lock

  • Trong tình huống không tranh chấp, khác biệt giữa các triển khai mutex là nhỏ và một spin lock vài dòng có thể tốt hơn
  • Tuy nhiên, chỉ nên dùng spin lock khi thực sự không còn lựa chọn nào khác
  • Nó hữu ích ở những nơi như kernel, nơi các ràng buộc cực thấp khiến việc dùng cách phức tạp hơn là khó khăn
  • Spin lock cũng có thể được dùng như một chi tiết triển khai nội bộ bên trong nsync lock
  • Nếu chỉ nhìn vào wall time, spin lock có thể trông hấp dẫn hơn, vì vậy cần kiểm tra cả thời gian CPU bằng getrusage()

1 bình luận

 
GN⁺ 2024-10-03
Ý kiến trên Hacker News
  • Việc so sánh các triển khai mutex mới lúc nào cũng thú vị, nhưng tôi không thích cách benchmark này. Nó gần như trông giống một microbenchmark
    Những người thực sự triển khai lock nhanh thường dùng các chương trình đa luồng rất lớn làm phương tiện kiểm thử hiệu năng chính. Trong các workload phức tạp nơi độ dài vùng tới hạn, số lượng luồng tranh chấp và mức độ tranh chấp đều thay đổi, có vẻ những yếu tố làm mutex nhanh hay chậm cũng khác nhau
    Nhân tiện, tôi đã viết fast lock của WebKit, phát minh ra abstraction ParkingLot cho triển khai lock (cũng được dùng trong Rust và Unreal Engine), và trước đây cũng từng nghiên cứu, viết bài báo về fast lock cho Java

    • Nói thêm từ góc độ người làm ứng dụng desktop: với các app có vài chục luồng chạy thường xuyên, tôi muốn thấy số liệu hiệu năng trong trường hợp tranh chấp không nghiêm trọng
      Với tư cách lập trình viên audio thời gian thực, chi phí lấy một mutex vốn chưa bị khóa còn quan trọng hơn. Trong ứng dụng của chúng tôi, đây mới là tình huống áp đảo. Tương tự, tôi cũng muốn biết chi phí của thao tác try-lock sẽ thất bại, chứ không phải khi N luồng cùng tranh chấp
      Cosmopolitan là mã nguồn mở nên có thể tự đo, nhưng vẫn thấy tiếc
    • Tôi cũng nghĩ vậy. Có nhiều loại mutex khác nhau, và với từng workload cụ thể thì cái này hay cái kia sẽ tốt hơn. Tôi nghĩ ngay đến DistributedMutexSharedMutex (https://github.com/facebook/folly/blob/main/folly/synchroniz..., https://github.com/facebook/folly/blob/main/folly/SharedMute...)
      Cũng như hashmap, hiếm khi có một hashmap duy nhất tốt hơn cho mọi workload có thể có
    • Kiểu mutex này cũng sẽ được dùng trong PyMutex của Python 3.13. Có benchmark thực tế cho thấy PyMutex nhanh hơn bao nhiêu so với PyThread_type_lock trước 3.13
    • Đây chắc chắn là microbenchmark, và rất có thể không đại diện cho hiệu năng tổng quát. Trang này đưa ra một chuẩn tham khảo tốt về thực hành benchmark hệ điều hành, dù hơi thiên về học thuật hơn: https://gernot-heiser.org/benchmarking-crimes.html
    • Benchmark cụ thể đó thậm chí có thể ưu ái những hành vi không mong muốn, ví dụ sự bất công bệnh lý. Lập lịch tối ưu sẽ là chạy hết toàn bộ phép tăng của luồng thứ nhất, rồi mới chạy hết của luồng thứ hai, vì như vậy lưu lượng giữa các bộ xử lý được giảm xuống mức tối thiểu
      Một mutex ngủ trong khoảng thời gian cố định khi thất bại trong việc lấy lock (ví dụ 100µs) gần như luôn khiến công việc bị gom cụm lại để tiến gần kiểu hành vi đó và có thể “thắng” benchmark. Nhưng trong ứng dụng thực tế, chỉ cần có tranh chấp một chút thôi là loại mutex đó đã rất tệ
      Điều đó không có nghĩa mutex này tệ hay pthread mutex tốt hơn, mà là microbenchmark đó không đo thứ có thể dự đoán hiệu năng ứng dụng thực tế
  • Ở đoạn “Cosmopolitan Mutex tốt vì đã dùng thư viện nsync”, nsync là cái tôi mới nghe lần đầu, nhưng Mike Burrows cũng là người viết triển khai mutex production của Google: https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
    Vì vậy tôi thắc mắc tại sao triển khai mutex này lại bị bỏ khỏi benchmark. Và nếu trên macOS nó ủy quyền cho __ulock, thì có vẻ chỉ cần dùng các hàm thành viên wait(), notify_one() trong thư viện atomic của libc++ cũng có thể đạt được điều đó một cách đơn giản hơn
    Trước đây cũng từng có một thread lớn liên quan đến việc cải thiện triển khai mutex của Rust: https://github.com/rust-lang/rust/issues/93740#issuecomment-... Điều thú vị là cơ chế hoạt động bên trong của gần như mọi triển khai mutex phổ biến đều được thảo luận chi tiết

    • Khi tôi vào AV, Mike đã là một huyền thoại rồi. Có giai thoại rằng mỗi khi cần làm công cụ tìm kiếm nhanh hơn, ông ấy sẽ xuất hiện, viết lại vài hàm cốt lõi rồi quay về việc đang làm
      Có thể là thật, nhưng tôi không thể tự xác nhận. Ông ấy là một kỹ sư cực kỳ thông minh, rất coi trọng hiệu quả. Dù vậy, chúng tôi cũng không vận hành trên một máy chủ quá lâu
    • Burrows cũng từng tham gia vào biến đổi Burrows-Wheeler, Bigtable, Dapper, Chubby và nhiều thứ khác
    • Thread Rust đó cuối cùng cũng nhắc đến, nhưng về cơ bản nó nói về công việc của Mara, nên cũng nhắc tới cuốn sách của cô ấy xuất bản tháng 1/2023
      Triển khai mutex Rust hiện tại được đưa vào từ đầu năm nay, và trên Linux có thể không khác biệt lớn, nhưng trên Windows và Mac thì theo tôi biết là công việc mới
      Dù vậy, phần Mara giải thích nội tình của các triển khai khác vẫn rất thú vị, nhưng trong hoàn cảnh của bạn thì nên kiểm tra xem thông tin đó có còn cũ không
    • Lý do triển khai mutex của Abseil bị bỏ khỏi benchmark có thể là vì nó không phải C mà là triển khai C++. Chỉ là phỏng đoán thôi
    • Có vẻ Mike Burrows cũng từng nhận giải của ACM, và hình như có ảnh ở đó nữa
      https://awards.acm.org/award-recipients/burrows_9434147
  • Câu “đây vẫn là một thư viện C mới nên còn khá thô, nhưng nó cải thiện quá nhanh đến mức bắt đầu thấy như thiếu trách nhiệm nghề nghiệp nếu không dùng trong production” nghe khá kỳ. Tôi đánh giá cao dự án Cosmopolitan, nhưng những tuyên bố ưu việt cường điệu kiểu này thường là một dấu hiệu cảnh báo khá tệ

    • Tôi nghĩ các tuyên bố của Justine nhìn chung phần lớn là đúng. Chỉ là dùng cách diễn đạt cường điệu và hơi phô trương có vẻ là phong cách, hoặc tính cách của cô ấy.
      Tôi cũng hiểu vì sao một số người có thể thấy điều đó khá gai góc. Trước đây cũng từng có drama kiểu như vậy quanh llamacpp
    • Justine có vẻ là một người rất xuất sắc và sáng tạo, nhưng tôi không muốn dùng một libc “mới” và “còn thô” trong production.
      Điều được ưu tiên nhất trong production không phải là “cải thiện cực nhanh”, mà là tính ổn định, tính dự đoán được và độ tin cậy. Tất nhiên hiệu năng cũng quan trọng. Mã chạy nhanh hơn có thể giúp giảm hạ tầng, từ đó tốt hơn về chi phí và môi trường. Nhưng tốc độ đứng cuối cùng trong thứ tự ưu tiên
    • Khi ngồi một mình trước máy tính và code trong thời gian dài, có lẽ do thiếu tiếp xúc xã hội nên phần nào sẽ kéo theo sự ngạo mạn. Nếu không có cơ chế để kìm lại tầm quan trọng của bản thân hay công việc mình làm, thì dù thành quả có ấn tượng đến đâu, nó vẫn có thể trông vĩ đại hơn mức được công nhận rộng rãi.
      Ví dụ, tôi thấy APE là một cú hack rất ấn tượng, nhưng cũng có thể phê phán rằng: “Vậy là giờ không còn chỉ thiếu an toàn trên một nền tảng nữa, mà có thể thiếu an toàn đồng thời trên nhiều nền tảng sao?”
      Càng ở lâu trong lĩnh vực kỹ thuật, bạn càng nhận ra lợi ích hoàn toàn song phương là cực kỳ hiếm, và phần lớn mọi thứ đều là trade-off giữa cái được và cái mất
    • Ít nhất thì với tôi, nó giống như một câu đùa
    • Tôi tự hỏi liệu bạn có từng nghĩ rằng có thể bạn và Justine có gu hài hước khác nhau không. Tôi cũng không hiểu việc đăng chuyện này lên đây thì sẽ giúp ích cho ai
  • Hơi lạc đề hẳn, nhưng với tư cách là lập trình viên game, tôi đã dần thích mutex chậm để debug thật nhiều trong mọi bản build dành cho lập trình viên. Kiểu có tên/ID debug, theo dõi owner, báo thời gian tiêu tốn vì contention cho profiler, và cũng báo cả các lần đổi ownership vào profiler.
    Game có xu hướng tổ chức concurrency theo cách khác, và các mẫu để tránh lock cũng đã phát triển. Nhưng những mẫu đó khó dùng và đòi hỏi lập trình viên phải thay đổi cấu trúc. Phần lớn mã đều bắt đầu từ kiểu “cứ gắn cái lock vào đây trước đã rồi cố qua milestone”.
    Ngay cả lock nhanh cũng có thể chậm đi một cách khó lường, và nếu có bảo đảm thời gian thực thì nó sẽ phá hỏng điều đó. Trung bình có thể nhanh, nhưng tail latency không tự biến mất. Tôi không muốn là người phải quay lại để lần theo chuyện “game của chúng ta bị khựng”, nhưng thường thì tôi lại chính là người đó.
    Vì thế tôi thích dùng lock chậm hơn. Loại lock hiện lên to đùng màu đỏ trong profiler ấy. Nếu thấy rõ nó đang gây đau, thì ta refactor để loại bỏ nó là được.
    Tôi biết đó là một yêu cầu khó. Trong production AAA, số người biết dùng profiler chỉ đếm trên đầu ngón tay. Tôi đã thấy nhiều production và lúc nào cũng vậy.
    Xin lỗi vì than phiền, nhưng tôi vẫn mong nghiên cứu về các primitive và thuật toán concurrency nhanh tiếp tục được thúc đẩy

    • Lạc đề thêm chút nữa, đây cũng là một trong những lý do khiến việc phát triển game bằng Rust trở nên thú vị với tôi.
      Trong game, nếu có thể thì bạn tuyệt đối không muốn có lock contention, và trong nhiều trường hợp có thể chứng minh rằng việc lấy lock là không cần thiết. Ví dụ, mỗi frame được chia thành các giai đoạn, và quyền truy cập có thể thay đổi vào một tài nguyên dùng chung chỉ cần ở những giai đoạn nhất định, như update() trước render() hoặc hot reload asset.
      Dùng scoped thread và các quy tắc borrow của Rust, bạn có thể cấu trúc để hoàn toàn không cần mutex, và có thể tin rằng nếu về sau code thay đổi đến mức cần mutex thì compiler sẽ nghiêm khắc báo lỗi ngay.
      Nếu có thể chọn, lúc nào nhận lỗi biên dịch cũng tốt hơn nhận một spike trong profiler
    • Hoàn toàn đồng ý. Các tính năng debug như phát hiện deadlock hay kiểm tra trạng thái nội bộ rất dễ chứng minh giá trị của chúng. Nếu bạn đang lấy lock thường xuyên đến mức ảnh hưởng hiệu năng, thì nên xem lại thiết kế. Nên tránh chia sẻ trạng thái có thể thay đổi giữa các thread
  • Một mặt, dòng Cosmo/APE/redbean thực sự trông rất ấn tượng, các bình luận dưới những bài viết liên quan nhìn chung cũng khá tích cực, và hầu như không thấy mấy nội dung bác bỏ chính khái niệm này. Nhưng mặt khác, tôi gần như chưa từng nghe nói có ai khác đang dùng nó
    Không phải ai cũng chia sẻ rộng rãi về công việc của mình, nhưng nếu đã qua vài năm thì tôi nghĩ ít nhất cũng phải thấy vài bài tổng kết dự án. Tất cả các nhắc đến về Cosmo/APE/redbean mà tôi từng thấy đều xuất phát từ trang của Justine
    Nên tôi thấy tò mò. Có cạm bẫy ẩn nào không? Đây có phải là kiểu công cụ làm chuyện gì đó không hay để đạt được kết quả không? Hay là một kiểu đùa cợt hoặc trolling theo phong cách tom7 mà tôi không hiểu vì không rành sâu về compiler hay runtime? Hay thật sự đây chỉ là những công cụ rất sáng tạo nhưng vẫn chưa được phổ biến rộng rãi?

    • APE hoạt động bằng một mẹo cực kỳ tinh vi có thể bị chặn bất cứ lúc nào, và trên OpenBSD thì thực tế đã bị chặn
      Phần lớn những người làm phần mềm đa nền tảng không muốn một file thực thi duy nhất chạy trên mọi nền tảng, mà muốn một codebase duy nhất hoạt động đúng trên từng nền tảng được hỗ trợ
      Từ góc nhìn đó, những ngôn ngữ như Go, nơi bạn có thể cross-compile sang mọi mục tiêu nếu tránh CGO, mang lại trải nghiệm dễ chịu hơn. Nhưng “phép màu” chạy được theo ba cách của APE, dù rất thông minh, vẫn không tạo cảm giác đáng tin rằng nó sẽ hoạt động mãi mãi, và với đa số trường hợp cũng không đem lại nhiều lợi ích thực tế
      Mỗi nền tảng đều có yêu cầu đóng gói và ký số riêng, nên tốt hơn là compile riêng cho từng mục tiêu nền tảng
    • Cá nhân tôi thấy cosmo và ape rất thông minh, nhưng nếu các công cụ bình thường đã hoạt động tốt thì trong công việc không cần kiểu thông minh này
      Ví dụ, nếu bạn đã có thể cross-compile dự án sang các hệ điều hành và nền tảng khác, hoặc đã có hạ tầng build cho việc đó, thì không có lý do gì phải đi tìm cách tạo ra một binary duy nhất chạy ở mọi nơi
      APE cũng dùng những cú hack thông minh để chạy được trên nhiều hệ điều hành. Nhưng nếu các định dạng executable tiếp tục tiến hóa và một ngày nào đó những cú hack đó bị phá vỡ thì sao? Nếu đến lúc đó không ai có thời gian sửa APE để theo kịp thay đổi thì sao?
      Ngược lại, những công cụ nhàm chán như gcc, clang, go, rust sẽ tiếp tục được cập nhật và vẫn hoạt động trên các hệ điều hành đang tiến hóa. Vì vậy tôi cứ ở lại với phía nhàm chán. Lý do tôi không bận tâm đến thứ thông minh là vì thứ nhàm chán đơn giản là hoạt động tốt với tôi
    • llamafile của Mozilla dùng thứ này. Nó gói trọng số mô hình và file thực thi làm một để có thể chạy trên mọi nền tảng cosmo/ape, đồng thời khởi chạy cả máy chủ HTTP redbean để tương tác
      Bạn cũng có thể chạy mà không cần trọng số được tích hợp sẵn, và để nó đọc trọng số từ filesystem. Đây có thể là cách dễ nhất để khiến local LLM thực sự trở thành kiểu “tải về rồi chạy ngay”
    • Cosmopolitan lúc nào cũng cho cảm giác như một lỗ hổng kỹ thuật thú vị rất hợp để viết blog. Đây là kiểu thứ gần như chắc suất lên trang nhất ở những nơi như HN chỉ nhờ vào sự khéo léo và mức độ ám ảnh với thiết lập
      Nhưng để dùng như công nghệ nền tảng kiểu libc thì nó chủ yếu trông hữu ích cho những món đồ chơi thú vị hoặc các dự án cá nhân nhỏ
      Trong bối cảnh đó, khi nó được đưa ra như một phương án thay thế nghiêm túc cho glibc, musl, msvcrt thì tôi thấy hơi lạ. Đây là một cú hack rất dễ thương, nhưng nếu bắt gặp nó trong thứ mà tôi phụ thuộc một cách nghiêm túc thì có lẽ tôi sẽ khá hoang mang
    • Mozilla có dự án Llamafile dựa trên Cosmopolitan libc: https://github.com/Mozilla-Ocho/llamafile
      Trên Hugging Face họ cũng thường xuyên đăng các mô hình phổ biến được đóng gói lại theo định dạng đó: https://huggingface.co/models?search=llamafile
      Tuy vậy, việc nó có tính thực dụng vượt quá mức thử nhanh các mô hình nhỏ hay không lại là một câu chuyện khác
  • Nếu tốt đến vậy thì tôi thắc mắc vì sao không phải mọi thư viện C đều áp dụng cùng một mẹo đó
    Tôi đoán là những mẹo đó chỉ luôn nhanh trong một số kiến trúc nhất định, một số mẫu CPU nhất định, hoặc một số khối lượng công việc hay kiểu truy cập nhất định. Nếu benchmark đúng cách trên nhiều loại workload khác nhau, trên toàn bộ phần cứng được hỗ trợ, có thể sẽ không cho ra cùng lợi thế đó
    Hoặc cũng có thể ngữ nghĩa của API pthread mà Cosmopolitan đang cố triển khai hơi khác một chút, và cách triển khai này không tuân thủ đặc tả một cách nghiêm ngặt
    Thật khó tưởng tượng rằng các tác giả libc lại không theo kịp các nghiên cứu mới nhất về các primitive của hệ điều hành

    • Các dự án như vậy có hàng chục ưu tiên khác ngoài chỉ một API cụ thể. Ám ảnh với từng API riêng lẻ không phải là cách tốt để dùng quỹ thời gian hạn chế. Và để phản biện, chỉ cần nhìn vào malloc và các routine chuỗi trong libc phổ biến trên Linux
      malloc của glibc tạm dùng được, nhưng về tổng thể tốc độ và khả năng mở rộng thì dễ bị các lựa chọn hiện đại hơn vượt qua. Nó bị phân mảnh nặng, càng về lâu dài càng tệ, và còn có nhiều tham số tinh chỉnh như MALLOC_ARENA_MAX có ảnh hưởng lớn tới workload thực tế. malloc của musl thì tệ khủng khiếp ở mọi cấp độ về mặt hiệu năng. Dùng allocator của musl trong chương trình đa luồng sẽ phá hỏng hiệu năng rất nặng, gần như có thể xem là cẩu thả
      musl cũng không có những routine so sánh chuỗi tối ưu bằng SIMD. Nếu biết có bao nhiêu chu kỳ CPU bị tiêu tốn cho việc này trong các chương trình không tầm thường, bạn sẽ ngạc nhiên; nó hiện rõ trong profile thực tế, và cải thiện nó sẽ giúp gần như mọi chương trình tốt lên trên diện rộng. Các routine tối ưu của glibc thì tốt, nhưng vẫn có vẻ còn có thể nhanh hơn nữa
      Đây không phải kiểu “tối ưu chỉ dành cho một kiến trúc nên không thể khái quát”. Đặc biệt hai mảng này là những lĩnh vực đã được nghiên cứu và hiểu khá kỹ, có thể giảm thời gian thực tế 2–5 lần trong gần như mọi workload, đồng thời cải thiện mạnh việc tận dụng working set dài hạn. Vậy tại sao chưa áp dụng? Như thường lệ, có lẽ vì còn việc khác phải làm, hoặc vì có những ưu tiên đánh đổi khác như musl, nơi sự đơn giản được ưu tiên hơn hiệu năng tối đa
      Đây không phải là trách cứ các dự án đó. Không ai nói “chương trình của tôi chậm khủng khiếp và được thiết kế để không làm gì ra hồn, và tôi tự hào về điều đó”. Chỉ là ý tưởng cho rằng những người làm dự án đó đã thiết kế bằng cách chỉ chọn đúng biên Pareto hoàn hảo thì hoàn toàn không thực tế, và không phản ánh cách đa số dự án thực sự vận hành
    • Có thể là do chính trị, hội chứng NIH, hoặc các maintainer đã quá lâu năm
      Muốn thay đổi thứ gì đó trong glibc hay phía tương đương bên C++ thì mất cả đời
      Có nhiều loại primitive đồng bộ khác nhau, còn pthreads chỉ hỗ trợ một phần trong số đó. Tự giới hạn mình vào đó thì nhìn chung là đổi hiệu năng lấy tính di động
    • Tôi thắc mắc câu “thật khó tưởng tượng rằng các tác giả libc lại không theo kịp các nghiên cứu mới nhất về các primitive của hệ điều hành” có phải là nói mỉa không
      Tôi không biết về các maintainer libc, nhưng với tư cách người đang maintain vài thứ, tôi không cố triển khai các nghiên cứu mới nhất. Tôi cố giữ ổn định và xác nhận hiệu năng ở mức chấp nhận được. Các bản triển khai kiểu nghiên cứu nằm ngoài ngân sách “bảo trì” của tôi
    • Tôi thắc mắc liệu có các cân nhắc ABI nào khi thay đổi cách triển khai pthread mutex không
    • Câu hỏi “nếu tốt đến vậy thì tại sao không phải mọi thư viện C đều áp dụng cùng một mẹo?” làm tôi nhớ đến chuyện đùa này
      Một người đàn ông và một nhà thống kê đang đi bộ thì thấy tờ 50 euro. Nhà thống kê cứ đi tiếp, còn người đàn ông dừng lại và nói: “Nhìn kìa, có tiền trên mặt đất.” Nhà thống kê đáp: “Chắc là giả thôi. Nếu là thật thì đã có người nhặt rồi,” rồi tiếp tục bước đi. Một người khác đến và nhặt tờ tiền đó
  • Thread và mutex là những thứ khiến khoa học máy tính trở nên phức tạp nhất. Tôi luôn hoài nghi với các cách triển khai mới cho đến khi chúng được dùng ở quy mô lớn trong nhiều năm
    Bug trong các cơ chế threading như thế này thường xuyên lọt qua cả những đợt rà soát gắt gao nhất. Khi Java xuất hiện vào giữa thập niên 90, đủ loại bug về thread và mutex của Solaris đã lộ ra
    Điều cần không phải là triển khai mutex nhanh nhất mà là triển khai đáng tin cậy

    • Mutex còn lâu mới là thứ “phức tạp nhất”. Cũng không có quá nhiều cách để triển khai nó cho hiệu quả. Trong đa số trường hợp, nhất là trên đường đọc, tốt nhất là tránh nó đi
  • Đoạn mã này benchmark tranh chấp mutex chứ không phải hiệu năng khóa mutex. Nếu bạn đang dùng lock theo kiểu này thì nên đánh giá lại mã của mình
    Mỗi lần một thread tăng g_chores thì nó lại khóa rồi mở mutex. Điều này tạo ra overhead vì phải lấy và nhả mutex quá thường xuyên, và lặp lại 100.000 lần cho mỗi thread
    Overhead đó che khuất chênh lệch hiệu năng thực sự giữa các cơ chế lock. Vì benchmark bị chi phối bởi tranh chấp lock chứ không phải công việc thực tế. Kiểu benchmark này là vô dụng

  • Tôi là fan của Justine và công việc đó, nhưng đây có lẽ là một trong những test case benchmark mutex kém thú vị nhất. Tình huống nhiều thread cứ liên tục đập vào cùng một mutex là thứ vốn nên tránh ngay từ đầu
    Vì vậy tôi thấy việc implementation mutex nào xử lý trường hợp này tốt nhất không có gì đặc biệt thú vị

    • Tôi tò mò bạn nghĩ test case benchmark tốt cho mutex nên là gì
    • Phần lớn các trường hợp tôi dùng lock hay semaphore đều nằm quanh những tài nguyên rất đắt đỏ. Mức sử dụng tài nguyên đó lấn át hoàn toàn overhead hiệu năng của lock
    • Vậy thì nên đo cái gì? Trường hợp không có tranh chấp là quan trọng và là baseline, nhưng ngoài ra đây chính là điểm yếu của mutex. Nếu xử lý tranh chấp kém thì phần cứng sẽ bị nhàn rỗi, scheduler phải làm nhiều hơn, hoặc số lần vào kernel tăng lên
      Tôi còn quên một điều quan trọng: lock có hiệu năng kém khi tranh chấp còn có thể gây ra các hiệu ứng hệ thống rất tiêu cực, như tạo hotspot trên mạng bộ nhớ, và điều này cũng sẽ lộ ra ở đây
    • Tôi không thể hoàn toàn đồng ý với nhận định rằng “nhiều thread không nên liên tục đập vào cùng một mutex”
      Tôi có thể nghĩ ra vài trường hợp nhiều thread cùng dồn vào một mutex. Ví dụ đơn giản là cùng lúc điền dữ liệu vào một cấu trúc dữ liệu như list hoặc dictionary
      Có thể làm bằng truyền thông điệp, nhưng sẽ tốn thêm bộ nhớ và có thể chậm hơn việc chờ để ghi vào một vị trí dùng chung
  • Production không phải về tốc độ, hiệu suất, hay thứ rõ ràng là “mánh khóe thông minh”
    Nếu phải hy sinh 50% hiệu suất để có bảo đảm rằng tôi sẽ không bị gọi lúc 3 giờ sáng Chủ nhật để đi sửa một hệ thống bị hỏng, tôi sẽ chọn như vậy mọi lần
    Production là về độ tin cậy, và viết mã đáng tin cậy khó hơn gấp 10 lần so với viết mã “nhanh”