Tổng hợp các mã lệnh được dùng trong bài tập 1 khóa học Kiến trúc máy tính và Assembly

Bài tập: nhập hai số nguyên, tính tổng và in ra màn hình.

Video 1: https://youtu.be/JJTuWwFW2nc

Video 2: https://youtu.be/rKNuJ7QJt4Q

Video 3: https://youtu.be/cdmgwjOI5Rc

MOV – Di chuyển dữ liệu

Sao chép giá trị từ nguồn sang đích mà không làm thay đổi giá trị nguồn.

Ví dụ: `mov rax, 1` gán giá trị 1 vào thanh ghi rax.

Lưu ý là chúng ta không thể thực hiện một lệnh mov từ bộ nhớ đến bộ nhớ, để làm vậy bạn cần hai lệnh mov riêng biệt và dùng một thanh ghi làm nơi chứa giá trị trung gian.

Không phải thanh ghi nào cũng có thể được thay đổi giá trị bằng lệnh mov.

SYSCALL – Gọi hệ thống

Thực hiện lời gọi hệ thống của Linux.

Lệnh syscall chuyển điều khiển cho hệ điều hành, đồng thời đưa CPU về kernel mode, bạn cần gán giá trị thanh ghi rax bằng mã chức năng mà bạn muốn gọi.

Xem chi tiết trong bài: https://youtu.be/gefL014dGz8

Continue reading “Tổng hợp các mã lệnh được dùng trong bài tập 1 khóa học Kiến trúc máy tính và Assembly”

Vì sao nên nắm vững Kiến trúc máy tính và ngôn ngữ Assembly

Đối với người học lập trình phần mềm chuyên sâu, đặc biệt là sinh viên, việc hiểu rõ kiến trúc máy tính là yêu cầu bắt buộc để có thể hiểu sâu vào những gì diễn ra bên trong máy tính. Do vậy đây là một môn học bắt buộc, không hiểu rõ những kiến thức nền tảng này sẽ khiến bạn không thể đào sâu những khái niệm liên quan như đa luồng, bất đồng bộ, các kỹ thuật tối ưu, cách sử dụng bộ nhớ hiệu quả… Và hiển nhiên bạn không thể trở thành một chuyên gia nếu chỉ biết những kiến thức cơ bản, và trong ngành này, việc không thể trở thành chuyên gia khiến bạn chỉ có thể cạnh trạnh bằng sức khỏe, một thứ mà theo thời gian sẽ ngày càng kém đi.

Assembly là ngôn ngữ lập trình sát với phần cứng nhất, khi viết bằng ngôn ngữ này, chúng ta kiểm soát chương trình đến từng byte, từng mã lệnh. Hai môn học Kiến trúc máy tính và ngôn ngữ Assembly luôn đi cùng với nhau, bởi phải có kiến thức về Kiến trúc máy tính mới có thể viết chương trình Assembly, ngược lại, viết chương trình bằng Assembly sẽ giúp bạn có một môi trường thực hành, bạn có thể “nhìn thấy” những thứ trong lý thuyết được áp dụng vào thực tế.

Continue reading “Vì sao nên nắm vững Kiến trúc máy tính và ngôn ngữ Assembly”

Con trỏ gần, con trỏ xa

Sau khi đọc bài: https://daohainam.com/2021/08/13/cau-chuyen-ve-con-tro-pointer, bạn đã biết biến con trỏ là một số nguyên chứa địa chỉ của một vùng nhớ. Tuy nhiên đôi khi bạn còn nghe về khái niệm con trỏ gần và con trỏ xa (near pointer và far pointer), vậy chúng là gì?

Với các CPU đời cũ thời 16bit (đọc tiếp: 64 bit? 32 bit?), để định vị được đến tất cả các địa chỉ trong bộ nhớ, CPU dùng cơ chế segment:offset. Như đã đọc trong bài con trỏ, nếu thanh ghi địa chỉ có kích thước 16 bit, nó chỉ có thể chứa một địa chỉ từ 0-65535 (64KB), một biến con trỏ khi đó cũng chỉ chứa được một địa chỉ trong phạm vi tương tự.

Tuy nhiên, các CPU này lại có tới 20 đường địa chỉ, tức là giữa CPU và MCU (Memory Control Unit) có tới 20 “sợi dây điện” (gọi vậy cho dễ hình dung :D, đặt tên là A0-A19), do vậy CPU có thể gửi 1 con số lớn tới 20 bit đến MCU mỗi khi nó cần đọc/ghi 1 giá trị trong bộ nhớ. Vì một thanh ghi chỉ có kích thước 16 bit, do vậy nếu muốn lưu lại một địa chỉ, CPU phải kết hợp 2 thanh ghi lại với nhau. Người ta chia bộ nhớ thành từng phân đoạn (segment), mỗi segment sẽ bắt đầu tại một địa chỉ cách nhau 16 byte (1). Như vậy, segment 0 bắt đầu từ địa chỉ 0, segment 1 bắt đầu từ địa chỉ 16, segment 2 bắt đầu từ 32… và cứ như vậy.

Continue reading “Con trỏ gần, con trỏ xa”

THẾ NÀO LÀ NGÔN NGỮ LẬP TRÌNH BẬC THẤP, BẬC CAO?

Ngôn ngữ lập trình là gì thì chắc ở đây ai cũng biết rồi, vậy nhưng tại sao người ta còn có bậc thấp và bậc cao?

Để ngắn gọn, ta có thể nhớ luôn Hợp ngữ (Assembly language) và ngôn ngữ máy là ngôn ngữ cấp thấp, còn tất cả các ngôn ngữ khác đều là bậc cao.

NGÔN NGỮ MÁY

Máy tính vốn không hiểu tiếng người, bộ nhớ của nó chỉ chứa duy nhất các bit 0 và 1, được gom lại thành từng byte. Việc đọc hay ghi luôn được thực hiện theo đơn vị byte, cũng như việc đánh địa chỉ cũng theo byte. Bạn không thể yêu cầu CPU hay các thiết bị ngoại vi: “Hãy lấy cho tôi 1 bit ở vị trí xyz nào đó”. Muốn làm điều đó bạn phải tính toán xem bit đó thuộc byte nào (cứ chia cho 8 là được), đọc byte đó, rồi xem bit đó tương ứng với vị trí thứ mấy trong byte, dùng một toán tử bit nào đó (AND chẳng hạn) để kiểm tra xem nó bằng 1 hay bằng 0.

Continue reading “THẾ NÀO LÀ NGÔN NGỮ LẬP TRÌNH BẬC THẤP, BẬC CAO?”

GIẢI THÍCH: Vấn đề nằm ở CPU cache

Đáp án cho câu hỏi trong bài: https://daohainam.com/2021/12/30/vi-sao-duyet-mang-theo-dong-lai-nhanh-hon-theo-cot/

👉 Khi nằm trong bộ nhớ, các mảng nhiều chiều sẽ được diễn dịch thành một mảng 1 chiều (vì bộ nhớ về cơ bản cũng chỉ là mảng 1 chiều). Mỗi dòng sẽ được sắp xếp liên tục theo thứ tự. Mỗi khi cần truy xuất đến 1 ô nào đó có địa chỉ m[dòng, cột], trình biên dịch sẽ biến đổi thành m[dòng * chiều rộng mảng + cột] (xem hình minh họa).

👉 Như vậy, nếu ta đi chuyển theo dòng->cột (tương ứng với calculate_sum(sum, 1) trong https://github.com/…/clanc…/blob/master/CachingTests.cpp), thứ tự truy xuất trong bộ nhớ sẽ được tăng dần, trong khi đó, nếu ta di chuyển theo cột->dòng, thứ tự truy xuất theo hình minh họa sẽ là 0, 4, 8, 1, 5…

👉 Bộ nhớ cache trong CPU được tổ chức theo từng lance (không biết dịch ra thế nào, trong tiếng Việt ta vẫn dùng từ lance để chỉ các phần đường phân cách nhau). Mỗi lance có kích thước 64 byte, mỗi khi nạp từ RAM vào cache, hay từ cache vào RAM nó sẽ luôn làm việc với từng lance như vậy. Do đó khi đọc vào 1 byte, tất cả các byte lân cận trong cùng lance sẽ nằm sẵn ngay trong cache, khi bạn đọc đến byte kế tiếp bạn chỉ cần lấy nó ra từ cache (cache hit). Tốc độ của cache lại nhanh hơn RAM rất nhiều, người ta tính toán rằng cache L1 trong CPU có tốc độ nhanh hơn vài chục tới cả trăm lần so với truy xuất từ RAM.

👉 Kết quả là việc đọc/ghi dữ liệu tuần tự sẽ cho tốc độ tốt hơn nhiều so với truy xuất ngẫu nhiên. Điều này cũng tương tự như khi bạn đọc dữ liệu từ ổ SSD, vốn không có các cơ cấu cơ học và không có thời gian di chuyển đầu đọc như HDD, tuy nhiên khi copy 1 file lớn vẫn nhanh hơn nhiều so với copy nhiều file nhỏ. Đó cũng là do khi đọc/ghi tuần tự thì xác suất cache hit sẽ lớn hơn nhiều so với cache miss.❗️Khi làm việc với các ứng dụng lớn, việc tổ chức cách lưu trữ dữ liệu rất quan trọng!

VÌ SAO DUYỆT MẢNG THEO DÒNG LẠI NHANH HƠN THEO CỘT?

Mình vừa viết một chương trình nhỏ, chỉ để tính tổng các ô trong một ma trận, tuy nhiên khi thử duyệt theo dòng thì luôn thấy nhanh hơn cột, mảng càng lớn tốc độ càng khác biệt.Các bạn có thể tải về chương trình tại ( https://github.com/namdotnet/clancetest) và chạy thử xem có đúng không, và mất bao nhiêu tick mỗi bước, laptop mình đang dùng chạy Xeon mất hơn 600 ticks cho bước 1.

Nếu nhiều người ủng hộ thì mình sẽ giải thích lý do tại sao 😉(ghi chú là trong ví dụ này mình gọi dòng trước cột sau nhé int m[ROWS][COLS]).

NÊN SỬ DỤNG MULTI THREADING THẾ NÀO

Sau khi đã biết cách viết chương trình multi-thread, giờ ta sẽ cùng bàn đến vấn đề quan trọng hơn: Khi nào nên dùng MT, và còn có lựa chọn nào khác nữa không?

ℹ️ Multi threading phù hợp với những trường hợp sau đây:

👉 Chương trình cần tính toán nhiều (sử dụng nhiều năng lực tính toán của CPU) và muốn tận dụng nhiều CPU hoặc core khác nhau, mỗi thread sẽ tính toán một bài toán riêng lẻ. Tuy nhiên lưu ý là không phải cứ tạo thread mới thì nó sẽ tận dụng được các nhân khác, nếu bạn có 2 thread nhưng được gán cho cùng một core/cpu thì hiệu năng cũng sẽ không tăng lên. Với các bài toán này, bạn nên tìm hiểu về các thư viện hỗ trợ lập trình song song trên nền tảng của bạn.Ngay cả khi bạn chỉ có một bài toán cần tính, bạn cũng nên đưa vào một thread chạy ngầm bởi 2 lý do:

– Chương trình sẽ không bị khóa cứng lại và trở nên “not responsive”, người dùng có thể theo dõi tiến độ, hoặc hủy nếu quá trình tính toán quá lâu.

Continue reading “NÊN SỬ DỤNG MULTI THREADING THẾ NÀO”

VẤN ĐỀ ĐỒNG BỘ TRUY CẬP DỮ LIỆU

Bài toán đồng bộ

Đối với các chương trình single-thread, ta không cần quan tâm đến việc đồng bộ, bởi chương trình sẽ thực thi theo thứ tự từ trên xuống dưới từng câu lệnh một, trạng thái của chương trình sẽ chỉ bị thay đổi bởi chính nó. Tuy nhiên với các chương trình multi-thread, sẽ xảy ra trường hợp có nhiều hơn một thread cố gắng truy cập vào cùng một biến và gây ra sai sót.

👉 Xét ví dụ sau khi tráo đổi giá trị hai biến x và y (swap):

x = 10;
y = 20;
t = x; 
x = y; (*)
y = t;

Tại vị trí (*), giá trị của x và y bằng nhau, và bằng 20. Đây có thể coi là một trạng thái sai, bởi chỉ có thể x = 10, y = 20 hoặc x = 20, y = 10 sau khi tráo đổi xong. Trước hay sau khi tráo lại thì x * y cũng phải bằng 200, tuy nhiên tại vị trí (*), x * y sẽ là 400.Nếu có một thread thứ hai đọc giá trị của x và y để xử lý công việc, sẽ có một lúc nào đó giá trị nó đọc được sẽ bị sai.Cũng với ví dụ trên, giả sử có 2 thread cùng thực thi, bởi các câu lệnh có thể ngắt quãng và thực thi xen kẽ bởi các thead khác nhau, nên có thể xảy ra trường hợp sau:(thread 1 ký hiệu t1, thread 2 ký hiệu t2)

Continue reading “VẤN ĐỀ ĐỒNG BỘ TRUY CẬP DỮ LIỆU”

NÓI THÊM MỘT CHÚT VỀ THREAD

Trong các ứng dụng single thread, ta chỉ có duy nhất một thread chạy trong một không gian địa chỉ (1), do vậy ta có thể đảm bảo không ai thay đổi dữ liệu trong suốt quá trình chạy. Tuy nhiên trong các ứng dụng multi-thread sẽ có 2 hoặc nhiều hơn thread chạy đồng thời và chia sẻ chung không gian địa chỉ, do vậy một trong những bài toán quan trọng nhất là đồng bộ dữ liệu dùng chung giữa các thread.

Nên lưu ý khái niệm chạy đồng thời ở đây là tương đối, vì một máy tính có thể chỉ có rất ít CPU, nên nó phải chia sẻ giữa nhiều thread khác nhau. Trong thực tế, số thread luôn lớn hơn số CPU có trong hệ thống, trong một thời điểm có vài trăm đến vài ngàn thread chạy đồng thời là điều bình thường, mỗi thread sẽ chỉ được cấp một khoảng thời gian để chạy (gọi là time slide), sau đó HĐH sẽ lấy lại quyền điều khiển và chuyển sang thread khác.

Continue reading “NÓI THÊM MỘT CHÚT VỀ THREAD”

LẬP TRÌNH MULTI THREAD

Vậy là ta đã xong OOP, con trỏ và các phần nói về quản lý bộ nhớ, giờ tiếp đến một chủ đề nữa: lập trình đa luồng – multi thread programming.Trong bài này tôi chỉ giới thiệu qua các khái niệm cơ bản và các từ chuyên ngành liên quan, để dễ nhất các bạn nên đọc lại những bài viết về chủ đề quản lý bộ nhớ (stack, heap) và bài về từ khóa virtual trong OOP.

ℹ️ Trước tiên ta cần hiểu thread là gì

Về kỹ thuật, một thread là một chuỗi các lệnh cần được thực thi bởi CPU, hay ta có thể tưởng tượng một thread là một function, trong đó chứa các lệnh thực thi, và quan trọng nhất – nó sẽ chạy ĐỒNG THỜI với chương trình chính.Bạn có thể thấy, chương trình sẽ bắt đầu bằng hàm main, Main, hay với nhiều ngôn ngữ là từ câu lệnh đầu tiên. Mỗi khi bạn gọi một function, bạn sẽ phải chờ nó kết thúc, giờ mỗi khi gọi một hàm, nó sẽ được thực thi bởi một CPU khác, vì ta có 2 CPU nên hàm chính và hàm được gọi sẽ chạy đồng thời với nhau.

❓Bạn sẽ đặt câu hỏi: Nếu máy của tôi chỉ có 1 CPU, vậy làm sao tôi có thể chạy đa luồng được?

ℹ️ Đó là nhiệm vụ của hệ điều hành, với các hệ điều hành đa nhiệm (multi tasking OS), nó sẽ có những cách sau để cho phép bạn chạy 2 thread cùng lúc.

Continue reading “LẬP TRÌNH MULTI THREAD”