🌱 Function Call in C/C++
Bài viết hôm nay mình bàn đến một topic cũng rất quan trọng trong C/C++, đó là quá trình Function Call, tức là khi gọi một hàm thì sẽ có những điều gì xảy ra đối với phần cứng, và chúng ta sẽ cần lưu ý gì về mặt phần mềm. Ngôn ngữ C/C++ có khả năng tiếp cận với phần cứng dễ dàng hơn những ngôn ngữ lập trình bậc cao khác.
Đặc biết đối với những ứng dụng nhúng, ngoại trừ các ưu điểm và ứng dụng của Function, chúng ta còn cần quan tâm đến việc gọi hàm có thể gây tác động như thế nào về mặt thời gian cũng như bộ nhớ trong quá trình build, runtime.
Bài viết này sẽ phù hợp với các bạn đã có kiến thức cơ bản về C và kiến trúc máy tính, đã từng sử dụng function!
👉 Bàn một chút về kiến trúc máy tính
![]() |
Cơ chế hoạt động của CPU |
Về cơ chế hoạt động của CPU (máy tính nói chung và Vi điều khiển nói riêng thì cấu tạo và cơ chế hoạt động sẽ tương đối giống nhau), thành phần chính của nó bao gồm:
- CU - Control Unit để điều khiển các hoạt động Fetch lệnh, thực thi lệnh chứa trong thanh ghi PC (Program Counter).
- ALU - Arithmetic & Logical Unit - Bộ xử lý tính toán số học và logic, thực hiện toàn bộ phép tính toán trong chương trình.
- Registers - Các thanh ghi chung (Không nói đến các thanh ghi chức năng đặc biệt như PC) - Dùng để lưu trữ dữ liệu tính toán chung. Khi thực hiện một lệnh thì các data sẽ được lưu tạm thời trong các thanh ghi này!
👉 Những yếu tố có thể bị thay đổi
💬 Trong quá trình Function Call, có thể thấy các Register trong CPU sẽ cần tham gia vào quá trình lưu trữ các Argument của Function, lưu trữ các biến local trong function. Vì vậy, các Register này hoàn toàn có thể bị thay đổi.
Nhưng trên thực tế, trước khi call function thì các Register này cũng dùng để lưu những giá trị tính toán khác, có thể là giá trị của các biến. Vậy nên sau khi call và thoát khỏi hàm thì những Register này cần giữ nguyên giá trị cũ.
💬 Một thành phần tiếp theo bị thay đổi là thanh ghi PC - Program Counter, thanh ghi này sẽ lưu trữ địa chỉ câu lệnh hiện tại của chương trình. Thông thường trong một function, PC sẽ tăng dần theo thứ tự tăng dần địa chỉ của các câu lệnh (Ví dụ PC += 4).
Nhưng khi call function, giá trị PC sẽ thay đổi đột ngột theo địa chỉ của hàm được call (Như hình trên khi call func(), PC = 0x100). Tuy nhiên, khi thoát ra khỏi hàm, chương trình cần biết được chính xác địa chỉ của câu lệnh tiếp theo (next instruction = 0x24). Vì vậy, giá trị địa chỉ của câu lệnh tiếp theo này cần được lưu trữ khi call function.
💬 Thành phần thứ 3 là những variable của function, trong đó có các Arguments và Return Value.
👉 Quá trình Stacking - UnStacking
Những yếu tố kể trên yêu cầu được lưu trữ tạm thời khi call function, và được trả lại trạng thái ban đầu khi thoát khỏi hàm. Đặc điểm này rất phù hợp với hoạt động Push-Pop và đặc tính LIFO - Last In First Out của bộ nhớ Stack.
Vì vậy, khi call function, các thành phần trên sẽ được push vào Stack (gọi là quá trình Stacking), và được pop ra khỏi Stack khi thoát khỏi function (gọi là UnStacking).
Các thành phần sẽ được push-pop trên Stack (Lấy ví dụ theo hình vẽ trên):
- Các Arguments của Function (x = 1).
- Địa chỉ của câu lệnh tiếp theo (0x24).
- Register của CPU, gọi là Frame, Frame này sẽ chứa các thanh ghi tùy thuộc theo kiến trúc vi xử lý sử dụng (Ví dụ máy tính - chip x86 có thanh ghi edi, esi - Cái này mình không chắc 100% các bạn có thể kiểm chứng thêm, với ARM thì có các thanh ghi R0, R1, R2, R3, R12).
👉 Kết luận
Chúng ta có thể kết luận, quá trình Function Call là một quá trình khá "tốn tài nguyên" - Cả về thời gian và bộ nhớ, vì vậy việc chúng ta sử dụng function cũng cần có những lưu ý nhất định.
- Về mặt thời gian, quá trình Stacking và Unstacking cũng chiếm một chút thời gian không lớn, nhưng nếu "lạm dụng" việc call function (Ví dụ việc viết các hàm ngắn và gọi nhiều lần, hoặc sử dụng thuật toán đệ quy), thì quãng thời gian tiêu tốn cho công việc này cũng không hề nhỏ.
Đôi khi với những function nhỏ và gọi nhiều lần, chúng ta có thể khắc phục bằng cách sử dụng Inline Function. - Về mặt bộ nhớ, việc call function chiếm một số lượng bộ nhớ Stack khá lớn (Như ví dụ trên, nếu x86 sử dụng 2 thanh ghi edi và esi cho frame, kích thước mỗi thanh ghi là 8 bytes, địa chỉ lệnh tiếp theo là 8 bytes, 1 param kiểu int chiếm 4 byte, tổng chúng ta mất 8 + 8 + 4 = 20 bytes). Kích thước này không hề nhỏ, nó sẽ còn nhân lên nếu như chúng ta call vào hàm nhiều lần hoặc sử dụng đệ quy, hay truyền những tham số với size quá lớn!
Trường hợp tham số có kích thước lớn (Các struct), có thể khắc phục bằng cách Pass by Reference!
0 Nhận xét