Tại sao một số chương trình C được viết trong một tệp nguồn lớn?


88

Ví dụ, SysInternals cụ "FileMon" từ quá khứ có một trình điều khiển kernel-mode có mã nguồn là hoàn toàn trong một tập tin 4.000 dòng. Điều tương tự cho chương trình ping đầu tiên từng được viết (~ 2.000 LỘC).

Câu trả lời:


143

Sử dụng nhiều tập tin luôn đòi hỏi chi phí quản trị bổ sung. Người ta phải thiết lập một tập lệnh xây dựng và / hoặc makefile với các giai đoạn biên dịch và liên kết riêng biệt, đảm bảo các phụ thuộc giữa các tệp khác nhau được quản lý chính xác, viết tập lệnh "zip" để phân phối mã nguồn dễ dàng hơn qua email hoặc tải xuống, và vì vậy trên, bật. Các IDE hiện đại ngày nay thường chịu rất nhiều gánh nặng đó, nhưng tôi khá chắc chắn vào thời điểm khi chương trình ping đầu tiên được viết, không có IDE nào như vậy. Và đối với các tệp nhỏ ~ 4000 LỘC, không có IDE như vậy quản lý nhiều tệp cho bạn, việc đánh đổi giữa chi phí được đề cập và lợi ích từ việc sử dụng nhiều tệp có thể cho phép mọi người đưa ra quyết định cho cách tiếp cận tệp duy nhất.


9
"Và đối với các tệp nhỏ tới ~ 4000 LỘC ..." Tôi hiện đang làm việc như một nhà phát triển JS. Khi tôi có một tệp chỉ dài 400 dòng mã, tôi cảm thấy lo lắng về việc nó trở nên lớn đến mức nào! (Nhưng chúng tôi có hàng tá và hàng chục tệp trong dự án của chúng tôi.)
Kevin

36
@Kevin: Một sợi tóc trên đầu tôi quá ít, một sợi tóc trong súp của tôi quá nhiều ;-) AFAIK trong nhiều tệp không gây ra nhiều chi phí quản trị như trong "C mà không có IDE hiện đại".
Doc Brown

4
@Kevin JS là một con thú khá khác nhau mặc dù. JS được truyền đến người dùng cuối mỗi khi người dùng tải trang web và trình duyệt của họ không được lưu trong bộ nhớ cache. C chỉ phải truyền mã một lần, sau đó người ở đầu kia biên dịch mã và nó vẫn được biên dịch (rõ ràng có trường hợp ngoại lệ, nhưng đó là trường hợp sử dụng dự kiến ​​chung). Ngoài ra, nội dung C có xu hướng là mã kế thừa, vì phần lớn trong số '4000 dòng là bình thường' mà mọi người đang mô tả trong các nhận xét.
Pharap

5
@Kevin Bây giờ hãy đi xem cách underscore.js (1700 loc, một tệp) và vô số các thư viện khác được phân phối được viết. Javascript thực sự gần như tồi tệ như C liên quan đến mô đun hóa và triển khai.
Voo

2
@Pharap Tôi nghĩ rằng anh ta có nghĩa là sử dụng một cái gì đó như Webpack trước khi triển khai mã. Với Webpack, bạn có thể làm việc trên nhiều tệp và sau đó biên dịch chúng thành một gói.
Brian McCutchon

81

Bởi vì C không giỏi về mô đun hóa. Nó trở nên lộn xộn (các tệp tiêu đề và #incoides, các hàm ngoài, lỗi thời gian liên kết, v.v.) và càng nhiều mô-đun bạn mang vào, nó càng khó hơn.

Các ngôn ngữ hiện đại hơn có khả năng mô đun hóa tốt hơn một phần vì chúng học được từ những sai lầm của C và chúng giúp dễ dàng phân tách cơ sở mã của bạn thành các đơn vị nhỏ hơn, đơn giản hơn. Nhưng với C, có thể có ích để tránh hoặc giảm thiểu tất cả những rắc rối đó, ngay cả khi điều đó có nghĩa là bỏ đi những gì sẽ được coi là quá nhiều mã vào một tệp.


38
Tôi nghĩ thật không công bằng khi mô tả phương pháp C là 'sai lầm'; chúng là những quyết định hoàn toàn hợp lý và hợp lý tại thời điểm chúng được đưa ra.
Jack Aidley ngày

14
Không có công cụ mô đun hóa nào là đặc biệt phức tạp. Nó có thể được làm cho phức tạp bởi phong cách mã hóa xấu, nhưng nó không khó để hiểu hoặc thực hiện, và không ai trong số đó có thể được phân loại là "sai lầm". Lý do thực sự, theo câu trả lời của Snowman, là việc tối ưu hóa trên nhiều tệp nguồn trước đây không tốt lắm và trình điều khiển FileMon yêu cầu hiệu năng cao. Ngoài ra, trái với quan điểm của OP, đó không phải là những tệp đặc biệt lớn.
Graham

8
@Graham Bất kỳ tệp nào lớn hơn 1000 dòng mã phải được coi là mùi mã.
Mason Wheeler

11
@JackAidley nó không công bằng chút nào , có một lỗi là không loại trừ lẫn nhau khi nói rằng đó là một quyết định hợp lý tại thời điểm đó. Những sai lầm không thể tránh khỏi khi đưa ra thông tin không hoàn hảo và thời gian hạn chế và nên học hỏi từ việc không che giấu hoặc phân loại lại một cách đáng xấu hổ để giữ thể diện.
Jared Smith

8
Bất cứ ai tuyên bố rằng cách tiếp cận của C không phải là một lỗi không hiểu làm thế nào một tệp C dường như mười phần thực sự có thể là tệp mười nghìn lót với tất cả các tiêu đề #include: d. Điều này có nghĩa là mỗi tệp trong dự án của bạn có hiệu quả ít nhất là mười nghìn dòng, bất kể số lượng dòng được đưa ra bởi "wc -l" là bao nhiêu. Hỗ trợ tốt hơn cho tính mô đun sẽ dễ dàng cắt thời gian phân tích cú pháp và biên dịch thành một phần rất nhỏ.
juhist ngày

37

Ngoài các lý do lịch sử, có một lý do để sử dụng điều này trong phần mềm nhạy cảm hiệu năng hiện đại. Khi tất cả các mã nằm trong một đơn vị biên dịch, trình biên dịch có thể thực hiện tối ưu hóa toàn bộ chương trình. Với các đơn vị biên dịch riêng biệt, trình biên dịch có thể tối ưu hóa toàn bộ chương trình theo một số cách nhất định (ví dụ: nội tuyến mã nhất định).

Trình liên kết chắc chắn có thể thực hiện một số tối ưu hóa ngoài những gì trình biên dịch có thể làm, nhưng không phải tất cả. Ví dụ: các trình liên kết hiện đại thực sự rất giỏi trong việc tách biệt các hàm không được ước tính, thậm chí trên nhiều tệp đối tượng. Họ có thể thực hiện một số tối ưu hóa khác, nhưng không có gì giống như những gì trình biên dịch có thể làm bên trong một hàm.

Một ví dụ nổi tiếng của mô-đun mã nguồn đơn là SQLite. Bạn có thể đọc thêm về nó trên trang Hợp nhất SQLite .

1. Tóm tắt

Hơn 100 tệp nguồn riêng biệt được ghép nối thành một tệp lớn mã C có tên "sqlite3.c" và được gọi là "sự hợp nhất". Sự hợp nhất chứa mọi thứ mà một ứng dụng cần để nhúng SQLite. Tệp hợp nhất có độ dài hơn 180.000 dòng và kích thước hơn 6 megabyte.

Kết hợp tất cả mã cho SQLite vào một tệp lớn giúp SQLite dễ triển khai hơn - chỉ có một tệp để theo dõi. Và bởi vì tất cả các mã nằm trong một đơn vị dịch thuật, trình biên dịch có thể thực hiện tối ưu hóa quy trình liên tục tốt hơn dẫn đến mã máy nhanh hơn từ 5% đến 10%.


15
Nhưng lưu ý rằng trình biên dịch C hiện đại có thể thực hiện tối ưu hóa toàn bộ chương trình của nhiều tệp nguồn (mặc dù không phải nếu bạn biên dịch chúng thành các tệp đối tượng riêng lẻ trước).
Davislor

10
@Davislor Hãy nhìn vào tập lệnh xây dựng điển hình: trình biên dịch không thực sự sẽ làm điều đó.

4
Việc thay đổi tập lệnh xây dựng thành dễ dàng hơn đáng kể $(CC) $(CFLAGS) $(LDFLAGS) -o $(TARGET) $(CFILES)so với việc di chuyển mọi thứ sang một tệp soudce. Bạn thậm chí có thể thực hiện việc biên dịch toàn bộ chương trình như là một mục tiêu thay thế cho tập lệnh xây dựng truyền thống bỏ qua việc biên dịch lại các tệp nguồn không thay đổi, tương tự như cách mọi người có thể tắt cấu hình và gỡ lỗi cho mục tiêu sản xuất. Bạn không có tùy chọn đó nếu mọi thứ nằm trong một nguồn lớn. Đó không phải là những gì mọi người đã quen, nhưng không có gì rườm rà về nó.
Davislor

9
@Davislor tối ưu hóa toàn bộ chương trình / tối ưu hóa thời gian liên kết (LTO) cũng hoạt động khi bạn "biên dịch" mã thành các tệp đối tượng riêng lẻ (tùy thuộc vào "biên dịch" nghĩa là gì đối với bạn). Ví dụ, LTO của GCC sẽ thêm biểu diễn mã được phân tích cú pháp của nó vào các tệp đối tượng riêng lẻ vào thời gian biên dịch và tại thời điểm liên kết sẽ sử dụng mã đó thay vì mã đối tượng (cũng có mặt) để biên dịch lại và xây dựng toàn bộ chương trình. Vì vậy, điều này hoạt động với các thiết lập xây dựng biên dịch thành các tệp đối tượng riêng lẻ trước, mặc dù mã máy được tạo bởi quá trình biên dịch ban đầu bị bỏ qua.
mơ mộng ngày

8
JsonCpp hiện nay cũng vậy. Điều quan trọng là các tệp không theo cách này trong quá trình phát triển.
Các cuộc đua nhẹ nhàng trong quỹ đạo

15

Ngoài yếu tố đơn giản mà người trả lời khác đã đề cập, nhiều chương trình C được viết bởi một cá nhân.

Khi bạn có một nhóm các cá nhân, việc phân chia ứng dụng trên một số tệp nguồn để tránh xung đột vô cớ trong các thay đổi mã trở nên mong muốn. Đặc biệt là khi có cả những lập trình viên tiên tiến và rất trẻ đang làm việc trong dự án.

Khi một người làm việc một mình, đó không phải là vấn đề.

Cá nhân, tôi sử dụng nhiều tập tin dựa trên chức năng như một thói quen. Nhưng đó chỉ là tôi.


4
@OskarSkog Nhưng bạn sẽ không bao giờ sửa đổi một tệp cùng lúc với bản thân tương lai của bạn.
Loren Pechtel

2

Bởi vì C89 không có inlinechức năng. Điều đó có nghĩa là việc chia nhỏ tệp của bạn thành các hàm gây ra chi phí đẩy các giá trị lên ngăn xếp và nhảy xung quanh. Điều này đã thêm khá nhiều chi phí cho việc triển khai mã trong 1 câu lệnh chuyển đổi lớn (vòng lặp sự kiện). Nhưng một vòng lặp sự kiện luôn khó thực hiện hiệu quả (hoặc thậm chí chính xác) hơn nhiều so với giải pháp được mô đun hóa nhiều hơn. Vì vậy, đối với các dự án quy mô lớn, mọi người vẫn sẽ chọn không tham gia mô đun hóa. Nhưng khi họ đã nghĩ ra thiết kế trước và có thể kiểm soát trạng thái trong 1 tuyên bố chuyển đổi, họ đã chọn điều đó.

Ngày nay, ngay cả trong C, người ta không cần phải hy sinh hiệu năng để mô đun hóa bởi vì ngay cả trong các chức năng C cũng có thể được nội tuyến.


2
Các hàm C có thể giống như nhiều dòng trong 89 như ngày nay, nội tuyến là thứ nên được sử dụng gần như không bao giờ - trình biên dịch biết rõ hơn bạn trong hầu hết các tình huống. Và hầu hết các tệp 4k LỘC đó không phải là một hàm khổng lồ - đó là một kiểu mã hóa khủng khiếp sẽ không có bất kỳ lợi ích hiệu suất đáng chú ý nào.
Voo

@Voo, tôi không biết tại sao bạn lại đề cập đến phong cách mã hóa. Tôi đã không ủng hộ nó. Trong thực tế, tôi đã đề cập rằng trong hầu hết các trường hợp, nó đảm bảo một giải pháp kém hiệu quả hơn do việc triển khai bị phá hỏng. Tôi cũng đã đề cập rằng đó là một ý tưởng tồi bởi vì nó không mở rộng quy mô (cho các dự án lớn hơn). Phải nói rằng, trong các vòng lặp rất chặt chẽ (đó là những gì xảy ra trong mã mạng gần với phần cứng), không cần thiết phải đẩy và bật các giá trị trên / tắt ngăn xếp (khi gọi các chức năng) sẽ làm tăng thêm chi phí của chương trình đang chạy. Đây không phải là một giải pháp tuyệt vời. Nhưng nó là thứ tốt nhất có sẵn tại thời điểm đó.
Dmitry Rubanovich

2
Lưu ý bắt buộc: từ khóa nội tuyến chỉ có một chút liên quan đến tối ưu hóa nội tuyến. Nó không phải là một gợi ý đặc biệt cho trình biên dịch để thực hiện tối ưu hóa đó, thay vào đó nó phải thực hiện với việc liên kết với các biểu tượng trùng lặp.
hyde

@Dmitry Vấn đề là tuyên bố rằng vì không có inlinetừ khóa trong trình biên dịch C89 không thể nội tuyến, đó là lý do tại sao bạn phải viết mọi thứ trong một hàm khổng lồ là không chính xác. Bạn gần như không bao giờ sử dụng inlinenhư một tối ưu hóa hiệu suất - trình biên dịch nói chung sẽ biết rõ hơn bạn dù thế nào (và cũng có thể bỏ qua từ khóa).
Voo

@Voo: Một lập trình viên và một trình biên dịch nói chung sẽ biết một số thứ khác không có. Các inlinetừ khóa có ngữ nghĩa mối liên kết liên quan đến mà quan trọng hơn vấn đề có hay không để thực hiện tối ưu hóa nội dòng, nhưng một số hiện thực có chỉ thị khác để kiểm soát trong lót và những thứ như vậy đôi khi có thể rất quan trọng. Trong một số trường hợp, một chức năng có thể trông quá lớn để có giá trị xếp hàng, nhưng việc gập liên tục có thể làm giảm kích thước và thời gian thực hiện xuống gần như không có gì. Một trình biên dịch không được khuyến khích mạnh mẽ để khuyến khích nội tuyến có thể không ...
supercat

1

Đây được coi là một ví dụ về sự tiến hóa, điều mà tôi ngạc nhiên chưa được đề cập.

Trong những ngày đen tối của lập trình, việc biên dịch một TẬP TIN có thể mất vài phút. Nếu một chương trình được mô đun hóa, thì việc bao gồm các tệp tiêu đề cần thiết (không có tùy chọn tiêu đề được biên dịch trước) sẽ là một nguyên nhân bổ sung đáng kể của sự chậm lại. Ngoài ra, trình biên dịch có thể chọn / cần giữ một số thông tin trên đĩa, có thể không có lợi ích của tệp hoán đổi tự động.

Các thói quen mà các yếu tố môi trường này đã dẫn đến các hoạt động phát triển đang diễn ra và chỉ dần dần thích nghi theo thời gian.

Tại thời điểm thu được từ việc sử dụng một tệp sẽ tương tự như chúng ta có được bằng cách sử dụng ổ SSD thay vì ổ cứng.

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.