1 điểm bởi GN⁺ 2023-10-17 | 1 bình luận | Chia sẻ qua WhatsApp
  • C từng phù hợp với việc trừu tượng hóa phần cứng trong thời PDP-11, nhưng trên CPU hiện đại, mô hình trừu tượng của C là thực thi tuần tự và bộ nhớ phẳng lệch rất xa với phần cứng thực tế
  • Spectre và Meltdown có liên hệ với việc bộ xử lý phụ thuộc mạnh vào dự đoán nhánh, thực thi suy đoán và song song mức lệnh để chạy nhanh mô hình tuần tự của C
  • Để làm mã C chạy nhanh, không chỉ cần chuyển đổi đơn giản sang mã máy mà cần các tối ưu hóa phức tạp ở quy mô LLVM/Clang, và một số tối ưu hóa có thể xung đột với ngữ nghĩa của C
  • Các quy tắc như nguồn gốc con trỏ, đệm trong struct, giá trị chưa khởi tạo, tràn số nguyên có dấu khiến kết quả thực thi khó dự đoán hơn và cũng dẫn tới lỗ hổng bảo mật
  • Mô hình phù hợp hơn với phần cứng hiện đại là tận dụng nhiều luồng, đơn vị vector rộng và mô hình bộ nhớ đơn giản hơn, nhưng tính tương thích với mã C hiện có vẫn là ràng buộc lớn nhất

Vì sao C từng trông giống một ngôn ngữ “cấp thấp”

  • Nếu là ngôn ngữ cấp thấp, các trừu tượng mà phần cứng cung cấp phải dễ dàng tương ứng với máy trừu tượng của ngôn ngữ
  • Trên PDP-11, C có thể được xem là ngôn ngữ cấp thấp
    • Chương trình được thực thi tuần tự
    • Bộ nhớ được xử lý như một không gian phẳng
    • Các toán tử pre-increment và post-increment khớp tốt với cách định địa chỉ của PDP-11
  • Alan Perlis định nghĩa rằng “khi chương trình phải chú ý tới những thứ không liên quan thì ngôn ngữ đó là cấp thấp”, nhưng chỉ riêng định nghĩa này khó giải thích đầy đủ điều mà mọi người kỳ vọng ở ngôn ngữ cấp thấp là “gần phần cứng”

CPU hiện đại hoạt động như trình giả lập PDP-11 tốc độ cao

  • Nguyên nhân gốc rễ của Spectre và Meltdown không chỉ nằm ở việc tạo ra bộ xử lý nhanh, mà còn gắn với thiết kế bộ xử lý cố gắng phơi bày nhanh một máy trừu tượng kiểu PDP-11
  • Mã C, trước C11 nếu bỏ qua các mở rộng không chuẩn của nhà cung cấp, trên thực tế cung cấp một máy tuần tự hoàn chỉnh, và sau C11 phần lớn vẫn giữ máy trừu tượng tuần tự
  • CPU hiện đại trích xuất song song mức lệnh (ILP) để giữ cho các đơn vị thực thi luôn bận
    • Chúng kiểm tra các phép toán lân cận và phát hành song song các phép toán độc lập
    • Đổi lại việc cho phép lập trình viên chủ yếu viết mã tuần tự, độ phức tạp và điện năng tiêu thụ tăng lên
  • GPU vẫn có thể đạt hiệu năng cao mà không cần loại logic này, nhưng đòi hỏi chương trình song song tường minh

Spectre, Meltdown và cái giá của thực thi suy đoán

  • Bộ xử lý Intel hiện đại có thể giữ tối đa 180 lệnh ở trạng thái đang bay cùng lúc
  • Trong mã C, trung bình cứ khoảng 7 lệnh lại có một nhánh
    • Để lấp đầy pipeline trên một luồng đơn, cần phải đoán đích của 25 nhánh tiếp theo
    • Dự đoán sai tạo ra công việc bị bỏ đi sau khi đã thực hiện xong, đồng thời lãng phí điện năng
  • Spectre và Meltdown có thể khai thác tác dụng phụ quan sát được của những công việc bị loại bỏ này qua side channel
  • register rename engine trên các lõi hiệu năng cao hiện đại là một trong những nơi tiêu thụ lớn về diện tích khuôn và điện năng
    • Khi lệnh đang chạy thì khó có thể tắt hoặc cắt điện chúng
    • GPU không có các đơn vị như vậy; tính song song đến từ nhiều luồng

Mô hình bộ nhớ phẳng của C không khớp với thực tế cache

  • Bộ nhớ phẳng, cốt lõi của máy trừu tượng C, đã không còn khớp với phần cứng thực tế trong hơn 20 năm
  • Bộ xử lý hiện đại thường có 3 tầng cache giữa thanh ghi và bộ nhớ chính
    • Cache đúng như tên gọi của nó, bị che giấu khỏi lập trình viên và không hiện ra trong C
    • Để viết mã nhanh trên bộ xử lý hiện đại, phải dùng cache hiệu quả
  • Lập trình viên C phải hiểu không chỉ máy trừu tượng mà cả chi tiết cài đặt mới đạt hiệu năng
    • Ví dụ, hai giá trị được căn chỉnh 64 byte có thể vẫn nằm trong cùng một cache line

Độ phức tạp của trình biên dịch cần có để làm mã C chạy nhanh

  • Nếu là ngôn ngữ cấp thấp, đáng lẽ phải dễ chuyển thành mã nhanh mà không cần trình biên dịch phức tạp, nhưng C không như vậy
  • Clang và các phần LLVM liên quan có quy mô khoảng 2 triệu dòng mã
    • Chỉ tính riêng các pass phân tích và biến đổi cần để chạy C nhanh, bỏ chú thích và dòng trống, đã gần 200 nghìn dòng
  • Khi xử lý lượng dữ liệu lớn trong C, thường người ta viết vòng lặp xử lý tuần tự từng phần tử
    • Để chạy tối ưu trên CPU hiện đại, trước tiên trình biên dịch phải xác định tính độc lập giữa các lần lặp
    • Từ khóa restrict có thể bảo đảm rằng ghi qua một con trỏ sẽ không can thiệp vào đọc qua con trỏ khác
  • Fortran có lợi thế hơn C trong việc cung cấp loại thông tin này, và đó là một trong những lý do chính khiến C chưa thể thay thế Fortran trong tính toán hiệu năng cao

Xung đột giữa vector hóa và bảo đảm bố trí bộ nhớ của C

  • Nếu các lần lặp của vòng lặp độc lập, trình biên dịch sẽ cố gắng vector hóa kết quả
    • Bộ xử lý hiện đại có thể đạt thông lượng cao hơn 4–8 lần với mã vector so với mã scalar
  • Với ngôn ngữ cấp thấp cho các bộ xử lý như vậy, việc có kiểu vector native với độ dài tùy ý là điều tự nhiên
    • LLVM IR cung cấp mô hình như vậy vì việc chia phép toán vector lớn thành phép toán nhỏ dễ hơn làm ngược lại
  • Các bảo đảm về bố trí bộ nhớ của C xung đột với tối ưu hóa
    • Có thể dùng thay thế lẫn nhau các struct có cùng prefix
    • Offset các trường của struct bị lộ ra ở cấp ngôn ngữ
    • Trình biên dịch khó có thể đổi thứ tự trường hoặc chèn thêm padding để cải thiện vector hóa
  • Khả năng kiểm soát tinh vi bố trí cấu trúc dữ liệu có thể là ưu điểm của ngôn ngữ cấp thấp, nhưng đồng thời cũng khiến việc làm C chạy nhanh trở nên khó hơn

Vấn đề của padding, SROA và loop unswitching

  • C yêu cầu padding ở cuối struct để bảo đảm không có padding bên trong mảng
  • Struct phải hỗ trợ so sánh không phụ thuộc kiểu như memcmp, nên sao chép struct cũng phải giữ nguyên cả padding
    • Một số thử nghiệm cho thấy tỷ lệ đáng kể của tổng thời gian chạy trên một số workload được dùng để sao chép padding
  • SROA là tối ưu hóa cố gắng thay struct và mảng độ dài cố định bằng các biến riêng lẻ
    • Nó cho phép xử lý truy cập một cách độc lập và loại bỏ các phép toán mà kết quả không quan sát được
    • Trong một số trường hợp nó loại bỏ padding, nhưng không phải lúc nào cũng vậy
  • loop unswitching là tối ưu hóa đưa điều kiện ra ngoài vòng lặp và tạo vòng lặp ở cả hai nhánh
    • Điều này xung đột với ý niệm rằng khi mã ngôn ngữ cấp thấp chạy, lập trình viên biết mã nào chạy và chạy lúc nào
    • Nó cũng gây vấn đề với các khái niệm unspecified value và undefined behavior trong C

Giá trị chưa khởi tạo và hành vi không xác định

  • Trong C, đọc một biến chưa khởi tạo sẽ tạo ra unspecified value, và mỗi lần đọc có thể cho giá trị khác nhau
  • Quy tắc này cho phép các hành vi như tái sử dụng trang theo kiểu trì hoãn
    • Cài đặt malloc của FreeBSD thông báo cho hệ điều hành về các trang hiện không dùng
    • Hệ điều hành có thể xem lần ghi đầu tiên vào trang như tín hiệu rằng trang đó lại cần thiết
  • Nếu unspecified value được dùng cho điều khiển luồng thì sẽ thành undefined behavior
    • Ví dụ như dùng một giá trị chưa khởi tạo trong điều kiện if
  • Với loop unswitching, nếu vòng lặp chạy 0 lần thì trong mã gốc toàn bộ thân vòng lặp là mã chết
    • Sau unswitching, chương trình có thể rẽ nhánh dựa trên biến có thể chưa được khởi tạo
    • Nghĩa là mã chết biến thành hành vi không xác định
  • Có thể làm mã C chạy nhanh, nhưng để tạo một trình biên dịch đủ thông minh cần tới hàng nghìn năm-người, và đôi khi còn phải vi phạm một số quy tắc của ngôn ngữ

Vì sao C trở nên khó hiểu

  • Với ngôn ngữ cấp thấp, lập trình viên phải dễ hiểu được sự tương ứng giữa máy trừu tượng và máy vật lý thực tế
  • Trên PDP-11, biểu thức C dễ dàng ánh xạ thành một hoặc hai lệnh, còn biến cục bộ và kiểu nguyên thủy cũng tương ứng đơn giản với phần cứng
  • Về sau, các bản cài đặt C ngày càng phức tạp để duy trì ảo tưởng về mã nhanh và sự tương ứng với phần cứng
  • Một khảo sát năm 2015 với lập trình viên C, tác giả trình biên dịch và thành viên ủy ban tiêu chuẩn đã cho thấy vấn đề về khả năng hiểu của C
    • Khi khởi tạo một struct về 0 rồi đặt một số trường, 36% tin chắc rằng toàn bộ bit padding sẽ là 0, còn 29% trả lời là không biết
    • Kết quả thực tế có thể thay đổi tùy theo trình biên dịch và mức tối ưu hóa

Nguồn gốc con trỏ và lỗ hổng bảo mật

  • Mô hình BCPL tương đối đơn giản: giá trị là word, và mỗi word либо là dữ liệu, либо là địa chỉ của dữ liệu
  • Mô hình C được thiết kế để có thể triển khai trên nhiều đích khác nhau, bao gồm kiến trúc phân đoạn và máy ảo có garbage collection
  • Để tránh vấn đề trên các hệ thống như vậy, chuẩn C hạn chế các phép toán hợp lệ trên con trỏ
  • C Defect Report 260 đã đưa khái niệm pointer provenance vào định nghĩa con trỏ
    • Bản cài đặt có thể theo dõi nguồn gốc của mẫu bit
    • Ngay cả khi giống nhau ở mức bit, con trỏ có nguồn gốc khác nhau vẫn có thể được phân biệt
  • Từ provenance không xuất hiện trong đặc tả C11, nên tác giả trình biên dịch phải tự quyết định ý nghĩa
    • GCC và Clang khác nhau về việc provenance có được giữ nguyên hay không khi con trỏ được chuyển sang số nguyên rồi lại chuyển về con trỏ
  • Đã ghi nhận các lỗ hổng bảo mật trong mã có signed integer overflow và mã dereference con trỏ trước khi null check
    • Vì dereference null pointer là undefined behavior trong C, trình biên dịch có thể giả định rằng con trỏ đã bị dereference thì không thể là null
    • Một ví dụ là CVE-2009-1897

Hình dung bộ xử lý không dành cho C

  • Các bản sửa lỗi được đề xuất cho Spectre và Meltdown áp đặt mức phạt hiệu năng đáng kể, làm triệt tiêu phần lớn tiến bộ vi kiến trúc của 10 năm qua
  • Đã đến lúc suy nghĩ lại mô hình lập trình phù hợp với bộ xử lý nhanh, thay vì chỉ cố làm mã C chạy nhanh
  • Các chip đa luồng mạnh như Sun/Oracle UltraSPARC Tx không cần quá nhiều cache để lấp đầy các đơn vị thực thi
    • Nếu có đủ song song mức cao, có thể tạm dừng luồng đang chờ bộ nhớ và lấp đầy đơn vị thực thi bằng lệnh từ luồng khác
    • Vấn đề là chương trình C có xu hướng có ít luồng bận rộn
  • ARM SVE (Scalar Vector Extensions) là ví dụ về giao diện tốt hơn giữa chương trình và phần cứng
    • Các đơn vị vector truyền thống phơi bày phép toán vector kích thước cố định và kỳ vọng trình biên dịch điều chỉnh thuật toán theo kích thước đó
    • SVE cho phép lập trình viên mô tả mức độ song song sẵn có, còn phần cứng ánh xạ nó theo số lượng đơn vị thực thi
    • Trong C, autovectorizer phải suy ra tính song song từ cấu trúc vòng lặp nên phức tạp, còn với phép map theo phong cách hàm, độ dài mảng được ánh xạ chính là mức độ song song sẵn có nên việc sinh mã đơn giản hơn

Mô hình bộ nhớ đơn giản hơn và lập trình song song

  • Trên CPU hiện đại, cache coherency protocol là một trong những phần khó làm vừa nhanh vừa đúng nhất
  • Phần lớn độ phức tạp xuất phát từ việc phải hỗ trợ các ngôn ngữ kỳ vọng dữ liệu có thể được chia sẻ và thay đổi
  • Trong máy trừu tượng kiểu Erlang, mọi đối tượng либо là cục bộ theo luồng, либо là bất biến
    • Erlang có mô hình đơn giản hơn, trong đó mỗi luồng chỉ có một đối tượng có thể thay đổi
    • Giao thức nhất quán cache cho hệ thống như vậy có thể được chia thành hai trường hợp: mutable hoặc shared
  • Đối tượng bất biến có thể làm cache đơn giản hơn và giúp nhiều phép toán rẻ hơn
    • Project Maxwell của Sun Labs chỉ ra rằng tập đối tượng trong cache và tập đối tượng được cấp phát vào young generation gần như là cùng một tập
    • Nếu đối tượng chết trước khi bị đẩy khỏi cache thì không cần ghi lại vào bộ nhớ chính, giúp tiết kiệm điện
    • Dùng đối tượng bất biến trên heap và stack có thể thay đổi giúp garbage collector trở thành một máy trạng thái đơn giản, dễ triển khai bằng phần cứng
  • Bộ xử lý được thiết kế thuần túy cho tốc độ nhiều khả năng sẽ hỗ trợ nhiều luồng, đơn vị vector rộng và mô hình bộ nhớ đơn giản hơn
    • Việc chạy mã C trên các hệ thống như vậy có thể là vấn đề
    • Vì có quá nhiều mã C legacy trên toàn thế giới, rất khó để thành công về mặt thương mại
  • Quan niệm phổ biến rằng lập trình song song là khó thực ra đúng hơn với việc lập trình song song trong các ngôn ngữ có máy trừu tượng kiểu C
    • Alan Kay từng dạy ngôn ngữ actor-model cho trẻ em, và chúng viết được các chương trình hoạt động với hơn 200 luồng
    • Lập trình viên Erlang thường viết các chương trình có hàng nghìn thành phần song song
    • Trong bối cảnh CPU đa lõi và GPU many-core được dùng rộng rãi, C không còn ánh xạ tốt tới phần cứng hiện đại

1 bình luận

 
GN⁺ 2023-10-17
Các ý kiến trên Hacker News
  • Lý do C là ngôn ngữ cấp thấp ít nhất là vì quản lý bộ nhớ thủ công
    Đặc biệt trên phần cứng hiện đại, quản lý bộ nhớ nằm ở trung tâm của lập trình. Việc Rust quảng bá tính an toàn bộ nhớ mà không cần garbage collector rốt cuộc cũng là vì lý do cốt lõi khiến Rust tồn tại gần với quản lý bộ nhớ. Lý do C nhanh cũng là bộ nhớ, và lý do C không an toàn phần lớn cũng là bộ nhớ. Một trong những lý do lớn khiến tính toán song song khó là truy cập bộ nhớ đồng thời. Lập trình hàm thường được bao quanh bởi các khái niệm toán học, nhưng phần đáng kể nằm ở việc khiến đối tượng giả vờ là bất biến, còn bên trong thì trình biên dịch xử lý bộ nhớ khả biến
    Trong C, nếu dùng allocator thì mọi lời gọi đó đều là tường minh. new/delete và con trỏ thô trong C++ cũ cũng gọi allocator một cách tường minh, nhưng cũng có nhiều việc tự động diễn ra trong destructor. Smart pointer của C++ hiện đại về bản chất giống các ngôn ngữ có garbage collection ở chỗ cả cấp phát lẫn giải phóng đều diễn ra tự động

    • Đúng, nhưng ngay cả trong C cũng không thể quản lý bộ nhớ ở mức thấp theo đúng cách bộ xử lý thực sự làm
      Bạn không thể chỉ thị cho bộ xử lý đặt dữ liệu nào ở tầng cache nào, đưa thứ gì vào bộ nhớ ảo, v.v. Nó cấp thấp hơn Python, nhưng khó có thể coi là quản lý bộ nhớ cấp thấp như C thời PDP-11
    • Bản thân “bộ nhớ” cũng là một trừu tượng nằm trên một mô hình phức tạp hơn nhiều: bộ nhớ ảo/page, và hầu hết lập trình viên làm việc mà không thật sự biết rõ điều này
      Với các hệ thống cấp vi điều khiển hoặc hệ thống không có MMU thì câu chuyện khác, nhưng đó lại là vấn đề riêng
      Ngay cả tôi, một lập trình viên Rust, cũng làm việc trong ảo tưởng rằng con trỏ là một đối tượng vật lý thực sự như địa chỉ bộ nhớ. Rust, và ở mức nào đó C++, đặt các trừu tượng quản lý như tham chiếu và mượn lên phía trước, nhưng khái niệm cốt lõi vẫn còn nguyên
      Trên thực tế, kernel hệ điều hành đặt một tầng khổng lồ giữa bộ nhớ vật lý và chương trình, còn “địa chỉ” và “con trỏ” gần giống các handle mà OS và MMU xử lý đủ kiểu
      “Con trỏ thô” thật ra cũng không thô sơ. Nó là một handle tới offset trong một page, còn page thực có thể nằm rải rác khắp nơi. Nếu rời hẳn libc và mô hình C để đi vào thế giới các tham chiếu thuần tương tác trực tiếp với các page của subsystem VM, kiểu như một “object handle”, thì thậm chí có thể còn gần hơn với cách subsystem bên dưới thực sự vận hành
    • Quản lý bộ nhớ của C tự thân nó cũng là một trừu tượng. mallocfree là các hàm thư viện
      Phần cứng không có kiểu cấp phát theo từng byte như vậy, nên không chỉ là trừu tượng trên phần cứng mà còn trừu tượng hóa cả cách hệ điều hành cấp phát bộ nhớ
      Trong C, bạn cũng không truy cập trực tiếp được vào stack. Stack frame đã được trừu tượng hóa, và thứ bạn có thể dùng chỉ cỡ longjmp
      Nếu tính cả undefined behavior và strict aliasing rules, quyền được tùy ý chọc ngoáy bộ nhớ cũng không nhiều đến vậy
    • BASIC cũng có thể quản lý bộ nhớ thủ công, và từng chiếm một thế hệ trên những máy tính không thể triển khai đầy đủ ISO C
    • C thậm chí còn không xử lý an toàn toàn bộ số học số nguyên. Đây là một ngôn ngữ thật sự cố nhồi nhét sự không an toàn vào
  • Với tư cách là một lập trình viên C và người viết compiler, đối với những người hiểu C và dùng nó một cách chuyên nghiệp, C rõ ràng là ngôn ngữ cấp thấp
    Nếu đang tìm một ngôn ngữ cấp thấp, C và các họ hàng của nó là những lựa chọn tốt nhất
    Nếu bạn mới học C và muốn biết cách dùng nó như chuyên gia, tốt hơn nên bỏ qua bài này. Nó có thể chỉ khiến bạn rối thêm và làm giảm khả năng dùng C hiệu quả

    • C là ngôn ngữ cấp thấp, nhưng là ngôn ngữ cấp thấp sai kiểu
      Nó cung cấp cách tiếp cận cấp thấp tới một cỗ máy mà máy thật phải khá vất vả để giả lập. Những cơ cấu lỏng lẻo và các miếng vá được thêm vào qua nhiều năm để tiếp cận máy thật là những yếu tố tương đối lạc lõng trong C
      Tuy vậy, tôi đồng ý rằng tiêu đề có phần cường điệu về mặt tu từ. Việc là một ngôn ngữ cấp thấp sai kiểu không làm nó trở thành ngôn ngữ cấp cao. WASM cũng sẽ là “sai” nếu tuyên bố ánh xạ trực tiếp lên phần cứng hiện đại, nhưng điều đó không khiến nó thành cấp cao
      Bản thân việc C là một ánh xạ tệ không phải điều gây bực bội. Nó là ngôn ngữ từ thập niên 1970 nên có thể hiểu được, và đến nay rõ ràng vẫn hữu ích trong nhiều trường hợp. Điều bực bội hơn là C vẫn chi phối mạnh thiết kế ngôn ngữ, và ảnh hưởng rất sâu tới cách các nhà thiết kế ngôn ngữ nhìn phần cứng. Vì thế, quá nhiều thiết kế ngôn ngữ hiện đại chỉ dừng ở việc xáo trộn lại các mảnh của C, thay vì tạo ra ngôn ngữ khớp tốt với phần cứng
    • Nói với tư cách thành viên WG14, C là ngôn ngữ cấp thấp nhưng không phải assembler khả chuyển
      Nếu bạn nghĩ code mình viết sẽ có quan hệ một-một với assembly thì sẽ gặp vấn đề. Nếu muốn tìm hiểu sâu hơn những điểm này có thể khiến bạn vấp thế nào, hãy xem https://youtu.be/w3_e9vZj7D8
    • Tác giả đang chơi trò chữ về ngữ nghĩa
      Trọng tâm của tác giả hẳn không phải là “C không phải ngôn ngữ tốt cho lập trình hệ thống”. Khó mà viết một thứ tương đương như volatile int *dma_register = SCATTER_GATHER_BASE; trong Haskell
      Ý của tác giả là động lực khiến C và các ngôn ngữ “mô hình hóa máy von Neumann” khác chạy nhanh đã làm compiler trở nên rất phức tạp, và tác giả ngụ ý rằng “nếu là cấp thấp thì cần compiler đơn giản”. Các processor được tạo ra để chạy nhanh loại code đó cũng rất phức tạp, và sự phức tạp ấy có chi phí
      Ở nhiều khía cạnh, đây là bài kêu gọi chuyển đổi mô hình lập trình, và GPU được nêu như ví dụ cho khả năng xuất hiện khi “mô hình lập trình mới” và “silicon hỗ trợ nó” được tạo ra cùng nhau
    • “Cấp thấp” là từ có nhiều nghĩa
      Nghĩa ban đầu gần với cách bài viết sử dụng hơn. Ngôn ngữ cấp thấp không có tính khả chuyển và gắn với phần cứng mà nó chạy trên đó, còn ngôn ngữ cấp cao có thể nhắm tới nhiều nền tảng. Theo định nghĩa này, C rõ ràng là ngôn ngữ cấp cao
      Điều tôi không hài lòng không hẳn là tác giả chơi chữ, mà là việc bám vào thuật ngữ cũ khiến cách hiểu bị mờ đi. Phân loại theo “thế hệ” thường mang tính giải thích tốt hơn
      Thế hệ 1 là mã máy, thế hệ 2 là assembly, thế hệ 3 là ngôn ngữ đa dụng, thế hệ 4 là ngôn ngữ chuyên biệt theo lĩnh vực ứng dụng
      Ranh giới giữa thế hệ 3 và 4 đôi khi mờ, và trong thập niên 80–90 cũng từng có nói đến thế hệ 5 nhưng cuối cùng không thật sự đứng vững. Dù vậy, SQL, HyperCard, Mathematica là những ví dụ khá rõ về ngôn ngữ thế hệ 4
      Lý do cách tiếp cận này tốt là vì nó phân loại theo khác biệt tương đối rõ về thời điểm dùng ngôn ngữ. Sau đó “cấp cao/cấp thấp” có thể được dùng như thuật ngữ tương đối. Ngôn ngữ càng cấp cao thì thường càng trừu tượng hóa nhiều chi tiết hơn về việc máy tính thật sự làm gì. Làm vậy vẫn giữ được điểm rằng các ngôn ngữ thuộc thế hệ cao hơn nhìn chung cấp cao hơn, và thứ mất đi chỉ là những tranh cãi ngớ ngẩn quanh một đường ranh hoàn toàn tùy tiện và, nói thật, vô dụng
      Theo cách này, cũng khá thú vị khi có thể xem .NET IL, WebAssembly, Java bytecode là các ngôn ngữ thế hệ 2 rất cấp cao. Và Forth là ngôn ngữ thế hệ 3. Chuck muốn phản biện thì cứ việc
    • Bài này có vẻ nói về thứ vượt xa sự nhàm chán của công việc hằng ngày
      Nó không bàn cách dùng búa, mà giống như đặt câu hỏi liệu cách dùng búa cho mọi thứ, tức thiết kế của C, có đang giới hạn chúng ta hay không
  • Tôi không đồng ý với lập luận của tác giả rằng tập lệnh CPU nên phơi bày nhiều hơn cách triển khai CPU
    Việc này từng được thử trong quá khứ và về dài hạn đã thất bại. Ví dụ là branch delay slot của một số processor RISC được thiết kế cuối thập niên 80 và đầu thập niên 90, như MIPS và SuperH. Giải thích cho người chưa biết khái niệm này: lệnh ngay sau lệnh rẽ nhánh sẽ được thực thi bất kể nhánh có được lấy hay không
    Trong ngắn hạn, điều này giao cho lập trình viên nhiệm vụ tránh pipeline stall sau nhánh, nhờ đó processor có thể đơn giản và rẻ hơn. Nhưng theo thời gian, thiết kế processor và pipeline trở nên phức tạp hơn, và chỉ một lệnh đơn lẻ không còn đủ để che độ trễ nhánh. Cuối cùng, vì tương thích, nó trở thành di sản mà các processor tương lai phải xử lý, làm logic dự đoán nhánh và pipeline phức tạp hơn

    • Tôi không nghĩ tác giả đang nói “hãy phơi bày các chi tiết triển khai ngẫu nhiên”
      Phơi bày sai chi tiết dĩ nhiên là tệ. Ý chỉ là trong thế giới CPU hiện đại, mô hình C có những hạn chế đáng kể
    • Một phản biện là việc kiểm soát prefetching một cách tường minh, hoặc cung cấp thêm “engine” để đưa dữ liệu vào cache theo mẫu mong muốn, đặc biệt có lợi cho ứng dụng thời gian thực và nhạy độ trễ
      Tôi từng nghe một bài trình bày về việc các developer sử dụng hệ thống con như vậy trên một processor nào đó. Nếu không dùng, họ dành 95% cửa sổ thời gian chỉ để copy dữ liệu, nhưng khi dùng engine đó để yêu cầu dữ liệu trước, việc lấy dữ liệu chỉ tốn 10% cửa sổ thời gian, và họ hoàn thành công việc mong muốn trong khoảng 50% tổng cửa sổ thời gian, còn dư nhiều thời gian cho tính năng bổ sung và cải tiến
      Nếu x86 có chức năng như vậy, hồi làm tiến sĩ tôi đã dùng nó để yêu cầu trước dữ liệu ma trận sẽ truy cập. Mẫu tôi dùng không tuyến tính nhưng được định nghĩa rõ. Hiện tại, nếu muốn tăng tốc thêm đoạn code đó, tôi phải sắp xếp lại ma trận để prefetcher thích hơn và refactor toàn bộ codebase từ trên xuống dưới
    • Itanium cũng tương tự phải không? Theo kiểu compiler gánh phần chi phí rẽ nhánh
    • Branch delay slot là ví dụ duy nhất tôi biết. Có ví dụ nào khác không?
      Nếu muốn thì tất nhiên có thể thiết kế rất tệ, nhưng tôi tò mò có bao nhiêu trường hợp lịch sử đủ để khái quát hóa
  • Tôi cho rằng từ cấp thấp đến cấp cao không phải là một lưỡng phân, mà là một phổ
    Có thể nói C nằm trong một phần ba thấp hơn trong số các ngôn ngữ, và để lộ cho lập trình viên nhiều primitive của máy như quản lý bộ nhớ và luồng. Dù không thấp như assembly, nó vẫn thấp hơn Java hay Go, và chắc chắn cách khá xa Python hay JavaScript

    • Thực ra, chuẩn C để phần lớn những gì thật sự phụ thuộc vào máy ở trạng thái không được định nghĩa, và chỉ để rất ít là do triển khai định nghĩa
      Hơn nữa, C khá không phù hợp với các nền tảng dùng bộ nhớ phân đoạn hoặc địa chỉ không phẳng. Có dấu hiệu những thứ như vậy đang muốn thịnh hành trở lại, và mức độ phổ biến rộng rãi của C đang thật sự là một rào cản rất lớn
    • Hỏi thật lòng: giữa C và assembly có ngôn ngữ nào không? Nếu có thì tôi chưa từng nghe đến
      Vì vậy mô hình trong đầu tôi luôn là “C là tầng thấp nhất có thể xuống tới trước khi phải ra lệnh trực tiếp cho bộ xử lý”
    • Ngay từ năm 1990 người ta đã giải thích như thế này
      “C không hoạt động như một ngôn ngữ ‘cấp cao’ điển hình. Đó là vì nó cung cấp nhiều chức năng thường gắn với các ngôn ngữ ‘cấp thấp’ như hợp ngữ hơn. Những chức năng này bao gồm khả năng ghi và đọc dữ liệu tại các địa chỉ bộ nhớ cụ thể, khả năng thực hiện phép toán trên nội dung của các vị trí bộ nhớ, các lệnh tăng/giảm biến số nguyên … Vì vậy C vừa cung cấp cho lập trình viên sự linh hoạt và hiệu quả khi làm việc ở cấp thấp, vừa cung cấp các lợi ích của những thao tác cấp cao như cấu trúc dữ liệu tiên tiến hơn và điều khiển luồng chương trình, vốn điển hình trong các ngôn ngữ máy tính ngày nay. Vì lý do này, C đôi khi được mô tả là ‘ngôn ngữ cấp thấp cấp cao’ hoặc ‘ngôn ngữ cấp cao cấp thấp’.” - https://archive.org/details/computerprogramm0000ford/page/13...
    • Với một bài có tiêu đề “C không phải là ngôn ngữ cấp thấp”, khó mà kỳ vọng độ chính xác
  • Câu ở cuối bài “trong phát triển phần mềm có một huyền thoại phổ biến rằng lập trình song song là khó” dễ gây hiểu lầm
    Tác giả có đưa ra các tình huống cụ thể không khó, nhưng nếu là câu hỏi áp dụng nói chung thì lập trình song song là khó, và đó không phải là một huyền thoại phổ biến
    Lập trình song song có khó không? Nếu hỏi mà không thêm điều kiện chi tiết hơn thì có. Việc khái niệm hóa các lệnh mã chạy đồng thời khó hơn nhiều so với việc chúng chạy tuần tự từng lệnh một

    • Khi lập trình (map inc [0 1 2 3]), độ khó trong việc hình dung hàm inc được thực hiện tuần tự trên từng phần tử so với được thực hiện song song có thật sự khác nhau không?
      Tôi nghĩ khó khăn của lập trình song song không hẳn là bẩm sinh, mà gần với hai điều hơn
      Thứ nhất, các ngôn ngữ thường mặc định chạy tuần tự, nên muốn bất đồng bộ thì phải đưa thêm primitive cho lập trình viên
      Thứ hai, cần biết khi nào nên dùng lập trình song song một cách hiệu quả
      Nếu có một danh sách hoặc luồng các phần tử độc lập chỉ cần tính toán độc lập, lập trình song song là trực quan
      Điểm mọi người mắc kẹt là cố nhét bất đồng bộ vào nơi không cần, tức nơi có hiệu năng bằng hoặc tệ hơn chạy tuần tự, hoặc đưa bất đồng bộ vào khi các phép tính thực ra phụ thuộc lẫn nhau và làm hỏng hành vi
    • Tôi không nghĩ câu này đúng. Nghĩ về phép toán ma trận không phức tạp, và định nghĩa một tác nhân đơn lẻ nên hành động thế nào trong môi trường cũng không phức tạp
      Khi nói “nếu không có thêm chi tiết hay tính cụ thể”, thực ra bạn đang dùng thế giới quan C/họ C làm khung mặc định
      Ý của tác giả là lập trình tuần tự chỉ là một kiểu lập trình đơn giản, không phải kiểu duy nhất, và nó không dễ khớp với phần cứng hiện đại
    • Đồng ý. Trong xử lý song song, không gian trạng thái khả dĩ lớn hơn nhiều, vì thế phức tạp hơn, và do đó khó hơn
      Việc Erlang tồn tại và được mọi người dùng thành công không có nghĩa là thứ khó hơn lại không khó
    • Lập trình đồng thời, tức làm nhiều việc khác nhau cùng lúc, là khó
      Triển khai thuật toán song song bằng hạ tầng lập trình đồng thời như process hay thread cũng khó. Nhưng lập trình song song, tức khiến nhiều phần tử xử lý cùng làm một việc, thì dễ hơn nhiều nếu có đúng abstraction
    • Tôi cho rằng bản thân việc hình dung các lệnh chạy song song không khó bằng việc điều phối hiệu quả và chính xác các tác vụ con song song đó
      Tuy nhiên có một số ngoại lệ như phép nhân ma trận
  • Bài này đúng ở chỗ máy tính không phải là một PDP-11 nhanh, nhưng sai ở chỗ cho rằng điều đó liên quan đến C
    Chẳng hạn có câu “một cốt lõi khác của mô hình bộ nhớ máy trừu tượng C là bộ nhớ phẳng. Điều này đã không còn đúng suốt hơn 20 năm”
    Điều này không liên quan đến C. Phần cứng áp đặt abstraction này. Và may là như vậy. Nếu không, chương trình sẽ dừng chạy khi chuyển sang máy có cache khác

    • Bài viết lập luận rằng một lý do đáng kể khiến phần cứng bám vào abstraction này là do sự thống trị của C
    • Đúng. Theo tiêu chí của bài này, nhiều yếu tố khiến C không còn là cấp thấp đã có trên mainframe IBM từ hàng chục năm trước x86
      Ví dụ là cấu trúc bộ nhớ phân cấp giả vờ như RAM phẳng, CPU lớn hơn nhiều so với những gì tập lệnh ngụ ý cùng thực thi ngoài thứ tự và suy đoán, và các trình biên dịch tối ưu hóa tách biệt hơn nữa chương trình được viết với việc thực thi thực tế
      IBM đã làm những thứ này trong thập niên 1970, rất lâu trước khi C trỗi dậy. Phê phán mô hình này và tìm giải pháp thay thế là hợp lý, nhưng đổ lỗi cho C thì không công bằng
    • Bộ nhớ phẳng có hại cho hiệu năng. Đặc biệt bộ nhớ phẳng có nhất quán cache còn tệ hơn. Nhưng tiện cho lập trình viên
  • Bài viết này giờ đã là bài viết từ 5 năm trước, và tiền đề rằng về mặt cấu trúc máy tính không còn giống PDP-11 cho lắm thậm chí còn đúng hơn, nhưng kết luận “hãy tưởng tượng các bộ xử lý không phải dành cho C” thì có vẻ kém thuyết phục hơn
    Chúng ta đang thấy sự tách biệt rõ rệt giữa mã tuyến tính và mã có mức song song rất cao, và điều đó đã đúng từ năm 2018. Ví dụ rõ nhất là sự trỗi dậy của Python trong học máy và tính toán khoa học. Khi hiệu năng không phải ưu tiên hàng đầu, viết theo kiểu đơn luồng và mô hình bộ nhớ phẳng vẫn rất tiện lợi
    Khi hiệu năng trở nên quan trọng, chuyển sang một ngôn ngữ phù hợp hơn cho lập trình song song là hợp lý. Chẳng hạn như ngôn ngữ đồ thị tính toán của những thứ như Pytorch, các tập primitive khác trên CUDA, hoặc các ngôn ngữ mang tính thử nghiệm hơn như Futhark. Mã lõi hiệu năng cao vốn luôn có các ngôn ngữ chuyên biệt theo miền, và có vẻ chúng đang trở nên phổ biến hơn chứ không ít đi. Phần cứng cũng đang được tạo ra theo hướng đó. Ví dụ là tổ hợp CPU+GPU thường thấy trong PC để bàn, các phần mở rộng vector của x86 với những primitive trên thực tế tạo thành DSL riêng, hay những thứ như M1 gắn GPU vào CPU để cả hai cùng truy cập nhanh vào cùng bộ nhớ hệ thống
    Nói cách khác, thứ thật sự lỗi thời có lẽ không phải là C, mà là khái niệm ngôn ngữ đa dụng phù hợp tốt như nhau cho mọi loại công việc

  • Nếu vì sự tinh vi của CPU hiện đại mà C không còn là ngôn ngữ “cấp thấp” nữa, thì cùng logic đó cũng áp dụng cho hợp ngữ
    Vì những thứ như thực thi ngoài thứ tự và đổi tên thanh ghi cũng áp dụng cho hợp ngữ
    Việc trình biên dịch đã trở nên tinh vi hơn trong vài thập kỷ gần đây cũng củng cố lập luận này. Ngay cả hợp ngữ do trình biên dịch C tạo ra, tức mã đích, cũng có thể khác với dự đoán do các tối ưu như đưa tính toán ra ngoài vòng lặp, loại bỏ biểu thức con chung, v.v.
    Dù vậy, tôi vẫn cho rằng khái niệm gọi C là ngôn ngữ “cấp thấp” vẫn là một nhãn hữu ích. Nếu không thì chính cách gọi này nên được cho nghỉ hưu

    • Hợp ngữ hơi lười biếng, được mở rộng macro, và ngay cả địa chỉ bộ nhớ của máy tính cũng là một khái niệm được tạo ra
      Đúng là nó là một lớp trừu tượng trên máy tính thật, nhưng ít hơn nhiều so với những gì C xây dựng trên mô hình máy tính ảo. Hợp ngữ hiện nay ở mức tương tự C vào thời C được tạo ra. C hiện nay quá cấp cao, đến mức không cung cấp chức năng nào không thể có được bằng các ngôn ngữ tốt hơn và hiện đại hơn
      Tuy nhiên, tôi đồng ý rằng ngày nay các tên gọi “cấp thấp” và “cấp cao” không còn hữu ích cho lắm
  • Bài viết dường như triển khai hai mạch lập luận khó dung hòa với nhau
    Thứ nhất là lập luận rằng C không phải ngôn ngữ cấp thấp, với các ví dụ như padding trong struct và tràn số nguyên có dấu là hành vi không xác định. Phần này dễ hiểu, và trông có tính xây dựng vì đề xuất các tính năng ngôn ngữ cho một ngôn ngữ “cấp thấp thật sự” giả định
    Thứ hai là lập luận rằng do sự thống trị của C, các nhà thiết kế CPU đã phải gắng sức tạo ra thứ gì đó chạy C một cách tự nhiên. Ở đây có các ví dụ như đổi tên thanh ghi, bộ nhớ phẳng, caching. Lập luận này cũng hiểu được, nhưng tôi không chắc nó kết nối thế nào với lập luận thứ nhất và với tiêu đề bài viết. Nếu hiểu theo nghĩa đen, nó có vẻ hàm ý rằng trên phần cứng hiện đại, bản thân việc tạo ra một ngôn ngữ cấp thấp là bất khả thi, và ngay cả mã máy cũng là “cấp cao”. Khi đó kết luận sẽ là trước hết phải tạo một thế hệ phần cứng mới phơi bày nhiều độ phức tạp hơn hẳn trong kiến trúc tập lệnh, rồi sau đó mới có thể thiết kế một ngôn ngữ cấp thấp khai thác nó
    Cả hai lập luận đều có giá trị, nhưng đặt chúng cùng trong một bài và đặt tiêu đề “C không phải là ngôn ngữ cấp thấp” thì hơi thiếu vững. Lập luận thứ nhất hợp với tiêu đề này, còn lập luận thứ hai có lẽ nên được xử lý trong một bài tiếp theo mang tên “Mã máy cũng không phải là ngôn ngữ cấp thấp”

    • IA-64 của Intel được biết đến là đã phơi bày các tầng thấp hơn của bộ xử lý ra mã máy
      Nhưng tôi nghe nói thời gian biên dịch rất lâu, và trình biên dịch rốt cuộc đã không đạt được mức tối ưu hóa như kỳ vọng. Việc nó không tương thích với x86 cũng không giúp ích cho việc được chấp nhận
  • Tôi nhớ đến VLIW. Theo bài viết về Itanium trên Wikipedia thì như sau:
    “Một word lệnh VLIW có thể chứa nhiều lệnh độc lập có thể được thực thi song song mà không cần đánh giá tính độc lập. Trình biên dịch phải cố tìm các tổ hợp lệnh hợp lệ có thể chạy đồng thời; về thực chất, nó thực hiện việc lập lịch lệnh mà các bộ xử lý superscalar truyền thống làm bằng phần cứng ở runtime.”
    Nếu CPU phơi bày tính song song trong một luồng ra giao diện, thì có thể xử lý ở thời điểm biên dịch, hoặc tự quyết định trực tiếp bằng assembly inline
    Tôi tò mò không biết lý do nó không trụ được là do động lực kinh doanh của ngành, hay vì chiến lược này thực sự có lý do kỹ thuật không tốt

    • Nếu tôi nhớ không nhầm thì nó không trụ được chủ yếu vì hai lý do
      Thứ nhất, trình biên dịch không giỏi kiểu lập lịch lệnh đó, và đến khi chúng được cải thiện thì Itanium đã chìm rồi. Thứ hai, các tập lệnh hiện hữu, tức x86, đã xử lý việc này khá tốt bằng phần cứng ở runtime, và trên thực tế cho kết quả hơi tốt hơn lập lịch tĩnh. Vì ở runtime có dữ liệu profiling
      Tôi nghĩ Linus đã để lại một bài rant hay có liên quan phần nào đến chủ đề này ở [0]. “Trong khi những người RISC cố tối ưu trình biên dịch để tạo ra các vòng lặp dùng hiệu quả cả 32 thanh ghi, những người triển khai x86 lại làm cho chip chạy nhanh dưới nhiều tải khác nhau và dùng phần cứng đổi tên thanh ghi khổng lồ. Chúng ta cũng đang thấy cả đổi tên bộ nhớ.”
      [0] https://yarchive.net/comp/linux/x86.html
    • Độ phức tạp có thể được đẩy qua lại ở một mức nào đó giữa trình biên dịch, runtime và phần triển khai bộ xử lý
      VLIW thật sự hoạt động tốt trong một số ngách. Nó khó lập trình, dù bằng tay hay bằng trình biên dịch, hơn so với một lệnh đơn thực thi theo thứ tự, nhưng lại đơn giản hóa việc lập lịch trong phần cứng. Nó hoạt động tốt hơn khi các lệnh được gom nhóm có độ trễ tương tự nhau
      Bài toán thiết kế cốt lõi hiện nay nằm ở chỗ truy cập bộ nhớ tốn nhiều chu kỳ hơn số học rất nhiều. Việc ghép một phép toán số học vài chu kỳ với một lệnh tải bộ nhớ vài trăm chu kỳ không có nhiều ý nghĩa. Vì vậy VLIW hoạt động tốt khi biết rằng truy cập bộ nhớ là nhanh, đại khái là khi biết nó sẽ nằm trong cache L1 hoặc nơi tương đương. Tôi nghĩ đó là một trong những lý do nó hợp với các hệ thống kiểu DSP
      Pipeline được phơi bày cũng là một đặc tính thú vị của một số hệ thống như vậy. Nếu một lệnh trong gói VLIW ghi vào một thanh ghi, các lệnh sau đó đọc cùng thanh ghi sẽ thấy giá trị cũ trong N chu kỳ tiếp theo, rồi sau đó việc ghi mới trở nên thấy được. Lập trình bằng tay thì thật sự rất rối, nhưng trình biên dịch có thể xử lý kiểu lập lịch đó
    • Lập lịch tĩnh rất tệ với tải không phải DSP, không phải HPC, nơi luồng điều khiển và luồng dữ liệu phụ thuộc mạnh vào đầu vào, như các ứng dụng server hay desktop thông thường
      Cho đến gần đây, DSP và HPC chỉ là một phần rất nhỏ của thị trường, nên các kiến trúc có khả năng lập lịch động đã nhận được nhiều đầu tư hơn và thống trị cả những thị trường đó
      Với GPU thì tất nhiên tình hình đã khác, và thực tế GPU phụ thuộc nhiều hơn vào lập lịch tĩnh. Nhưng khi GPU mở rộng sang các tải đa dạng hơn, chúng cũng dần có thêm nhiều yếu tố động hơn
    • Các bình luận khác đã nói về việc vì sao VLIW có khiếm khuyết về mặt kỹ thuật
      https://news.ycombinator.com/context?id=37900987
    • Tôi đang đọc rằng TeraScale (AMD) hoạt động theo kiểu này
      Itanium là nỗ lực lớn nhằm đưa ý tưởng này ra thị trường dưới dạng CPU. Hiện tại AMD64 và ARM đang thống trị, nhưng có lẽ tương lai ta sẽ lại thấy nó