Linux không thể biên dịch mà không tối ưu hóa GCC; hàm ý? [đóng cửa]


9

Người ta có thể tìm thấy một số chủ đề trên Internet như thế này:

http://www.gossamer-threads.com/lists/linux/kernel/972619

nơi mọi người phàn nàn họ không thể xây dựng Linux với -O0 và được thông báo rằng điều này không được hỗ trợ; Linux dựa vào tối ưu hóa GCC cho các chức năng tự động nội tuyến, loại bỏ mã chết và nếu không thì thực hiện những việc cần thiết để quá trình xây dựng thành công.

Tôi đã tự xác minh điều này cho ít nhất một số hạt nhân 3.x. Những cái tôi đã thử thoát sau vài giây thời gian xây dựng nếu được biên dịch với -O0.

Đây có phải thường được coi là thực hành mã hóa chấp nhận được? Là tối ưu hóa trình biên dịch, chẳng hạn như nội tuyến tự động, đủ dự đoán để dựa vào; ít nhất là khi làm việc với chỉ một trình biên dịch? Các phiên bản tương lai của GCC có thể phá vỡ các bản dựng của các nhân Linux hiện tại với các tối ưu hóa mặc định (ví dụ -O2 hoặc -Os)?

Và trên một lưu ý mang tính mô phạm hơn: vì các hạt nhân 3.x không thể biên dịch mà không tối ưu hóa, chúng có nên được coi là mã C không chính xác về mặt kỹ thuật không?


4
Câu hỏi này không có chủ đề ở đây, nhưng sẽ là một câu hỏi hay trên Lập trình viên. Tôi nghĩ vậy.
Kyle Jones

Có lẽ nó có thể được chuyển sang Stack Overflow hoặc như vậy, nếu điều đó sẽ tốt hơn?
DanL4096

2
Tôi không đồng ý, vì câu hỏi này liên quan đến các cài đặt trình biên dịch GCC được điều chỉnh khác nhau giữa các bản phát hành. OP đang hỏi làm thế nào để sửa đổi các điều chỉnh một cách chung chung.
Eyoung100

1
Câu hỏi thực hành mã hóa phù hợp với Lập trình viên. Câu hỏi về phiên bản tương lai nào của GCC có thể phá vỡ các bản dựng nhân Linux sẽ dựa trên quan điểm, vì vậy câu hỏi đó sẽ bị loại bỏ.
Kyle Jones

Cách tạo phiên bản: stackoverflow.com/questions/29151235/
Ấn

Câu trả lời:


13

Bạn đã kết hợp với nhau một số câu hỏi khác nhau (nhưng có liên quan). Một vài trong số chúng không thực sự có chủ đề ở đây (ví dụ: tiêu chuẩn mã hóa), vì vậy tôi sẽ bỏ qua những điều đó.

Tôi sẽ bắt đầu với nếu kernel là "mã C không chính xác về mặt kỹ thuật". Tôi bắt đầu ở đây vì câu trả lời giải thích vị trí đặc biệt mà hạt nhân chiếm giữ, điều rất quan trọng để hiểu phần còn lại.

Là hạt nhân kỹ thuật mã C không chính xác?

Câu trả lời chắc chắn là "không chính xác".

Có một vài cách mà chương trình C có thể nói là không chính xác. Trước tiên, hãy lấy một vài cái đơn giản:

  • Một chương trình không tuân theo cú pháp C (nghĩa là có lỗi cú pháp) là không chính xác. Nhân sử dụng các phần mở rộng GNU khác nhau cho cú pháp C. Đó là, theo như tiêu chuẩn C có liên quan, lỗi cú pháp. (Tất nhiên, với GCC, họ không có. Hãy thử biên dịch với -std=c99 -pedantichoặc tương tự ...)
  • Một chương trình không làm những gì nó được thiết kế để làm là không chính xác. Hạt nhân là một chương trình khổng lồ và, ngay cả việc kiểm tra nhanh các thay đổi của nó sẽ chứng minh, chắc chắn là không. Hoặc, như chúng ta thường nói, nó có lỗi.

Tối ưu hóa có nghĩa là gì trong C

[LƯU Ý: Phần này có phần rất hạn chế các quy tắc thực tế; để biết chi tiết, xem tiêu chuẩn và tìm kiếm Stack Overflow.]

Bây giờ cho một trong đó có nhiều lời giải thích. Tiêu chuẩn C nói rằng mã nhất định phải tạo ra hành vi nhất định. Nó cũng cho biết một số điều có giá trị cú pháp C có "hành vi không xác định"; một ví dụ (không may là phổ biến!) là truy cập bên ngoài phần cuối của một mảng (ví dụ: tràn bộ đệm).

Hành vi không xác định là mạnh mẽ như vậy. Nếu một chương trình chứa nó, dù chỉ một chút xíu, tiêu chuẩn C không còn quan tâm đến hành vi nào mà chương trình thể hiện hoặc đầu ra mà trình biên dịch tạo ra khi đối mặt với nó.

Nhưng ngay cả khi chương trình chỉ chứa hành vi được xác định, C vẫn cho phép trình biên dịch mất nhiều thời gian. Như một ví dụ tầm thường (lưu ý: đối với các ví dụ của tôi, tôi đang bỏ qua #includecác dòng, v.v., để cho ngắn gọn):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}

Điều đó, tất nhiên, in 5 theo sau là một dòng mới. Đó là những gì được yêu cầu bởi tiêu chuẩn C.

Nếu bạn biên dịch chương trình đó và tháo rời đầu ra, bạn sẽ mong đợi malloc được gọi để lấy một số bộ nhớ, con trỏ được trả về được lưu trữ ở đâu đó (có thể là một thanh ghi), giá trị 3 được lưu vào bộ nhớ đó, sau đó thêm 2 vào bộ nhớ đó (có thể thậm chí yêu cầu tải, thêm và lưu trữ), sau đó bộ nhớ được sao chép vào ngăn xếp và cũng là một chuỗi điểm "%i\n"được đặt trên ngăn xếp, sau đó printfgọi hàm. Một chút công bằng. Nhưng thay vào đó, những gì bạn có thể thấy là như thể bạn đã viết:

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }

và đây là điều: tiêu chuẩn C cho phép điều đó. Tiêu chuẩn C chỉ quan tâm đến kết quả , không phải theo cách họ đạt được.

Đó là những gì tối ưu hóa trong C là về. Trình biên dịch đưa ra một cách thông minh hơn (thường là nhỏ hơn hoặc nhanh hơn, tùy thuộc vào các cờ) để đạt được kết quả theo yêu cầu của tiêu chuẩn C. Có một vài trường hợp ngoại lệ, chẳng hạn như -ffast-mathtùy chọn của GCC , nhưng nếu không thì mức tối ưu hóa không thay đổi hành vi của các chương trình chính xác về mặt kỹ thuật (nghĩa là các chương trình chỉ chứa hành vi được xác định).

Bạn có thể viết một kernel chỉ sử dụng hành vi được xác định không?

Hãy tiếp tục xem xét chương trình ví dụ của chúng tôi. Phiên bản chúng tôi đã viết, không phải những gì trình biên dịch biến nó thành. Điều đầu tiên chúng ta làm là gọi mallocđể có được một số bộ nhớ. Tiêu chuẩn C cho chúng ta biết những gì mallocnó làm, nhưng không phải nó làm như thế nào.

Nếu chúng ta nhìn vào một triển khai mallocnhằm mục đích rõ ràng (trái ngược với tốc độ), chúng ta sẽ thấy rằng nó tạo ra một số tòa nhà (chẳng hạn như mmapvới MAP_ANONYMOUS) để có được một khối lớn bộ nhớ. Nó bên trong giữ một số cấu trúc dữ liệu cho nó biết phần nào của đoạn đó được sử dụng so với miễn phí. Nó tìm thấy một đoạn miễn phí ít nhất lớn bằng những gì bạn yêu cầu, khắc số tiền bạn yêu cầu và trả về một con trỏ cho nó. Nó cũng hoàn toàn được viết bằng C và chỉ chứa hành vi được xác định. Nếu chủ đề của nó an toàn, nó có thể chứa một số cuộc gọi pthread.

Bây giờ, cuối cùng, nếu chúng ta nhìn vào những gì mmapkhông, chúng ta thấy tất cả các loại công cụ thú vị. Đầu tiên, nó thực hiện một số kiểm tra để xem hệ thống có đủ RAM miễn phí và / hoặc trao đổi để ánh xạ hay không. Tiếp theo, nó tìm thấy một số không gian địa chỉ miễn phí để đặt khối vào. Sau đó, nó chỉnh sửa cấu trúc dữ liệu được gọi là bảng trang và có thể thực hiện một loạt các cuộc gọi lắp ráp nội tuyến trên đường đi. Nó thực sự có thể tìm thấy một số trang bộ nhớ vật lý miễn phí (nghĩa là các bit thực tế trong các mô-đun DRAM thực tế) --- một quá trình có thể yêu cầu buộc bộ nhớ khác phải trao đổi ---. Nếu nó không làm điều đó cho toàn bộ khối được yêu cầu, thay vào đó, nó sẽ thiết lập mọi thứ để điều đó xảy ra khi bộ nhớ cho biết được truy cập lần đầu tiên. Phần lớn điều này được thực hiện với các bit lắp ráp nội tuyến, ghi vào các địa chỉ ma thuật khác nhau, v.v ... Lưu ý rằng nó cũng sử dụng các phần lớn của kernel, đặc biệt là nếu cần phải tráo đổi.

Việc lắp ráp nội tuyến, ghi vào các địa chỉ ma thuật, v.v ... đều nằm ngoài đặc tả C. Điều này không gây ngạc nhiên; C chạy trên nhiều kiến ​​trúc máy khác nhau, bao gồm một bó mà hầu như không thể tưởng tượng được vào đầu những năm 1970 khi C được phát minh. Ẩn mã cụ thể của máy đó là một phần cốt lõi của hạt nhân (và ở một mức độ nào đó thư viện C) dành cho.

Tất nhiên, nếu bạn quay lại chương trình ví dụ, nó trở nên rõ ràng printfphải tương tự. Thật rõ ràng làm thế nào để thực hiện tất cả các định dạng, vv trong tiêu chuẩn C; Nhưng thực sự có được nó trên màn hình? Hoặc dẫn đến một chương trình khác? Một lần nữa, rất nhiều phép thuật được thực hiện bởi kernel (và có thể là X11 hoặc Wayland).

Nếu bạn nghĩ về những thứ khác mà kernel làm, rất nhiều trong số chúng nằm ngoài C. Ví dụ, kernel đọc dữ liệu từ các đĩa (C không biết gì về đĩa, bus PCIe hoặc SATA) vào bộ nhớ vật lý (C chỉ biết về malloc, không phải của DIMM, MMU, v.v.), làm cho nó có thể thực thi được (C không biết gì về các bit thực thi của bộ xử lý), và sau đó gọi nó là các hàm (không chỉ bên ngoài C, rất không được phép).

Mối quan hệ giữa hạt nhân và trình biên dịch của nó

Nếu bạn nhớ từ trước, nếu một chương trình có hành vi không xác định, cho đến khi có liên quan đến tiêu chuẩn C, tất cả các cược đều bị tắt. Nhưng một hạt nhân thực sự phải chứa hành vi không xác định. Vì vậy, phải có một số mối quan hệ giữa kernel và trình biên dịch của nó, ít nhất là đủ để các nhà phát triển kernel có thể tự tin rằng kernel sẽ hoạt động mặc dù vi phạm tiêu chuẩn C. Ít nhất là trong trường hợp của Linux, điều này bao gồm kernel có một số kiến ​​thức về cách GCC hoạt động bên trong.

Làm thế nào nó có khả năng để phá vỡ?

Các phiên bản GCC trong tương lai có thể sẽ phá vỡ kernel. Tôi có thể nói điều này khá tự tin như nó đã xảy ra vài lần trước đây. Tất nhiên, những thứ như tối ưu hóa răng cưa nghiêm ngặt trong GCC cũng đã phá vỡ rất nhiều thứ bên cạnh kernel.

Cũng lưu ý rằng nội tuyến mà hạt nhân Linux phụ thuộc vào không phải là nội tuyến tự động, đó là nội tuyến mà các nhà phát triển nhân đã chỉ định thủ công. Có nhiều người đã biên dịch kernel bằng -O0 và báo cáo về cơ bản nó hoạt động, sau khi sửa một vài vấn đề nhỏ. (Một là ngay cả trong chủ đề bạn liên kết đến). Hầu hết, đó là các nhà phát triển nhân thấy không có lý do để biên dịch -O0và yêu cầu tối ưu hóa vì tác dụng phụ làm cho một số thủ thuật hoạt động và không ai kiểm tra -O0, vì vậy nó không được hỗ trợ.

Ví dụ, điều này biên dịch và liên kết với -O1hoặc cao hơn, nhưng không phải với -O0:

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}

Với tối ưu hóa, gcc có thể tìm ra rằng f()sẽ không bao giờ được gọi và bỏ qua nó. Không tối ưu hóa, gcc để lại cuộc gọi và trình liên kết không thành công vì không có định nghĩa f(). Các nhà phát triển kernel dựa vào hành vi tương tự để làm cho mã kernel dễ đọc / ghi hơn.


0

Từ Wiki Tối ưu hóa GCC Gentoo

Mục 2.3: Cờ -O

-O Tiếp theo là biến -O. Điều này kiểm soát mức độ tối ưu hóa tổng thể. Điều này làm cho quá trình biên dịch mã mất nhiều thời gian hơn và có thể chiếm nhiều bộ nhớ hơn, đặc biệt là khi bạn tăng mức độ tối ưu hóa.

Có bảy cài đặt -O: -O0, -O1, -O2, -O3, -Os, -Og và -Ofast. Bạn chỉ nên sử dụng một trong số chúng trong /etc/portage/make.conf.

Ngoại trừ -O0, mỗi cài đặt -O kích hoạt một số cờ bổ sung, do đó hãy nhớ đọc chương của hướng dẫn sử dụng GCC về các tùy chọn tối ưu hóa để tìm hiểu cờ nào được kích hoạt ở mỗi cấp -O, cũng như một số giải thích về những gì chúng làm

Hãy xem xét từng cấp độ tối ưu hóa:

-O0: Cấp độ này (đó là chữ "O" theo sau là số 0) tắt hoàn toàn tối ưu hóa và là mặc định nếu không có cấp -O được chỉ định trong CFLAGS hoặc CXXFLAGS. Điều này giúp giảm thời gian biên dịch và có thể cải thiện thông tin gỡ lỗi, nhưng một số ứng dụng sẽ không hoạt động đúng nếu không được tối ưu hóa. Tùy chọn này không được khuyến khích ngoại trừ mục đích gỡ lỗi.
-O1: Đây là mức tối ưu hóa cơ bản nhất. Trình biên dịch sẽ cố gắng tạo ra mã nhanh hơn, nhỏ hơn mà không mất nhiều thời gian biên dịch. Nó khá cơ bản, nhưng nó sẽ hoàn thành công việc mọi lúc.
-O2: Một bước tiến lên từ -O1. Đây là mức tối ưu hóa được khuyến nghị trừ khi bạn có nhu cầu đặc biệt. -O2 sẽ kích hoạt thêm một vài cờ ngoài các cờ được kích hoạt bởi -O1. Với -O2, trình biên dịch sẽ cố gắng tăng hiệu suất mã mà không ảnh hưởng đến kích thước và không mất quá nhiều thời gian biên dịch.
-O3: Đây là mức tối ưu hóa cao nhất có thể. Nó cho phép tối ưu hóa tốn kém về thời gian biên dịch và sử dụng bộ nhớ. Biên dịch với -O3 không phải là một cách đảm bảo để cải thiện hiệu suất và trên thực tế, trong nhiều trường hợp có thể làm chậm hệ thống do nhị phân lớn hơn và tăng mức sử dụng bộ nhớ. -O3 cũng được biết là phá vỡ một số gói. Do đó, không nên sử dụng -O3.
-Os: Tùy chọn này sẽ tối ưu hóa mã của bạn cho kích thước. Nó kích hoạt tất cả các tùy chọn -O2 không làm tăng kích thước của mã được tạo. Nó có thể hữu ích cho các máy có không gian lưu trữ đĩa cực kỳ hạn chế và / hoặc có CPU với kích thước bộ đệm nhỏ.
-Og: Trong GCC 4.8, một mức tối ưu hóa chung mới, -Og, đã được giới thiệu. Nó giải quyết nhu cầu biên dịch nhanh và trải nghiệm gỡ lỗi vượt trội đồng thời cung cấp mức hiệu năng thời gian chạy hợp lý. Kinh nghiệm tổng thể để phát triển phải tốt hơn mức tối ưu hóa mặc định -O0. Lưu ý rằng -Og không ngụ ý -g, nó chỉ đơn giản là vô hiệu hóa tối ưu hóa có thể can thiệp vào việc gỡ lỗi.
-Ofast: Mới trong GCC 4.7, bao gồm -O3 plus -ffast-math, -fno-bảo vệ-parens và -fstack-mảng. Tùy chọn này phá vỡ sự tuân thủ các tiêu chuẩn nghiêm ngặt và không được khuyến khích sử dụng. Như đã đề cập trước đây, -O2 là mức tối ưu hóa được đề xuất. Nếu quá trình biên dịch gói không thành công và bạn không sử dụng -O2, hãy thử xây dựng lại với tùy chọn đó. Là một tùy chọn dự phòng, hãy thử đặt CFLAGS và CXXFLAGS của bạn ở mức tối ưu hóa thấp hơn, chẳng hạn như -O1 hoặc thậm chí -O0 -g2 -ggdb (để báo cáo lỗi và kiểm tra các sự cố có thể xảy ra).

Bạn đã hỏi cụ thể về -O0, không tối ưu hóa. Đọc ở trên nói rằng O0 chỉ nên được sử dụng để gỡ lỗi. Nếu bạn đã từng sử dụng menuconfig, bạn sẽ nhận thấy rằng có một tùy chọn để bật hoặc tắt gỡ lỗi kernel. Khi được bật, tùy chọn này xuất ra thông tin gỡ lỗi theo cách tương tự như O0 sẽ cung cấp cho bạn thông tin. Ngoài ra, tôi nghĩ rằng bạn có thể thiếu điểm rằng toàn bộ hệ thống được xây dựng hoặc biên dịch với một và chỉ một cài đặt tối ưu hóa, nghĩa là bạn không thể biên dịch kernel tại O0 và phần còn lại của hệ thống tại O2


Về khả năng tương thích ngược giữa các phiên bản GCC, GCC sẽ luôn tương thích giữa các phiên bản vì cho phép Cờ -O trong một phiên bản giống với cài đặt -O trong phiên bản mới. Xem ghi chú ở trên liên quan đến GCC4.7 và tùy chọn -Ofast, trong đó tùy chọn chỉ khả dụng trong 4.7 trở đi, nhưng -O2 trong 4.7 = -O2 ở mọi phiên bản


2
Bạn có chắc chắn toàn bộ hệ thống phải được biên dịch với cùng mức tối ưu hóa không? -Os nhân hoạt động tốt với không gian người dùng -O2 theo kinh nghiệm của tôi và -Os tắt rất nhiều tối ưu hóa được kích hoạt trong -O2.
DanL4096

Ngoài ra, gỡ lỗi sang một bên, không phải là -O0 hành vi chính tắc của trình biên dịch, như được chỉ định bởi tiêu chuẩn ngôn ngữ? tức là tối ưu hóa có thể "sửa" mã không đúng về mặt ngữ nghĩa; biên dịch ở đâu mà không tối ưu hóa sẽ không?
DanL4096

@ DanL4096 Trong Hệ thống Gentoo, ít nhất, điều này không được khuyên ở cấp độ mỗi gói để tránh tình trạng mà OP đang yêu cầu. Trên hệ thống chỉ nhị phân như Debian, mức -O được xác định bởi các nhà bảo trì hệ điều hành và không thể thay đổi AFAIK. -O0 là đường cơ sở nhưng -O2 là cài đặt được đề xuất, vì vì các quốc gia Wiki không phải tất cả các chương trình sẽ biên dịch với -O0
eyoung100
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.