Nếu một máy tính thực hiện được một triệu phép tính mỗi giây thì tại sao đoạn code này lại chạy mãi chẳng xong vậy?

  • Hỏi: Nếu một máy tính thực hiện được một triệu phép tính mỗi giây thì tại sao đoạn code dưới đây lại chạy mãi chẳng xong vậy?
for (int I = 0; I < 1000000; I++) { cout <<I; }
  • Đáp: bởi Steve Baker, Blogger tại LetsRunWithIt.com (từ 2013-nay)

Giải đáp

(Ví dụ) nếu bạn viết đoạn code NÀY bằng ngôn ngữ C hoặc C++ nhé:

for ( int i = 0 ; i < 1000000 ; i++ ) x += i ;

(Hãy lưu ý rằng tôi đã thay đổi đoạn mã để cộng tổng tất cả các số từ 0 tới 1000000 thay vì hiển thị các con số đó lên màn hình - RẤT quan trọng đấy!) …và dịch đoạn đó thành mã máy thì máy tính sẽ phải:

  • Kiểm tra xem liệu ‘i’ có còn nhỏ hơn 1 triệu nữa không.
  • Cộng ‘i’ vào ‘x’
  • Tăng ‘i’ lên một
  • Quay ngược lại đoạn ban đầu và làm lại hết tất cả

Có khoảng 4 đoạn lệnh mã máy - mỗi đoạn được thực thi một triệu lần - khoảng 4 triệu lệnh. Và do máy tính của bạn có thể thực hiện hàng TỶ lệnh mỗi giây nên đoạn code này có thể chạy rất nhanh... Chậc, còn dưới một một phần ngàn giây đối với những chiếc máy tính hiện đại nữa kia.

Giờ thì... Tất cả những điều tôi đang nói đều được đặt ra với giả thiết rằng bạn đang dùng C hoặc C++ và dịch chương trình đó ra mã máy nhé.

Nhưng câu hỏi bạn đặt ra ấy là:

for ( int i = 0 ; i < 1000000 ; i++ ) cout << i ;

Mới nhìn qua thì đối với máy tính, đoạn cout << i; không khác lắm so với x += i ; - nhưng thực sự lại "khoai" thật đấy!

Cứ thử nhìn xem máy tính sẽ phải làm gì để thực hiện một vòng lặp của đoạn mã đó nhé:

  • Kiểm tra xem liệu ‘i’ có còn nhỏ hơn 1 triệu nữa không.
  • Tăng ‘i’ lên một
  • Gửi ‘i’ tới bộ đầu ra chuẩn. Và điều đó đồng nghĩa với việc:
    • Việc chuyển đổi số ‘i’ đó thành một xâu theo chuẩn ký tự ASCII cơ số 10 và thêm cả một ký tự rỗng (null terminator)… đòi hỏi một hàm khá phức tạp, kiểm tra xem số đó có âm hay không (trường hợp này thì là không) và rồi lại phải thực hiện một vòng lặp làm vài thứ kiểu chia liên tiếp ‘i’ cho 10 và cộng mã ASCII của ‘0’ vào mỗi chữ số. Có lẽ điều này tương đương với khoảng một vài trăm phép xử lý máy tính - số càng lớn càng phải thực hiện nhiều lần hơn.
    • Gửi xâu chứa các chữ số đó tới “standard output” stream - giờ lại cần thêm một vài vòng lặp để sao chép mỗi ký tự ASCII đó vào một bộ đệm (buffer) trong object cout stream. Thêm vài chục câu lệnh nữa nhé.
    • Kiểm tra xem bộ đệm đó đã đầy chưa - chắc cứ vài trăm vòng lặp là phải làm một lần và nếu đầy thì sao...
    • Gửi bộ đệm đó tới hệ thống phân chia cửa sổ…lại HÀNG TRĂM câu lệnh nữa.
    • Hệ thống phân chia cửa sổ sẽ xác nhận xem bộ đệm đó sẽ được gửi tới cửa sổ hiện hành nào.
    • Tìm xem loại font đang được sử dụng là gì.
    • Kiểm tra xem xâu đó có chứa những ký tự như tab, dấu backspace hay newline hay không - hoặc xem nó có in đậm, nghiêng gì không.
    • Có thể nó sẽ cho rằng cửa sổ cần được cuộn lên và sẽ phải update lại thanh cuộn và chuyển mọi thứ khác lên trên. Vì bạn có thể tự mình dùng chuột cuộn cửa sổ đó lên nên máy cũng phải lưu lại những ký tự đó ở nơi nào đó để bạn có thể cuộn xem lại về sau. (Hàng triệu câu lệnh luôn!)
    • Sau đó nó phải gọi tới driver đồ họa để sử dụng card đồ họa.
    • Từ đó máy phải gọi kernel hệ điều hành, và kernel đấy phải lưu lại trạng thái chương trình để sau có thể sử dụng lại (hàng trăm câu lệnh nhé)...
    • Việc này đồng nghĩa với việc nó phải kiểm tra xem có chương trình nào khác cần chạy hay không. - và có thể phải chạy những chương trình đó một lúc đấy (hàng chục triệu câu lệnh nữa!).
    • Cuối cùng là câu lệnh vẽ các điểm ảnh tạo nên ký hiệu cho giá trị thập phân của ‘i’ và được gửi vào hàng chờ để gửi tới chip GPU (lại vài trăm câu lệnh nữa nhé).
    • Khi GPU xong việc (có thể tốn thời gian một chút nếu có chương trình nào khác muốn hiển thị cái gì đó) - thì dữ liệu để cập nhật lại cửa sổ của bạn sẽ được gửi tới GPU thông qua cơ chế “DMA” (direct memory acess) khi có thời gian.
    • Thoát tất cả những câu lệnh đó và chờ hệ điều hành quyết định xem lúc nào có thể chạy chương trình đó một lần nữa...
  • Và CUỐI CÙNG... Quay ngược lại đoạn ban đầu và làm lại hết tất cả những thứ trên đây.

Vì thế vấn đề là - chính vòng lặp đó chỉ tốn khoảng một phần tỷ giây mỗi lần lặp thôi - song những gì bạn làm trong vòng lặp đó là thứ gây nên một loạt phản ứng dây chuyền, những thứ thực sự phức tạp đó!

Một trong những bài học mà bạn phải học được khi lập trình ấy là Input/Output là thứ ngốn tài nguyên nhất mà một chương trình có thể thực hiện.

Đây không phải là khả năng duy nhất có thể xảy ra đâu - bạn có thể sẽ phải điều hướng standard output stream tới một file nào đó trên đĩa cứng... Từ đó dẫn tới việc điều hướng một mớ hổ lốn các process và driver khác nữa. Thực tế thì, đối với những thứ như video game - toàn bộ process phải được rút ngắn bất cứ khi nào có thể bằng việc để chương trình kết nối trực tiếp với chip graphic - loại trừ tất cả những bước trung gian và chỉ để lại một vài thao tác nhỏ vậy thôi.

Máy tính nhanh vãi lúa - song chúng cũng phải thực hiện những công việc điên khùng vãi lúa luôn đấy!!

Source:

  • Quora
  • Bài dịch từ Quora Việt Nam, share bởi Humans of Programming