Cách tiếp cận tốt nhất khi viết chức năng cho phần mềm nhúng để có hiệu suất tốt hơn là gì? [đóng cửa]


13

Tôi đã thấy một số thư viện cho vi điều khiển và các chức năng của chúng làm một việc tại một thời điểm. Ví dụ, một cái gì đó như thế này:

void setCLK()
{
    // Code to set the clock
}

void setConfig()
{
    // Code to set the config
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
}

Sau đó, đến các hàm khác trên đầu nó sử dụng mã 1 dòng này chứa một hàm để phục vụ các mục đích khác. Ví dụ:

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Tôi không chắc chắn, nhưng tôi tin rằng theo cách này, nó sẽ tạo ra nhiều lệnh gọi nhảy hơn và tạo ra chi phí xếp chồng các địa chỉ trả về mỗi khi hàm được gọi hoặc thoát. Và điều đó sẽ làm cho chương trình hoạt động chậm, phải không?

Tôi đã tìm kiếm và ở mọi nơi họ nói rằng quy tắc ngón tay cái của lập trình là một hàm chỉ nên thực hiện một nhiệm vụ.

Vì vậy, nếu tôi viết trực tiếp một mô-đun chức năng initModule để đặt đồng hồ, hãy thêm một số cấu hình mong muốn và thực hiện một số thứ khác mà không cần gọi các hàm. Đó có phải là một cách tiếp cận tồi khi viết phần mềm nhúng?


EDIT 2:

  1. Có vẻ như nhiều người đã hiểu câu hỏi này như thể tôi đang cố gắng tối ưu hóa một chương trình. Không, tôi không có ý định làm . Tôi đang để trình biên dịch làm điều đó, bởi vì nó sẽ luôn luôn (tôi hy vọng là không!) Tốt hơn tôi.

  2. Tất cả các lỗi đổ lỗi cho tôi vì đã chọn một ví dụ đại diện cho một số mã khởi tạo . Câu hỏi không có ý định liên quan đến các cuộc gọi chức năng được thực hiện cho mục đích khởi tạo. Câu hỏi của tôi là Việc phá vỡ một nhiệm vụ nhất định thành các chức năng nhỏ của nhiều dòng ( vì vậy không có vấn đề gì ) chạy bên trong một vòng lặp vô hạn có bất kỳ lợi thế nào so với việc viết hàm dài mà không có hàm lồng nhau nào không?

Vui lòng xem xét khả năng đọc được xác định trong câu trả lời của @Jonk .


28
Bạn rất ngây thơ (không có nghĩa là một sự xúc phạm) nếu bạn tin rằng bất kỳ trình biên dịch hợp lý nào sẽ mù quáng biến mã như được viết thành nhị phân như được viết. Hầu hết các trình biên dịch hiện đại khá tốt trong việc xác định khi nào một thói quen được nội tuyến tốt hơn và ngay cả khi đăng ký so với vị trí RAM nên được sử dụng để giữ một biến. Thực hiện theo hai quy tắc tối ưu hóa: 1) không tối ưu hóa. 2) không tối ưu hóa được nêu ra . Làm cho mã của bạn có thể đọc và duy trì được, và chỉ sau khi cấu hình một hệ thống làm việc, hãy tìm cách tối ưu hóa.
akohlsmith

10
@akohlsmith IIRC Ba quy tắc tối ưu hóa là: 1) Đừng! 2) Không thực sự Đừng! 3) Hồ sơ trước, sau đó và chỉ sau đó tối ưu hóa nếu bạn phải - Michael_A._Jackson
esoterik

3
Chỉ cần nhớ rằng "tối ưu hóa sớm là gốc rễ của mọi tội lỗi (hoặc ít nhất là phần lớn) trong lập trình" - Knuth
Mawg nói rằng hãy phục hồi Monica

1
@Mawg: Từ phẫu thuật có sớm . . bit cho đến khi bạn có một cái gì đó để hồ sơ - nhưng cũng không tham gia vào bi quan, ví dụ bằng cách sử dụng các công cụ sai lầm trắng trợn cho công việc.
cHao

1
@Mawg Tôi không biết tại sao tôi nhận được câu trả lời / phản hồi liên quan đến tối ưu hóa, vì tôi chưa bao giờ đề cập đến từ này và tôi dự định thực hiện nó. Câu hỏi là nhiều hơn về cách viết các hàm trong Lập trình nhúng để đạt được hiệu suất tốt hơn.
MaNyYaCk

Câu trả lời:


28

Có thể cho rằng, trong ví dụ của bạn, hiệu suất sẽ không thành vấn đề, vì mã chỉ được chạy một lần khi khởi động.

Một nguyên tắc nhỏ mà tôi sử dụng: Viết mã của bạn càng dễ đọc càng tốt và chỉ bắt đầu tối ưu hóa nếu bạn nhận thấy rằng trình biên dịch của bạn không thực hiện đúng phép thuật của nó.

Chi phí của một cuộc gọi chức năng trong ISR có thể giống như chi phí của một cuộc gọi chức năng trong khi khởi động về mặt lưu trữ và thời gian. Tuy nhiên, các yêu cầu về thời gian trong ISR đó có thể quan trọng hơn rất nhiều.

Hơn nữa, như đã được người khác chú ý, chi phí (và ý nghĩa của 'chi phí') của một lệnh gọi hàm khác nhau bởi nền tảng, trình biên dịch, cài đặt tối ưu hóa trình biên dịch và các yêu cầu của ứng dụng. Sẽ có một sự khác biệt rất lớn giữa 8051 và vỏ não-m7, máy tạo nhịp tim và công tắc đèn.


6
IMO đoạn thứ hai nên được in đậm và ở trên cùng. Không có gì sai khi chọn đúng thuật toán và cấu trúc dữ liệu ngay lập tức, nhưng lo lắng về chi phí gọi hàm trừ khi bạn phát hiện ra rằng đó là một nút cổ chai thực sự chắc chắn là tối ưu hóa sớm, và nên tránh.
Vụ kiện của Quỹ Monica

11

Không có lợi thế nào tôi có thể nghĩ đến (nhưng xem ghi chú cho JasonS ở phía dưới), gói một dòng mã dưới dạng hàm hoặc chương trình con. Ngoại trừ có lẽ bạn có thể đặt tên cho chức năng là "có thể đọc được". Nhưng bạn cũng có thể bình luận dòng. Và vì việc gói một dòng mã trong một hàm tốn bộ nhớ mã, không gian ngăn xếp và thời gian thực hiện đối với tôi, nó dường như chủ yếu là phản tác dụng. Trong một tình huống giảng dạy? Nó có thể có ý nghĩa. Nhưng điều đó phụ thuộc vào lớp học sinh, sự chuẩn bị trước của họ, chương trình giảng dạy và giáo viên. Hầu hết, tôi nghĩ đó không phải là một ý tưởng tốt. Nhưng đó là ý kiến ​​của tôi.

Điều này đưa chúng ta đến điểm mấu chốt. Khu vực câu hỏi rộng của bạn, trong nhiều thập kỷ, là một vấn đề tranh luận và cho đến ngày nay vẫn là một vấn đề tranh luận. Vì vậy, ít nhất là khi tôi đọc câu hỏi của bạn, dường như tôi là một câu hỏi dựa trên ý kiến ​​(như bạn đã hỏi nó).

Nó có thể được chuyển khỏi vị trí dựa trên ý kiến, nếu bạn phải chi tiết hơn về tình huống và mô tả cẩn thận các mục tiêu bạn giữ là chính. Bạn càng xác định tốt các công cụ đo lường của mình, câu trả lời càng khách quan.


Nói rộng ra, bạn muốn làm như sau cho bất kỳ mã hóa nào . .

  1. Hãy nhất quán về cách tiếp cận của bạn, để người khác đọc mã của bạn có thể phát triển sự hiểu biết về cách bạn tiếp cận quá trình mã hóa của mình. Không nhất quán có lẽ là tội ác tồi tệ nhất có thể. Nó không chỉ gây khó khăn cho người khác mà còn gây khó khăn cho chính bạn khi quay trở lại mã năm sau đó.
  2. Ở mức độ có thể, hãy thử và sắp xếp mọi thứ sao cho việc khởi tạo các phần chức năng khác nhau có thể được thực hiện mà không liên quan đến việc đặt hàng. Trong trường hợp yêu cầu đặt hàng, nếu đó là do sự kết hợp chặt chẽ của hai chức năng con có liên quan cao, thì hãy xem xét một khởi tạo duy nhất cho cả hai để có thể sắp xếp lại mà không gây hại. Nếu điều đó là không thể, thì hãy ghi lại yêu cầu đặt hàng khởi tạo.
  3. Đóng gói kiến thức ở chính xác một nơi, nếu có thể. Các hằng số không được trùng lặp khắp nơi trong mã. Các phương trình giải quyết một số biến nên tồn tại ở một và chỉ một nơi. Và như thế. Nếu bạn thấy mình sao chép và dán một số dòng thực hiện một số hành vi cần thiết ở nhiều địa điểm khác nhau, hãy xem xét một cách để nắm bắt kiến ​​thức đó ở một nơi và sử dụng nó khi cần thiết. Ví dụ, nếu bạn có một cấu trúc cây mà phải đi theo một cách cụ thể, làm khôngsao chép mã đi bộ cây ở mỗi và mọi nơi bạn cần lặp qua các nút cây. Thay vào đó, hãy chụp phương pháp đi bộ trên cây ở một nơi và sử dụng nó. Theo cách này, nếu cây thay đổi và phương thức đi bộ thay đổi, bạn chỉ có một nơi để lo lắng và tất cả phần còn lại của mã "chỉ hoạt động đúng".
  4. Nếu bạn trải đều tất cả các thói quen của mình lên một tờ giấy lớn, phẳng, với các mũi tên nối với chúng khi chúng được gọi bởi các thói quen khác, bạn sẽ thấy trong bất kỳ ứng dụng nào sẽ có "cụm" các thói quen có rất nhiều mũi tên giữa họ nhưng chỉ có một vài mũi tên bên ngoài nhóm. Vì vậy, sẽ có ranh giới tự nhiên của các thói quen kết hợp chặt chẽ và kết nối lỏng lẻo giữa các nhóm khác của các thói quen kết hợp chặt chẽ. Sử dụng thực tế này để tổ chức mã của bạn thành các mô-đun. Điều này sẽ hạn chế sự phức tạp rõ ràng của mã của bạn, về cơ bản.

Trên đây chỉ là nói chung về tất cả các mã. Tôi đã không thảo luận về việc sử dụng các tham số, biến cục bộ hoặc biến toàn cục, v.v ... Lý do là vì lập trình nhúng, không gian ứng dụng thường đặt các ràng buộc mới cực kỳ và rất quan trọng và không thể thảo luận về tất cả chúng mà không thảo luận về mọi ứng dụng nhúng. Và điều đó không xảy ra ở đây, dù sao đi nữa.

Những ràng buộc này có thể là bất kỳ (và hơn thế nữa) trong số này:

  • Hạn chế chi phí nghiêm trọng đòi hỏi MCU cực kỳ nguyên thủy với RAM cực nhỏ và hầu như không có số lượng pin I / O. Đối với những điều này, bộ quy tắc hoàn toàn mới áp dụng. Ví dụ: bạn có thể phải viết mã lắp ráp vì không có nhiều không gian mã. Bạn có thể phải sử dụng CHỈ các biến tĩnh vì việc sử dụng các biến cục bộ quá tốn kém và mất thời gian. Bạn có thể phải tránh sử dụng quá nhiều chương trình con vì (ví dụ: một số phần PIC của Microchip) chỉ có 4 thanh ghi phần cứng để lưu địa chỉ trả về chương trình con. Vì vậy, bạn có thể phải "làm phẳng" mã của mình. Vân vân.
  • Các hạn chế về năng lượng nghiêm trọng đòi hỏi mã được chế tạo cẩn thận để khởi động và tắt hầu hết MCU và đặt các giới hạn nghiêm trọng về thời gian thực thi mã khi chạy ở tốc độ tối đa. Một lần nữa, điều này có thể yêu cầu một số mã hóa lắp ráp, đôi khi.
  • Yêu cầu thời gian nghiêm trọng. Ví dụ, có những lúc tôi phải đảm bảo rằng việc truyền cống mở 0 phải thực hiện chính xác số chu kỳ tương tự như việc truyền 1. Và việc lấy mẫu này cũng phải được thực hiện với một giai đoạn tương đối chính xác đến thời điểm này. Điều này có nghĩa là C KHÔNG thể được sử dụng ở đây. Cách duy nhất có thể để thực hiện đảm bảo đó là cẩn thận làm mã lắp ráp. (Và thậm chí sau đó, không phải lúc nào cũng trên tất cả các thiết kế ALU.)

Và như thế. (Mã dây cho thiết bị y tế quan trọng trong cuộc sống cũng có cả một thế giới riêng.)

Kết quả cuối cùng ở đây là mã hóa nhúng thường không phải là một số miễn phí, nơi bạn có thể mã hóa như bạn có thể trên máy trạm. Thường có những lý do nghiêm trọng, cạnh tranh cho một loạt các ràng buộc rất khó khăn. Và những điều này có thể tranh luận mạnh mẽ chống lại các câu trả lời truyền thốngchứng khoán hơn .


Về khả năng đọc, tôi thấy rằng mã có thể đọc được nếu nó được viết theo kiểu nhất quán mà tôi có thể học khi đọc nó. Và trong trường hợp không có sự cố tình làm xáo trộn mã. Thực sự không có nhiều yêu cầu hơn.

Mã có thể đọc được có thể khá hiệu quả và nó có thể đáp ứng tất cả các yêu cầu trên mà tôi đã đề cập. Điều chính là bạn hiểu đầy đủ những gì mỗi dòng mã bạn viết tạo ra ở cấp độ lắp ráp hoặc máy, khi bạn mã hóa nó. C ++ đặt một gánh nặng nghiêm trọng lên lập trình viên ở đây vì có nhiều tình huống trong đó các đoạn mã C ++ giống hệt nhau thực sự tạo ra các đoạn mã máy khác nhau có hiệu suất rất khác nhau. Nhưng C, nói chung, chủ yếu là một ngôn ngữ "những gì bạn thấy là những gì bạn nhận được". Vì vậy, nó an toàn hơn trong vấn đề đó.


EDIT mỗi JasonS:

Tôi đã sử dụng C từ năm 1978 và C ++ từ khoảng năm 1987 và tôi đã có nhiều kinh nghiệm sử dụng cả cho cả máy tính lớn, máy tính mini và (hầu hết) các ứng dụng nhúng.

Jason đưa ra một nhận xét về việc sử dụng 'nội tuyến' làm công cụ sửa đổi. (Theo quan điểm của tôi, đây là một khả năng tương đối "mới" vì đơn giản là nó không tồn tại được một nửa cuộc đời tôi hoặc hơn khi sử dụng C và C ++.) Việc sử dụng các hàm nội tuyến thực sự có thể thực hiện các cuộc gọi như vậy (ngay cả đối với một dòng mã) khá thực tế. Và nó tốt hơn nhiều, nếu có thể, hơn là sử dụng macro vì cách gõ mà trình biên dịch có thể áp dụng.

Nhưng cũng có những hạn chế. Đầu tiên là bạn không thể dựa vào trình biên dịch để "đưa ra gợi ý". Hên xui. Và có những lý do tốt để không đưa ra gợi ý. (Đối với một ví dụ rõ ràng, nếu địa chỉ của chức năng được thực hiện, điều này yêu cầu khởi tạo chức năng và sử dụng địa chỉ để thực hiện cuộc gọi sẽ ... yêu cầu một cuộc gọi. Sau đó, mã không thể được nội tuyến.) những lý do khác, là tốt. Trình biên dịch có thể có nhiều tiêu chí khác nhau để họ đánh giá cách xử lý gợi ý. Và là một lập trình viên, điều này có nghĩa là bạn phảidành thời gian tìm hiểu về khía cạnh đó của trình biên dịch nếu không bạn có thể đưa ra quyết định dựa trên những ý tưởng thiếu sót. Vì vậy, nó cũng tạo thêm gánh nặng cho người viết mã cũng như bất kỳ người đọc nào và bất kỳ ai dự định chuyển mã sang một số trình biên dịch khác.

Ngoài ra, trình biên dịch C và C ++ hỗ trợ biên dịch riêng. Điều này có nghĩa là họ có thể biên dịch một đoạn mã C hoặc C ++ mà không cần biên dịch bất kỳ mã nào khác có liên quan cho dự án. Để mã nội tuyến, giả sử trình biên dịch có thể chọn làm như vậy, nó không chỉ phải có khai báo "trong phạm vi" mà còn phải có định nghĩa. Thông thường, các lập trình viên sẽ làm việc để đảm bảo rằng đây là trường hợp nếu họ đang sử dụng 'nội tuyến'. Nhưng nó dễ dàng cho những sai lầm leo vào.

Nói chung, trong khi tôi cũng sử dụng nội tuyến nơi tôi nghĩ nó phù hợp, tôi có xu hướng cho rằng tôi không thể dựa vào nó. Nếu hiệu suất là một yêu cầu quan trọng và tôi nghĩ OP đã viết rõ ràng rằng đã có một hiệu suất đáng kể khi họ đi đến một tuyến đường "chức năng" hơn, thì tôi chắc chắn sẽ chọn tránh dựa vào nội tuyến như một cách thực hành mã hóa và thay vào đó sẽ đi theo một mô hình viết mã hơi khác, nhưng hoàn toàn nhất quán.

Một lưu ý cuối cùng về 'nội tuyến' và các định nghĩa là "trong phạm vi" cho một bước biên dịch riêng biệt. Có thể (không phải lúc nào cũng đáng tin cậy) cho công việc được thực hiện ở giai đoạn liên kết. Điều này có thể xảy ra khi và chỉ khi trình biên dịch C / C ++ chôn đủ chi tiết vào các tệp đối tượng để cho phép trình liên kết hành động theo yêu cầu 'nội tuyến'. Cá nhân tôi chưa có kinh nghiệm về một hệ thống liên kết (bên ngoài Microsoft) hỗ trợ khả năng này. Nhưng nó có thể xảy ra. Một lần nữa, việc có nên dựa vào hay không sẽ phụ thuộc vào hoàn cảnh. Nhưng tôi thường cho rằng điều này đã không được đưa vào trình liên kết, trừ khi tôi biết cách khác dựa trên bằng chứng tốt. Và nếu tôi dựa vào nó, nó sẽ được ghi nhận ở một nơi nổi bật.


C ++

Đối với những người quan tâm, đây là một ví dụ về lý do tại sao tôi vẫn khá thận trọng với C ++ khi mã hóa các ứng dụng nhúng, mặc dù nó đã sẵn sàng ngày hôm nay. Tôi sẽ đưa ra một số thuật ngữ mà tôi nghĩ rằng tất cả các lập trình viên C ++ nhúng cần biết lạnh lùng :

  • chuyên môn hóa một phần mẫu
  • vtables
  • đối tượng cơ sở ảo
  • khung kích hoạt
  • kích hoạt khung thư giãn
  • sử dụng con trỏ thông minh trong các nhà xây dựng, và tại sao
  • tối ưu hóa giá trị

Đó chỉ là một danh sách ngắn. Nếu bạn chưa biết tất cả mọi thứ về các điều khoản đó và tại sao tôi liệt kê chúng (và nhiều điều nữa tôi không liệt kê ở đây) thì tôi khuyên bạn không nên sử dụng C ++ cho công việc nhúng, trừ khi đó không phải là một lựa chọn cho dự án .

Chúng ta hãy xem nhanh ngữ nghĩa ngoại lệ của C ++ để có được một hương vị.

MộtB

Một

   .
   .
   foo ();
   String s;
   foo ();
   .
   .

Một

B

Trình biên dịch C ++ nhìn thấy cuộc gọi đầu tiên đến foo () và chỉ có thể cho phép một khung kích hoạt bình thường thư giãn xảy ra, nếu foo () ném ngoại lệ. Nói cách khác, trình biên dịch C ++ biết rằng không cần thêm mã vào thời điểm này để hỗ trợ quá trình thư giãn khung liên quan đến xử lý ngoại lệ.

Nhưng một khi String s đã được tạo, trình biên dịch C ++ biết rằng nó phải được hủy đúng trước khi cho phép mở khung hình, nếu một ngoại lệ xảy ra sau đó. Vì vậy, cuộc gọi thứ hai đến foo () khác về mặt ngữ nghĩa so với cuộc gọi đầu tiên. Nếu lệnh gọi thứ 2 đến foo () đưa ra một ngoại lệ (điều này có thể hoặc không thể thực hiện), trình biên dịch phải đặt mã được thiết kế để xử lý việc phá hủy Chuỗi s trước khi xảy ra tình trạng bung khung thông thường. Điều này khác với mã cần thiết cho cuộc gọi đầu tiên đến foo ().

(Có thể thêm các trang trí bổ sung trong C ++ để giúp hạn chế vấn đề này. Nhưng thực tế là, các lập trình viên sử dụng C ++ chỉ đơn giản là phải nhận thức rõ hơn về ý nghĩa của từng dòng mã họ viết.)

Không giống như malloc của C, mới của C ++ sử dụng các ngoại lệ để báo hiệu khi không thể thực hiện cấp phát bộ nhớ thô. 'Dynamic_cast' cũng vậy. (Xem phiên bản thứ 3 của Stroustrup, Ngôn ngữ lập trình C ++, trang 384 và 385 để biết các ngoại lệ tiêu chuẩn trong C ++.) Trình biên dịch có thể cho phép hành vi này bị vô hiệu hóa. Nhưng nói chung, bạn sẽ phải chịu một số chi phí do các phần mở đầu và ngoại lệ xử lý ngoại lệ được tạo đúng trong mã được tạo, ngay cả khi các ngoại lệ thực sự không diễn ra và ngay cả khi hàm được biên dịch thực sự không có bất kỳ khối xử lý ngoại lệ nào. (Stroustrup đã công khai than thở về điều này.)

Nếu không có chuyên môn mẫu một phần (không phải tất cả các trình biên dịch C ++ đều hỗ trợ nó), việc sử dụng các mẫu có thể đánh vần thảm họa cho lập trình nhúng. Không có nó, mã nở là một rủi ro nghiêm trọng có thể giết chết một dự án nhúng bộ nhớ nhỏ trong nháy mắt.

Khi một hàm C ++ trả về một đối tượng, trình biên dịch không tên tạm thời được tạo và hủy. Một số trình biên dịch C ++ có thể cung cấp mã hiệu quả nếu một hàm tạo đối tượng được sử dụng trong câu lệnh return, thay vì một đối tượng cục bộ, làm giảm nhu cầu xây dựng và phá hủy của một đối tượng. Nhưng không phải mọi trình biên dịch đều thực hiện điều này và nhiều lập trình viên C ++ thậm chí không nhận thức được "tối ưu hóa giá trị trả về" này.

Việc cung cấp một hàm tạo đối tượng với một loại tham số duy nhất có thể cho phép trình biên dịch C ++ tìm đường dẫn chuyển đổi giữa hai loại theo cách hoàn toàn bất ngờ cho lập trình viên. Loại hành vi "thông minh" này không phải là một phần của C.

Mệnh đề bắt chỉ định một loại cơ sở sẽ "cắt" một đối tượng dẫn xuất bị ném, bởi vì đối tượng bị ném được sao chép bằng cách sử dụng "kiểu tĩnh" của mệnh đề bắt chứ không phải "kiểu động" của đối tượng. Một nguồn không phải là hiếm của ngoại lệ (khi bạn cảm thấy bạn thậm chí có thể đủ khả năng ngoại lệ trong mã nhúng của mình.)

Trình biên dịch C ++ có thể tự động tạo các hàm tạo, hàm hủy, sao chép hàm tạo và toán tử gán cho bạn, với các kết quả ngoài ý muốn. Phải mất thời gian để có được cơ sở với các chi tiết của điều này.

Truyền các mảng của các đối tượng dẫn xuất đến một hàm chấp nhận các mảng của các đối tượng cơ sở, hiếm khi tạo ra các cảnh báo của trình biên dịch nhưng hầu như luôn mang lại hành vi không chính xác.

Do C ++ không gọi hàm hủy của các đối tượng được xây dựng một phần khi có ngoại lệ xảy ra trong hàm tạo đối tượng, nên việc xử lý ngoại lệ trong các hàm tạo thường bắt buộc "con trỏ thông minh" để đảm bảo rằng các đoạn được xây dựng trong hàm tạo bị phá hủy đúng cách nếu có ngoại lệ xảy ra ở đó . (Xem Stroustrup, trang 367 và 368.) Đây là vấn đề phổ biến khi viết các lớp tốt trong C ++, nhưng tất nhiên tránh được trong C vì C không có ngữ nghĩa về xây dựng và phá hủy được xây dựng. Viết mã thích hợp để xử lý việc xây dựng của các tiểu dự án trong một đối tượng có nghĩa là viết mã phải đối phó với vấn đề ngữ nghĩa duy nhất này trong C ++; nói cách khác là "viết xung quanh" các hành vi ngữ nghĩa của C ++.

C ++ có thể sao chép các đối tượng được truyền vào tham số đối tượng. Ví dụ: trong các đoạn sau, lệnh gọi "rA (x);" có thể khiến trình biên dịch C ++ gọi hàm tạo cho tham số p, sau đó gọi hàm tạo sao chép để chuyển đối tượng x sang tham số p, sau đó một hàm tạo khác cho đối tượng trả về (tạm thời không tên) của hàm rA, tất nhiên là sao chép từ tham số p. Tồi tệ hơn, nếu lớp A có các vật thể riêng cần xây dựng, điều này có thể gây ra thảm họa. (Lập trình viên AC sẽ tránh được phần lớn rác này, tối ưu hóa tay vì các lập trình viên C không có cú pháp tiện dụng như vậy và phải thể hiện tất cả các chi tiết một lần.)

    class A {...};
    A rA (A p) { return p; }
    // .....
    { A x; rA(x); }

Cuối cùng, một lưu ý ngắn cho lập trình viên C. longjmp () không có hành vi di động trong C ++. (Một số lập trình viên C sử dụng điều này như một loại cơ chế "ngoại lệ".) Một số trình biên dịch C ++ thực sự sẽ cố gắng thiết lập mọi thứ để dọn dẹp khi longjmp được thực hiện, nhưng hành vi đó không khả dụng trong C ++. Nếu trình biên dịch dọn sạch các đối tượng được xây dựng, thì nó không khả dụng. Nếu trình biên dịch không dọn sạch chúng, thì các đối tượng sẽ không bị hủy nếu mã rời khỏi phạm vi của các đối tượng được xây dựng do kết quả của longjmp và hành vi không hợp lệ. (Nếu việc sử dụng longjmp trong foo () không để lại phạm vi, thì hành vi có thể ổn.) Điều này không được sử dụng quá thường xuyên bởi các lập trình viên nhúng C nhưng họ nên tự nhận thức về các vấn đề này trước khi sử dụng chúng.


4
Loại hàm này chỉ được sử dụng một lần không bao giờ được biên dịch dưới dạng gọi hàm, mã được đặt đơn giản ở đó mà không có bất kỳ cuộc gọi nào.
Dorian

6
@Dorian - nhận xét của bạn có thể đúng trong một số trường hợp đối với các trình biên dịch nhất định. Nếu hàm là tĩnh trong tệp thì trình biên dịch có tùy chọn để tạo mã nội tuyến. nếu nó được hiển thị bên ngoài thì ngay cả khi nó không bao giờ thực sự được gọi, thì phải có một cách để hàm có thể gọi được.
uɐɪ

1
@jonk - Một mẹo khác mà bạn chưa đề cập trong một câu trả lời hay là viết các hàm macro đơn giản thực hiện khởi tạo hoặc cấu hình dưới dạng mã nội tuyến mở rộng. Điều này đặc biệt hữu ích trên các bộ xử lý rất nhỏ nơi độ sâu cuộc gọi RAM / stack / chức năng bị hạn chế.
uɐɪ

@ ouɐɪ Có, tôi đã bỏ lỡ việc thảo luận về macro trong C. Những điều đó không được chấp nhận trong C ++, nhưng một cuộc thảo luận về điểm đó có thể hữu ích. Tôi có thể giải quyết nó, nếu tôi có thể tìm ra một cái gì đó hữu ích để viết về nó.
jonk

1
@jonk - Tôi hoàn toàn không đồng ý với câu đầu tiên của bạn. Một ví dụ như inline static void turnOnFan(void) { PORTAbits &= ~(1<<8); }được gọi ở nhiều nơi là một ứng cử viên hoàn hảo.
Jason S

8

1) Mã cho khả năng đọc và bảo trì đầu tiên. Khía cạnh quan trọng nhất của bất kỳ cơ sở mã hóa nào là nó có cấu trúc tốt. Phần mềm được viết độc đáo có xu hướng có ít lỗi hơn. Bạn có thể cần thực hiện thay đổi trong một vài tuần / tháng / năm và điều này sẽ giúp ích rất nhiều nếu mã của bạn rất hay để đọc. Hoặc có thể người khác phải thay đổi.

2) Hiệu suất của mã chạy một lần không quan trọng lắm. Chăm sóc cho phong cách, không phải cho hiệu suất

3) Ngay cả mã trong các vòng lặp chặt chẽ cũng cần phải chính xác trước hết. Nếu bạn phải đối mặt với các vấn đề về hiệu suất, thì hãy tối ưu hóa khi mã chính xác.

4) Nếu bạn cần tối ưu hóa, bạn phải đo lường! Không quan trọng nếu bạn nghĩ hoặc ai đó nói với bạn rằng đó static inlinechỉ là một đề xuất cho trình biên dịch. Bạn phải xem những gì trình biên dịch làm. Bạn cũng phải đo nếu nội tuyến đã cải thiện hiệu suất. Trong các hệ thống nhúng, bạn cũng phải đo kích thước mã, vì bộ nhớ mã thường khá hạn chế. Đây là quy tắc quan trọng nhất giúp phân biệt kỹ thuật với phỏng đoán. Nếu bạn không đo nó, nó không có ích. Kỹ thuật là đo lường. Khoa học đang viết nó xuống;)


2
Những lời chỉ trích duy nhất tôi có về bài viết xuất sắc của bạn là điểm 2). Đúng là hiệu suất của mã khởi tạo là không liên quan - nhưng trong một môi trường nhúng, kích thước có thể quan trọng. (Nhưng điều đó không ghi đè lên điểm 1; bắt đầu tối ưu hóa kích thước khi bạn cần - chứ không phải trước đó)
Martin Bonner hỗ trợ Monica

2
Hiệu suất của mã khởi tạo lúc đầu có thể không liên quan. Khi bạn thêm chế độ năng lượng thấp và muốn phục hồi nhanh để xử lý sự kiện đánh thức, thì nó sẽ trở nên có liên quan.
berendi - phản đối

5

Khi một hàm chỉ được gọi ở một nơi (ngay cả bên trong hàm khác), trình biên dịch sẽ đặt mã ở vị trí đó thay vì thực sự gọi hàm. Nếu hàm được gọi ở nhiều nơi thì sẽ hợp lý hơn khi sử dụng hàm ít nhất theo quan điểm kích thước mã.

Sau khi biên dịch mã sẽ không có nhiều cuộc gọi thay vào đó khả năng đọc sẽ được cải thiện rất nhiều.

Ngoài ra, bạn sẽ muốn có ví dụ mã init ADC trong cùng thư viện với các hàm ADC khác không có trong tệp c chính.

Nhiều trình biên dịch cho phép bạn chỉ định các mức tối ưu hóa khác nhau cho tốc độ hoặc kích thước mã, vì vậy nếu bạn có một hàm nhỏ được gọi ở nhiều nơi thì hàm sẽ được "nội tuyến", sao chép ở đó thay vì gọi.

Tối ưu hóa cho tốc độ sẽ thực hiện các chức năng nội tuyến ở nhiều nơi có thể, tối ưu hóa cho kích thước mã sẽ gọi hàm, tuy nhiên, khi một hàm chỉ được gọi ở một nơi như trong trường hợp của bạn, nó sẽ luôn được "nội tuyến".

Mã như thế này:

function_used_just_once{
   code blah blah;
}
main{
  codeblah;
  function_used_just_once();
  code blah blah blah;
{

sẽ biên dịch thành:

main{
 code blah;
 code blah blah;
 code blah blah blah;
}

mà không sử dụng bất kỳ cuộc gọi nào.

Và câu trả lời cho câu hỏi của bạn, trong ví dụ của bạn hoặc tương tự, khả năng đọc mã không ảnh hưởng đến hiệu suất, không có gì nhiều về tốc độ hoặc kích thước mã. Thông thường sử dụng nhiều cuộc gọi chỉ để làm cho mã có thể đọc được, cuối cùng, chúng được tuân thủ như một mã nội tuyến.

Cập nhật để xác định rằng các tuyên bố trên không hợp lệ đối với các trình biên dịch phiên bản miễn phí bị làm tê liệt như phiên bản miễn phí Microchip XCxx. Loại cuộc gọi chức năng này là một mỏ vàng cho Microchip để cho thấy phiên bản trả phí tốt hơn bao nhiêu và nếu bạn biên dịch nó, bạn sẽ tìm thấy trong ASM chính xác nhiều cuộc gọi như bạn có trong mã C.

Ngoài ra, nó không dành cho các lập trình viên câm muốn sử dụng con trỏ đến một hàm nội tuyến.

Đây là phần điện tử, không phải phần chung C C ++ hay phần lập trình, câu hỏi là về lập trình vi điều khiển trong đó bất kỳ trình biên dịch tử tế nào sẽ thực hiện tối ưu hóa ở trên theo mặc định.

Vì vậy, xin vui lòng ngừng tải xuống chỉ vì trong trường hợp hiếm, điều này có thể không đúng.


15
Mã có trở thành nội tuyến hay không là vấn đề cụ thể của nhà cung cấp trình biên dịch; thậm chí sử dụng từ khóa nội tuyến không đảm bảo mã nội tuyến. Đó là một gợi ý cho trình biên dịch. Chắc chắn trình biên dịch tốt sẽ các hàm nội tuyến được sử dụng chỉ một lần nếu họ biết về chúng. Mặc dù vậy, nó thường không làm như vậy nếu có bất kỳ đối tượng "dễ bay hơi" nào trong phạm vi.
Peter Smith

9
Câu trả lời này chỉ là không đúng sự thật. Như @PeterSmith nói, và theo đặc tả ngôn ngữ C, trình biên dịch có tùy chọn để nội tuyến mã nhưng có thể không, và trong nhiều trường hợp sẽ không làm như vậy. Có rất nhiều trình biên dịch khác nhau trên thế giới dành cho rất nhiều bộ xử lý mục tiêu khác nhau tạo ra loại tuyên bố mền trong câu trả lời này và giả sử rằng tất cả các trình biên dịch sẽ đặt mã nội tuyến khi chúng chỉ có tùy chọn là không thể thực hiện được.
ɐɪ

2
@ ouɐɪ Bạn đang chỉ ra những trường hợp hiếm hoi không thể thực hiện được và sẽ là một ý tưởng tồi nếu không gọi hàm ngay từ đầu. Tôi chưa bao giờ thấy một trình biên dịch quá ngu ngốc để thực sự sử dụng cuộc gọi trong ví dụ đơn giản do OP đưa ra.
Dorian

6
Trong trường hợp các hàm này chỉ được gọi một lần, việc tối ưu hóa hàm gọi ra không phải là vấn đề. Hệ thống có thực sự cần phải vuốt lại mỗi chu kỳ đồng hồ trong quá trình thiết lập không? Như trường hợp tối ưu hóa ở bất cứ đâu - viết mã có thể đọc được và chỉ tối ưu hóa nếu hồ sơ cho thấy rằng nó là cần thiết .
Baldrickk

5
@MSalters Tôi không quan tâm đến những gì trình biên dịch kết thúc ở đây - nhiều hơn về cách lập trình viên tiếp cận nó. Không có, hoặc hiệu suất không đáng kể đánh vào việc phá vỡ khởi tạo như đã thấy trong câu hỏi.
Baldrickk

2

Trước hết, không có tốt nhất hoặc tồi tệ nhất; tất cả chỉ là vấn đề quan điểm Bạn rất đúng rằng điều này là không hiệu quả. Nó có thể được tối ưu hóa hoặc không; nó phụ thuộc Thông thường bạn sẽ thấy các loại chức năng, đồng hồ, GPIO, bộ đếm thời gian, vv trong các tệp / thư mục riêng biệt. Trình biên dịch nói chung không thể tối ưu hóa qua các khoảng trống này. Có một cái mà tôi có thể biết nhưng không được sử dụng rộng rãi cho những thứ như thế này.

Tập tin duy nhất:

void dummy (unsigned int);

void setCLK()
{
    // Code to set the clock
    dummy(5);
}

void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

Chọn một mục tiêu và trình biên dịch cho mục đích trình diễn.

Disassembly of section .text:

00000000 <setCLK>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e8bd4010     pop    {r4, lr}
  10:    e12fff1e     bx    lr

00000014 <setConfig>:
  14:    e92d4010     push    {r4, lr}
  18:    e3a00006     mov    r0, #6
  1c:    ebfffffe     bl    0 <dummy>
  20:    e8bd4010     pop    {r4, lr}
  24:    e12fff1e     bx    lr

00000028 <setSomethingElse>:
  28:    e92d4010     push    {r4, lr}
  2c:    e3a00007     mov    r0, #7
  30:    ebfffffe     bl    0 <dummy>
  34:    e8bd4010     pop    {r4, lr}
  38:    e12fff1e     bx    lr

0000003c <initModule>:
  3c:    e92d4010     push    {r4, lr}
  40:    e3a00005     mov    r0, #5
  44:    ebfffffe     bl    0 <dummy>
  48:    e3a00006     mov    r0, #6
  4c:    ebfffffe     bl    0 <dummy>
  50:    e3a00007     mov    r0, #7
  54:    ebfffffe     bl    0 <dummy>
  58:    e8bd4010     pop    {r4, lr}
  5c:    e12fff1e     bx    lr

Đây là những gì hầu hết các câu trả lời ở đây đang nói với bạn, rằng bạn ngây thơ và tất cả điều này được tối ưu hóa và các chức năng được loại bỏ. Chà, chúng không bị xóa vì chúng được định nghĩa toàn cầu theo mặc định. Chúng tôi có thể loại bỏ chúng nếu không cần thiết ngoài tập tin này.

void dummy (unsigned int);

static void setCLK()
{
    // Code to set the clock
    dummy(5);
}

static void setConfig()
{
    // Code to set the configuration
    dummy(6);
}

static void setSomethingElse()
{
   // 1 line code to write something to a register.
    dummy(7);
}

void initModule()
{
   setCLK();
   setConfig();
   setSomethingElse();
}

loại bỏ chúng ngay bây giờ khi chúng được nội tuyến.

Disassembly of section .text:

00000000 <initModule>:
   0:    e92d4010     push    {r4, lr}
   4:    e3a00005     mov    r0, #5
   8:    ebfffffe     bl    0 <dummy>
   c:    e3a00006     mov    r0, #6
  10:    ebfffffe     bl    0 <dummy>
  14:    e3a00007     mov    r0, #7
  18:    ebfffffe     bl    0 <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

Nhưng thực tế là khi bạn tiếp nhận nhà cung cấp chip hoặc thư viện BSP,

Disassembly of section .text:

00000000 <_start>:
   0:    e3a0d902     mov    sp, #32768    ; 0x8000
   4:    eb000010     bl    4c <initModule>
   8:    eafffffe     b    8 <_start+0x8>

0000000c <dummy>:
   c:    e12fff1e     bx    lr

00000010 <setCLK>:
  10:    e92d4010     push    {r4, lr}
  14:    e3a00005     mov    r0, #5
  18:    ebfffffb     bl    c <dummy>
  1c:    e8bd4010     pop    {r4, lr}
  20:    e12fff1e     bx    lr

00000024 <setConfig>:
  24:    e92d4010     push    {r4, lr}
  28:    e3a00006     mov    r0, #6
  2c:    ebfffff6     bl    c <dummy>
  30:    e8bd4010     pop    {r4, lr}
  34:    e12fff1e     bx    lr

00000038 <setSomethingElse>:
  38:    e92d4010     push    {r4, lr}
  3c:    e3a00007     mov    r0, #7
  40:    ebfffff1     bl    c <dummy>
  44:    e8bd4010     pop    {r4, lr}
  48:    e12fff1e     bx    lr

0000004c <initModule>:
  4c:    e92d4010     push    {r4, lr}
  50:    ebffffee     bl    10 <setCLK>
  54:    ebfffff2     bl    24 <setConfig>
  58:    ebfffff6     bl    38 <setSomethingElse>
  5c:    e8bd4010     pop    {r4, lr}
  60:    e12fff1e     bx    lr

Bạn chắc chắn sẽ bắt đầu thêm chi phí, có chi phí đáng chú ý cho hiệu suất và không gian. Một vài đến vài phần trăm của mỗi tùy thuộc vào mức độ nhỏ của từng chức năng.

Tại sao điều này được thực hiện anyway? Một số trong đó là tập hợp các giáo sư sẽ hoặc vẫn dạy để làm cho mã chấm điểm dễ dàng hơn. Các chức năng phải phù hợp trên một trang (trở lại khi bạn in tác phẩm ra giấy), không làm điều này, không làm điều đó, v.v ... Rất nhiều trong số đó là tạo các thư viện với các tên chung cho các mục tiêu khác nhau. Nếu bạn có hàng chục họ vi điều khiển, một số trong đó chia sẻ các thiết bị ngoại vi và một số không, có thể ba hoặc bốn hương vị UART khác nhau được trộn lẫn giữa các gia đình, GPIO khác nhau, bộ điều khiển SPI, v.v. Bạn có thể có chức năng gpio_init () chung, get_timer_count (), v.v. Và sử dụng lại những trừu tượng đó cho các thiết bị ngoại vi khác nhau.

Nó trở thành một trường hợp chủ yếu là khả năng bảo trì và thiết kế phần mềm, với một số khả năng dễ đọc. Khả năng bảo trì, khả năng đọc và hiệu suất bạn không thể có tất cả; bạn chỉ có thể chọn một hoặc hai lần, không phải cả ba.

Đây là rất nhiều câu hỏi dựa trên ý kiến, và ở trên cho thấy ba cách chính điều này có thể đi. Như con đường nào là TỐT NHẤT mà là ý kiến ​​nghiêm túc. Là làm tất cả các công việc trong một chức năng? Một câu hỏi dựa trên ý kiến, một số người nghiêng về hiệu suất, một số xác định tính mô đun và phiên bản dễ đọc của họ là TỐT NHẤT. Vấn đề thú vị với những gì nhiều người gọi là khả năng đọc là vô cùng đau đớn; để "xem" mã bạn phải mở 50-10.000 tệp cùng một lúc và bằng cách nào đó hãy cố gắng xem tuyến tính các hàm theo thứ tự thực hiện để xem điều gì đang xảy ra. Tôi thấy rằng điều ngược lại với khả năng đọc, nhưng những người khác thấy nó có thể đọc được vì mỗi mục phù hợp với cửa sổ màn hình / trình chỉnh sửa và có thể được sử dụng toàn bộ sau khi họ ghi nhớ các chức năng được gọi và / hoặc có một trình soạn thảo có thể bật và tắt mỗi chức năng trong một dự án.

Đó là một yếu tố lớn khác khi bạn thấy các giải pháp khác nhau. Trình soạn thảo văn bản, IDE, v.v ... rất cá nhân và vượt xa vi vs Emacs. Hiệu quả lập trình, các dòng mỗi ngày / tháng tăng lên nếu bạn cảm thấy thoải mái và hiệu quả với công cụ bạn đang sử dụng. Các tính năng của công cụ có thể / sẽ cố ý hoặc không nghiêng về cách người hâm mộ của công cụ đó viết mã. Và kết quả là nếu một cá nhân viết các thư viện này, dự án ở một mức độ nào đó phản ánh những thói quen này. Ngay cả khi đó là một nhóm, nhà phát triển chính hoặc thói quen / sở thích của ông chủ có thể bị ép buộc vào phần còn lại của nhóm.

Các tiêu chuẩn mã hóa có rất nhiều sở thích cá nhân bị chôn vùi trong chúng, vi tôn giáo so với Emacs một lần nữa, các tab so với không gian, cách các dấu ngoặc được xếp hàng, v.v. Và chúng chơi theo cách các thư viện được thiết kế ở một mức độ nào đó.

BẠN nên viết như thế nào? Tuy nhiên, bạn muốn, thực sự không có câu trả lời sai nếu nó hoạt động. Có mã xấu hoặc rủi ro chắc chắn, nhưng nếu được viết để bạn có thể duy trì nó khi cần, nó đáp ứng mục tiêu thiết kế của bạn, từ bỏ khả năng đọc và một số khả năng bảo trì nếu hiệu suất là quan trọng hoặc ngược lại. Bạn có thích tên biến ngắn để một dòng mã sẽ phù hợp với chiều rộng của cửa sổ soạn thảo không? Hoặc các tên mô tả quá dài để tránh nhầm lẫn, nhưng khả năng đọc bị giảm vì bạn không thể có một dòng trên một trang; bây giờ nó bị phá vỡ trực quan, rối tung với dòng chảy.

Bạn sẽ không đánh nhà chạy lần đầu tiên tại dơi. Có thể / nên mất nhiều thập kỷ để thực sự xác định phong cách của bạn. Đồng thời, qua thời gian đó, phong cách của bạn có thể thay đổi, nghiêng một chiều, rồi nghiêng người khác.

Bạn sẽ nghe thấy rất nhiều không tối ưu hóa, không bao giờ tối ưu hóa và tối ưu hóa sớm. Nhưng như được hiển thị, các thiết kế như thế này ngay từ đầu tạo ra các vấn đề về hiệu suất, sau đó bạn bắt đầu thấy các bản hack để giải quyết vấn đề đó thay vì thiết kế lại từ đầu để thực hiện. Tôi đồng ý có những tình huống, một hàm duy nhất một vài dòng mã mà bạn có thể cố gắng để thao tác trình biên dịch dựa trên nỗi sợ về trình biên dịch sẽ làm gì khác (lưu ý với kinh nghiệm loại mã hóa này trở nên dễ dàng và tự nhiên, tối ưu hóa khi bạn viết biết trình biên dịch sẽ biên dịch mã như thế nào), sau đó bạn muốn xác nhận nơi kẻ đánh cắp chu kỳ thực sự, trước khi tấn công nó.

Bạn cũng cần thiết kế mã của mình cho người dùng ở một mức độ nào đó. Nếu đây là dự án của bạn, bạn là nhà phát triển duy nhất; đó là bất cứ điều gì bạn muốn. Nếu bạn đang cố gắng tạo một thư viện để cho đi hoặc bán, bạn có thể muốn làm cho mã của mình trông giống như tất cả các thư viện khác, hàng trăm đến hàng nghìn tệp với các hàm nhỏ, tên hàm dài và tên biến dài. Mặc dù các vấn đề về khả năng đọc và các vấn đề về hiệu suất, IMO bạn sẽ thấy nhiều người sẽ có thể sử dụng mã đó.


4
Có thật không? Tôi có thể yêu cầu "một số mục tiêu" và "một số trình biên dịch"?
Dorian

Nó trông giống như một ARM8 32/64 bit, có thể từ một PI raspbery sau đó là một vi điều khiển thông thường. Bạn đã đọc câu đầu tiên trong câu hỏi?
Dorian

Vâng, trình biên dịch không loại bỏ các hàm toàn cầu không sử dụng, nhưng trình liên kết thì có. Nếu nó được cấu hình và sử dụng đúng cách, chúng sẽ không hiển thị trong tệp thực thi.
berendi - phản đối

Nếu ai đó đang tự hỏi trình biên dịch nào có thể tối ưu hóa qua các khoảng trống của tệp: trình biên dịch IAR hỗ trợ biên dịch đa tệp (đó là cách họ gọi nó), cho phép tối ưu hóa tệp chéo. Nếu bạn ném tất cả các tệp c / cpp vào nó trong một lần, bạn sẽ kết thúc với một tệp thực thi có chứa một hàm duy nhất: main. Những lợi ích hiệu suất có thể khá sâu sắc.
Arsenal

3
@Arsenal Tất nhiên gcc hỗ trợ nội tuyến, thậm chí trên các đơn vị biên dịch nếu được gọi đúng. Xem gcc.gnu.org/onlinesocs/gcc/Optizes-Options.html và tìm tùy chọn -flto.
Peter - Tái lập Monica

1

Quy tắc rất chung - trình biên dịch có thể tối ưu hóa tốt hơn bạn. Tất nhiên, có những trường hợp ngoại lệ nếu bạn đang thực hiện những việc rất chuyên sâu, nhưng nhìn chung nếu bạn muốn tối ưu hóa tốt cho tốc độ hoặc kích thước mã, hãy chọn trình biên dịch của bạn một cách khôn ngoan.


Đáng buồn thay, điều đó đúng với hầu hết các lập trình viên ngày nay.
Dorian

0

Nó chắc chắn phụ thuộc vào phong cách mã hóa của riêng bạn. Một quy tắc chung được đưa ra là, tên biến cũng như tên hàm phải rõ ràng và tự giải thích nhất có thể. Càng nhiều cuộc gọi phụ hoặc dòng mã bạn đặt vào một chức năng, càng khó xác định một nhiệm vụ rõ ràng cho một chức năng đó. Trong ví dụ của bạn, bạn có một chức năng initModule()khởi nhét và kêu gọi phụ thói quen mà sau đó đặt đồng hồ hoặc thiết lập các cấu hình . Bạn có thể nói rằng chỉ bằng cách đọc tên của hàm. Nếu bạn đặt tất cả mã từ các chương trình con vào initModule()trực tiếp, nó sẽ không rõ ràng hơn những gì hàm thực sự làm. Nhưng như thường lệ, nó chỉ là một hướng dẫn.


Cảm ơn bạn đã trả lời của bạn. Tôi có thể thay đổi kiểu nếu cần cho hiệu năng nhưng câu hỏi ở đây là tính dễ đọc của mã có ảnh hưởng đến hiệu suất không?
MaNyYaCk

Một cuộc gọi chức năng sẽ dẫn đến một cuộc gọi hoặc một lệnh jmp nhưng theo tôi đó là sự hy sinh tài nguyên không đáng kể. Nếu bạn sử dụng các mẫu thiết kế, đôi khi bạn kết thúc với hàng tá lớp lệnh gọi trước khi bạn đạt được mã thực tế bị cắt.
po.pe

@Humpawumpa - Nếu bạn đang viết cho một vi điều khiển chỉ có 256 hoặc 64 byte RAM thì hàng tá lớp gọi chức năng không phải là sự hy sinh không đáng kể, điều đó là không thể
uɐɪ

Có, nhưng đây là hai thái cực ... thông thường bạn có hơn 256 byte và đang sử dụng ít hơn một chục lớp - hy vọng là vậy.
po.pe

0

Nếu một chức năng thực sự chỉ làm một việc rất nhỏ, hãy xem xét thực hiện nó static inline .

Thêm nó vào một tệp tiêu đề thay vì tệp C và sử dụng các từ static inlineđể định nghĩa nó:

static inline void setCLK()
{
    //code to set the clock
}

Bây giờ, nếu hàm thậm chí dài hơn một chút, chẳng hạn như trên 3 dòng, có lẽ nên tránh static inlinevà thêm nó vào tệp .c. Rốt cuộc, các hệ thống nhúng có bộ nhớ hạn chế và bạn không muốn tăng kích thước mã quá nhiều.

Ngoài ra, nếu bạn xác định hàm trong file1.cvà sử dụng nó từ đó file2.c, trình biên dịch sẽ không tự động nội tuyến. Tuy nhiên, nếu bạn định nghĩa nó file1.hnhư là một static inlinehàm, rất có thể trình biên dịch của bạn sẽ nội tuyến nó.

Các static inlinechức năng này cực kỳ hữu ích trong lập trình hiệu suất cao. Tôi đã tìm thấy chúng để tăng hiệu suất mã thường xuyên hơn ba lần.


"chẳng hạn như trên 3 dòng" - số dòng không liên quan gì đến nó; chi phí nội tuyến có mọi thứ để làm với nó. Tôi có thể viết một hàm 20 dòng hoàn hảo cho nội tuyến và một hàm 3 dòng thật kinh khủng khi nội tuyến (ví dụ hàmA () gọi hàmB () 3 lần, hàmB () gọi hàmC () 3 lần và một vài cấp độ khác).
Jason S

Ngoài ra, nếu bạn xác định hàm trong file1.cvà sử dụng nó từ đó file2.c, trình biên dịch sẽ không tự động nội tuyến. Sai . Xem ví dụ -fltotrong gcc hoặc clang.
berendi - phản đối

0

Một khó khăn khi cố gắng viết mã hiệu quả và đáng tin cậy cho vi điều khiển là một số trình biên dịch không thể xử lý một số ngữ nghĩa nhất định trừ khi mã sử dụng các lệnh cụ thể của trình biên dịch hoặc vô hiệu hóa nhiều tối ưu hóa.

Ví dụ: nếu có một hệ thống lõi đơn với thói quen dịch vụ ngắt [chạy bằng dấu thời gian hoặc bất cứ điều gì]:

volatile uint32_t *magic_write_ptr,magic_write_count;
void handle_interrupt(void)
{
  if (magic_write_count)
  {
    magic_write_count--;
    send_data(*magic_write_ptr++)
  }
}

có thể viết các hàm để bắt đầu thao tác ghi nền hoặc đợi cho nó hoàn thành:

void wait_for_background_write(void)
{
  while(magic_write_count)
    ;
}
void start_background_write(uint32_t *dat, uint32_t count)
{
  wait_for_background_write();
  background_write_ptr = dat;
  background_write_count = count;
}

và sau đó gọi mã đó bằng cách sử dụng:

uint32_t buff[16];

... write first set of data into buff
start_background_write(buff, 16);
... do some stuff unrelated to buff
wait_for_background_write();

... write second set of data into buff
start_background_write(buff, 16);
... etc.

Thật không may, với tối ưu hóa hoàn toàn được kích hoạt, một trình biên dịch "thông minh" như gcc hoặc clang sẽ quyết định rằng không có cách nào tập hợp ghi đầu tiên có thể có bất kỳ ảnh hưởng nào đến khả năng quan sát của chương trình và do đó chúng có thể được tối ưu hóa. Trình biên dịch chất lượng nhưicc ít có xu hướng làm điều này nếu hành động thiết lập ngắt và chờ hoàn thành liên quan đến cả việc ghi dễ bay hơi và đọc dễ bay hơi (như trường hợp ở đây), nhưng nền tảng được nhắm mục tiêu bởiicc không phổ biến cho các hệ thống nhúng.

Tiêu chuẩn cố tình bỏ qua các vấn đề về chất lượng thực hiện, cho thấy có một số cách hợp lý mà cấu trúc trên có thể được xử lý:

  1. Một triển khai chất lượng dành riêng cho các lĩnh vực như crunching số cao cấp có thể hợp lý mong đợi rằng mã được viết cho các trường đó sẽ không chứa các cấu trúc như trên.

  2. Việc triển khai chất lượng có thể xử lý tất cả các truy cập vào volatilecác đối tượng như thể chúng có thể kích hoạt các hành động sẽ truy cập vào bất kỳ đối tượng nào có thể nhìn thấy ra thế giới bên ngoài.

  3. Việc triển khai đơn giản nhưng chất lượng tốt dành cho các hệ thống nhúng có thể xử lý tất cả các lệnh gọi đến các hàm không được đánh dấu là "nội tuyến" như thể chúng có thể truy cập bất kỳ đối tượng nào đã tiếp xúc với thế giới bên ngoài, ngay cả khi nó không xử lý volatilenhư được mô tả trong # 2.

Tiêu chuẩn không cố gắng đề xuất phương pháp nào ở trên là phù hợp nhất để thực hiện chất lượng, cũng như không yêu cầu việc triển khai "tuân thủ" phải có chất lượng đủ tốt để có thể sử dụng cho bất kỳ mục đích cụ thể nào. Do đó, một số trình biên dịch như gcc hoặc clang yêu cầu một cách hiệu quả rằng bất kỳ mã nào muốn sử dụng mẫu này phải được biên dịch với nhiều tối ưu hóa bị vô hiệu hóa.

Trong một số trường hợp, chắc chắn rằng các hàm I / O nằm trong một đơn vị biên dịch riêng biệt và trình biên dịch sẽ không có lựa chọn nào khác ngoài việc cho rằng chúng có thể truy cập vào bất kỳ tập hợp con tùy ý nào đã tiếp xúc với thế giới bên ngoài có thể là ít nhất hợp lý- cách viết mã tệ nạn sẽ hoạt động đáng tin cậy với gcc và clang. Tuy nhiên, trong những trường hợp như vậy, mục tiêu không phải là để tránh chi phí thêm cho một cuộc gọi chức năng không cần thiết, mà là chấp nhận chi phí không cần thiết để đổi lấy việc có được ngữ nghĩa cần thiết.


"chắc chắn rằng các chức năng I / O nằm trong một đơn vị biên dịch riêng biệt" ... không phải là cách chắc chắn để ngăn chặn các vấn đề tối ưu hóa như thế này. Ít nhất LLVM và tôi tin rằng GCC sẽ thực hiện tối ưu hóa toàn bộ chương trình trong nhiều trường hợp, do đó có thể quyết định nội tuyến các hàm IO của bạn ngay cả khi chúng ở trong một đơn vị biên dịch riêng biệt.
Jules

@Jules: Không phải tất cả các triển khai đều phù hợp để viết phần mềm nhúng. Vô hiệu hóa tối ưu hóa toàn bộ chương trình có thể là cách ít tốn kém nhất để buộc gcc hoặc clang hành xử như một triển khai chất lượng phù hợp với mục đích đó.
supercat

@Jules: Việc triển khai chất lượng cao hơn dành cho lập trình nhúng hoặc hệ thống nên có thể định cấu hình để có ngữ nghĩa phù hợp với mục đích đó mà không phải vô hiệu hóa hoàn toàn tối ưu hóa toàn bộ chương trình (ví dụ: có tùy chọn xử lý volatiletruy cập như thể chúng có khả năng kích hoạt tùy ý truy cập vào các đối tượng khác), nhưng vì bất kỳ lý do gì gcc và clang thà coi vấn đề chất lượng thực hiện là một lời mời để hành xử theo cách vô dụng.
supercat

1
Ngay cả việc triển khai "chất lượng cao nhất" cũng không đúng mã lỗi. Nếu buffkhông được khai báo volatile, nó sẽ không được coi là một biến dễ bay hơi, truy cập vào nó có thể được sắp xếp lại hoặc tối ưu hóa hoàn toàn nếu không được sử dụng sau này. Quy tắc rất đơn giản: đánh dấu tất cả các biến có thể được truy cập bên ngoài luồng chương trình bình thường (được trình biên dịch nhìn thấy) là volatile. Là nội dung của bufftruy cập trong một xử lý ngắt? Đúng. Sau đó, nó nên được volatile.
berendi - phản đối

@berendi: Trình biên dịch có thể cung cấp các đảm bảo vượt quá những gì Tiêu chuẩn yêu cầu và trình biên dịch chất lượng sẽ làm như vậy. Việc triển khai tự do chất lượng cho các hệ thống nhúng sẽ cho phép các lập trình viên tổng hợp các cấu trúc mutex, về cơ bản là những gì mã làm. Khi magic_write_countbằng 0, bộ lưu trữ được sở hữu bởi dòng chính. Khi nó khác không, nó thuộc sở hữu của trình xử lý ngắt. Làm cho buffdễ bay hơi sẽ yêu cầu mọi chức năng ở bất cứ nơi nào hoạt động khi nó sử dụng các volatilecon trỏ đủ tiêu chuẩn, điều này sẽ làm giảm tối ưu hóa hơn nhiều so với việc có một trình biên dịch ...
supercat
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.