Những kỹ thuật nào có thể được sử dụng để tăng tốc thời gian biên dịch C ++?


249

Những kỹ thuật nào có thể được sử dụng để tăng tốc thời gian biên dịch C ++?

Câu hỏi này xuất hiện trong một số bình luận cho phong cách lập trình C ++ của Stack Overflow , và tôi rất muốn nghe những ý tưởng nào có.

Tôi đã thấy một câu hỏi liên quan, Tại sao quá trình biên dịch C ++ lại mất nhiều thời gian như vậy? , nhưng điều đó không cung cấp nhiều giải pháp.


1
Bạn có thể cho chúng tôi một số bối cảnh? Hay bạn đang tìm kiếm câu trả lời rất chung chung?
Nhiệt kế

1
Rất giống với câu hỏi này: stackoverflow.com/questions/364240/ trên
Adam Rosenfield

Câu trả lời chung chung. Tôi đã có một cơ sở mã thực sự lớn được viết bởi nhiều người. Ý tưởng về cách tấn công đó sẽ tốt. Ngoài ra, các đề xuất để giữ cho các biên dịch nhanh chóng cho mã mới được viết sẽ rất thú vị.
Scott Langham

Lưu ý rằng thường thì một phần có liên quan của thời gian xây dựng không được trình biên dịch sử dụng mà bởi các tập lệnh xây dựng
thi gg

1
Tôi lướt qua trang này và không thấy bất kỳ đề cập nào về các phép đo. Tôi đã viết một tập lệnh shell nhỏ có thêm dấu thời gian cho mỗi dòng đầu vào mà nó nhận được, vì vậy tôi chỉ có thể đặt đường dẫn trong lệnh gọi 'make'. Điều này cho phép tôi xem mục tiêu nào đắt nhất, tổng thời gian biên dịch hoặc liên kết, v.v. chỉ bằng cách so sánh dấu thời gian. Nếu bạn thử phương pháp này, hãy nhớ rằng dấu thời gian sẽ không chính xác cho các bản dựng song song.
John P

Câu trả lời:


257

Kỹ thuật ngôn ngữ

Thành ngữ Pimpl

Hãy xem thành ngữ Pimpl ở đâyở đây , còn được gọi là một con trỏ mờ hoặc các lớp xử lý. Không chỉ tăng tốc độ biên dịch, nó còn tăng độ an toàn ngoại lệ khi kết hợp với chức năng hoán đổi không ném . Thành ngữ Pimpl cho phép bạn giảm sự phụ thuộc giữa các tiêu đề và giảm số lượng biên dịch lại cần phải thực hiện.

Tuyên bố chuyển tiếp

Bất cứ nơi nào có thể, sử dụng khai báo chuyển tiếp . Nếu trình biên dịch chỉ cần biết đó SomeIdentifierlà một cấu trúc hoặc một con trỏ hoặc bất cứ điều gì, không bao gồm toàn bộ định nghĩa, buộc trình biên dịch phải thực hiện nhiều công việc hơn mức cần thiết. Điều này có thể có hiệu ứng xếp tầng, làm cho cách này chậm hơn mức cần thiết.

Các luồng I / O đặc biệt được biết đến với việc làm chậm các bản dựng. Nếu bạn cần chúng trong tệp tiêu đề, hãy thử #including <iosfwd>thay vì <iostream>và #include <iostream>tiêu đề trong tệp thực hiện. Các <iosfwd>tiêu đề giữ chỉ tờ khai phía trước. Thật không may, các tiêu đề tiêu chuẩn khác không có tiêu đề khai báo tương ứng.

Thích tham chiếu qua tham chiếu đến giá trị truyền qua trong chữ ký hàm. Điều này sẽ loại bỏ nhu cầu #inc loại trừ các định nghĩa loại tương ứng trong tệp tiêu đề và bạn sẽ chỉ cần khai báo loại. Tất nhiên, thích tham chiếu const đến các tham chiếu không const để tránh các lỗi tối nghĩa, nhưng đây là một vấn đề cho một câu hỏi khác.

Điều kiện bảo vệ

Sử dụng các điều kiện bảo vệ để giữ các tệp tiêu đề không được bao gồm nhiều lần trong một đơn vị dịch.

#pragma once
#ifndef filename_h
#define filename_h

// Header declarations / definitions

#endif

Bằng cách sử dụng cả pragma và ifndef, bạn sẽ có được tính di động của giải pháp macro đơn giản, cũng như tối ưu hóa tốc độ biên dịch mà một số trình biên dịch có thể thực hiện khi có lệnh pragma once.

Giảm sự phụ thuộc lẫn nhau

Nói chung, càng nhiều mô-đun và ít phụ thuộc vào thiết kế mã của bạn, bạn sẽ càng ít phải biên dịch lại mọi thứ. Cuối cùng, bạn cũng có thể giảm số lượng công việc mà trình biên dịch phải thực hiện trên bất kỳ khối riêng lẻ nào cùng một lúc, nhờ thực tế là nó có ít theo dõi hơn.

Tùy chọn trình biên dịch

Tiêu đề được biên dịch sẵn

Chúng được sử dụng để biên dịch một phần chung của các tiêu đề được bao gồm một lần cho nhiều đơn vị dịch thuật. Trình biên dịch biên dịch nó một lần và lưu trạng thái bên trong của nó. Trạng thái đó sau đó có thể được tải nhanh chóng để bắt đầu biên dịch một tệp khác với cùng một bộ tiêu đề.

Hãy cẩn thận rằng bạn chỉ bao gồm các công cụ hiếm khi thay đổi trong các tiêu đề được biên dịch trước, hoặc cuối cùng bạn có thể thực hiện việc xây dựng lại đầy đủ thường xuyên hơn mức cần thiết. Đây là một nơi tốt cho các tiêu đề STL và thư viện khác bao gồm các tệp.

ccache là một tiện ích khác tận dụng các kỹ thuật lưu trữ để tăng tốc mọi thứ.

Sử dụng song song

Nhiều trình biên dịch / IDE hỗ trợ sử dụng nhiều lõi / CPU để thực hiện biên dịch đồng thời. Trong GNU Make (thường được sử dụng với GCC), sử dụng -j [N]tùy chọn. Trong Visual Studio, có một tùy chọn theo sở thích để cho phép nó xây dựng song song nhiều dự án. Bạn cũng có thể sử dụng /MPtùy chọn cho paralellism cấp độ tập tin, thay vì chỉ paralellism cấp dự án.

Các tiện ích song song khác:

Sử dụng mức tối ưu hóa thấp hơn

Trình biên dịch càng cố gắng tối ưu hóa, nó càng khó hoạt động.

Thư viện dùng chung

Di chuyển mã ít được sửa đổi của bạn vào thư viện có thể giảm thời gian biên dịch. Bằng cách sử dụng các thư viện dùng chung ( .sohoặc .dll), bạn cũng có thể giảm thời gian liên kết.

Nhận một máy tính nhanh hơn

Nhiều RAM hơn, ổ cứng nhanh hơn (bao gồm cả SSD) và nhiều CPU / lõi hơn sẽ tạo ra sự khác biệt về tốc độ biên dịch.


11
Tiêu đề được biên dịch trước không hoàn hảo. Một tác dụng phụ của việc sử dụng chúng là bạn nhận được nhiều tệp hơn mức cần thiết (bởi vì mọi đơn vị biên dịch đều sử dụng cùng một tiêu đề được biên dịch trước), điều này có thể buộc biên dịch lại đầy đủ thường xuyên hơn mức cần thiết. Chỉ là một thứ để ghi nhớ trong đầu.
jalf

8
Trong các trình biên dịch hiện đại, #ifndef chỉ nhanh như #pragma một lần (miễn là bộ bảo vệ bao gồm ở đầu tệp). Vì vậy, không có lợi ích cho #pragma một lần về biên soạn tốc độ
jalf

7
Ngay cả khi bạn chỉ có VS 2005, không phải 2008, bạn có thể thêm / MP switch trong các tùy chọn biên dịch để cho phép xây dựng song song ở cấp .cpp.
macbirdie

6
SSD rất đắt khi câu trả lời này được viết, nhưng ngày nay chúng là lựa chọn tốt nhất khi biên dịch C ++. Bạn truy cập rất nhiều tệp nhỏ khi biên dịch;. Điều đó đòi hỏi rất nhiều IOPS, mà SSD cung cấp.
MSalters

14
Thích tham chiếu qua tham chiếu đến giá trị truyền qua trong chữ ký hàm. Điều này sẽ loại bỏ sự cần thiết phải loại bỏ các định nghĩa loại tương ứng trong tệp tiêu đề Điều này sai , bạn không cần phải có loại đầy đủ để khai báo một hàm đi qua giá trị, bạn chỉ cần loại đầy đủ để thực hiện hoặc sử dụng chức năng đó , nhưng trong hầu hết các trường hợp (trừ khi bạn chỉ chuyển tiếp cuộc gọi), bạn sẽ vẫn cần định nghĩa đó.
David Rodríguez - dribeas

43

Tôi làm việc trong dự án STAPL, một thư viện C ++ được tạo khuôn mẫu. Thỉnh thoảng, chúng tôi phải xem lại tất cả các kỹ thuật để giảm thời gian biên dịch. Ở đây, tôi đã tóm tắt các kỹ thuật chúng tôi sử dụng. Một số kỹ thuật này đã được liệt kê ở trên:

Tìm các phần tốn thời gian nhất

Mặc dù không có mối tương quan đã được chứng minh giữa độ dài ký hiệu và thời gian biên dịch, chúng tôi đã quan sát thấy rằng kích thước ký hiệu trung bình nhỏ hơn có thể cải thiện thời gian biên dịch trên tất cả các trình biên dịch. Vì vậy, mục tiêu đầu tiên của bạn là tìm các ký hiệu lớn nhất trong mã của bạn.

Phương pháp 1 - Sắp xếp các ký hiệu dựa trên kích thước

Bạn có thể sử dụng nmlệnh để liệt kê các ký hiệu dựa trên kích thước của chúng:

nm --print-size --size-sort --radix=d YOUR_BINARY

Trong lệnh này, --radix=dcho phép bạn xem kích thước bằng số thập phân (mặc định là hex). Bây giờ bằng cách nhìn vào biểu tượng lớn nhất, xác định xem bạn có thể phá vỡ lớp tương ứng và cố gắng thiết kế lại nó bằng cách bao gồm các phần không được tạo mẫu trong một lớp cơ sở hoặc bằng cách chia lớp thành nhiều lớp.

Phương pháp 2 - Sắp xếp các ký hiệu dựa trên chiều dài

Bạn có thể chạy nmlệnh thông thường và chuyển nó sang tập lệnh yêu thích của bạn ( AWK , Python , v.v.) để sắp xếp các biểu tượng dựa trên chiều dài của chúng . Dựa trên kinh nghiệm của chúng tôi, phương pháp này xác định rắc rối lớn nhất khiến ứng viên trở nên tốt hơn phương pháp 1.

Phương pháp 3 - Sử dụng Templight

" Templight là một công cụ dựa trên Clang để xác định mức tiêu thụ thời gian và bộ nhớ của các lần khởi tạo mẫu và để thực hiện các phiên gỡ lỗi tương tác để đạt được sự hướng nội vào quy trình khởi tạo mẫu".

Bạn có thể cài đặt Templight bằng cách kiểm tra LLVM và Clang ( hướng dẫn ) và áp dụng bản vá Templight trên nó. Cài đặt mặc định cho LLVM và Clang là về gỡ lỗi và xác nhận và những điều này có thể ảnh hưởng đáng kể đến thời gian biên dịch của bạn. Có vẻ như Templight cần cả hai, vì vậy bạn phải sử dụng các cài đặt mặc định. Quá trình cài đặt LLVM và Clang sẽ mất khoảng một giờ hoặc lâu hơn.

Sau khi áp dụng bản vá, bạn có thể sử dụng templight++nằm trong thư mục bản dựng mà bạn đã chỉ định khi cài đặt để biên dịch mã của mình.

Hãy chắc chắn rằng đó templight++là trong ĐƯỜNG của bạn. Bây giờ để biên dịch thêm các công tắc sau vào CXXFLAGStrong Makefile của bạn hoặc vào các tùy chọn dòng lệnh của bạn:

CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

Hoặc là

templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system

Sau khi biên dịch xong, bạn sẽ có một .trace.memory.pbf và .trace.pbf được tạo trong cùng một thư mục. Để hình dung các dấu vết này, bạn có thể sử dụng Công cụ Templight có thể chuyển đổi các dấu vết này sang các định dạng khác. Thực hiện theo các hướng dẫn này để cài đặt templight-convert. Chúng tôi thường sử dụng đầu ra callgrind. Bạn cũng có thể sử dụng đầu ra GraphViz nếu dự án của bạn nhỏ:

$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace

$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot

Tệp callgrind được tạo có thể được mở bằng cách sử dụng kcachegrind, trong đó bạn có thể theo dõi việc khởi tạo tốn thời gian / bộ nhớ nhất.

Giảm số lần khởi tạo mẫu

Mặc dù không có giải pháp chính xác để giảm số lần tạo mẫu, nhưng có một vài hướng dẫn có thể giúp:

Lớp tái cấu trúc với nhiều hơn một đối số mẫu

Ví dụ, nếu bạn có một lớp,

template <typename T, typename U>
struct foo { };

và cả hai TUcó thể có 10 tùy chọn khác nhau, bạn đã tăng khả năng khởi tạo mẫu có thể của lớp này lên 100. Một cách để giải quyết điều này là trừu tượng hóa phần chung của mã thành một lớp khác. Phương pháp khác là sử dụng đảo ngược kế thừa (đảo ngược hệ thống phân cấp lớp), nhưng đảm bảo rằng các mục tiêu thiết kế của bạn không bị xâm phạm trước khi sử dụng kỹ thuật này.

Tái cấu trúc mã không templated cho các đơn vị dịch thuật riêng lẻ

Sử dụng kỹ thuật này, bạn có thể biên dịch phần chung một lần và liên kết nó với các TU (đơn vị dịch thuật) khác của bạn sau này.

Sử dụng tức thời mẫu bên ngoài (kể từ C ++ 11)

Nếu bạn biết tất cả các tức thời có thể có của một lớp, bạn có thể sử dụng kỹ thuật này để biên dịch tất cả các trường hợp trong một đơn vị dịch thuật khác.

Ví dụ: trong:

enum class PossibleChoices = {Option1, Option2, Option3}

template <PossibleChoices pc>
struct foo { };

Chúng tôi biết rằng lớp này có thể có ba khả năng tức thời:

template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;

Đặt phần trên vào một đơn vị dịch và sử dụng từ khóa extern trong tệp tiêu đề của bạn, bên dưới định nghĩa lớp:

extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;

Kỹ thuật này có thể giúp bạn tiết kiệm thời gian nếu bạn biên dịch các bài kiểm tra khác nhau với một tập hợp tức thời chung.

LƯU Ý: MPICH2 bỏ qua việc khởi tạo rõ ràng tại thời điểm này và luôn biên dịch các lớp khởi tạo trong tất cả các đơn vị biên dịch.

Sử dụng bản dựng thống nhất

Toàn bộ ý tưởng đằng sau các bản dựng unity là bao gồm tất cả các tệp .cc mà bạn sử dụng trong một tệp và chỉ biên dịch tệp đó một lần. Sử dụng phương pháp này, bạn có thể tránh khôi phục các phần chung của các tệp khác nhau và nếu dự án của bạn bao gồm nhiều tệp chung, có thể bạn cũng sẽ lưu vào các truy cập đĩa.

Như một ví dụ, chúng ta hãy giả sử bạn có ba tác phẩm foo1.cc, foo2.cc, foo3.ccvà tất cả họ bao gồm tupletừ STL . Bạn có thể tạo một foo-all.cchình giống như:

#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"

Bạn chỉ biên dịch tệp này một lần và có khả năng giảm các cảnh báo phổ biến trong số ba tệp. Thật khó để dự đoán chung liệu sự cải thiện có thể là đáng kể hay không. Nhưng một thực tế hiển nhiên là bạn sẽ mất tính song song trong các bản dựng của mình (bạn không còn có thể biên dịch ba tệp cùng một lúc).

Hơn nữa, nếu bất kỳ tệp nào trong số này xảy ra chiếm nhiều bộ nhớ, bạn thực sự có thể hết bộ nhớ trước khi quá trình biên dịch kết thúc. Trên một số trình biên dịch, chẳng hạn như GCC , điều này có thể ICE (Lỗi trình biên dịch nội bộ) trình biên dịch của bạn thiếu bộ nhớ. Vì vậy, không sử dụng kỹ thuật này trừ khi bạn biết tất cả các ưu và nhược điểm.

Tiêu đề được biên dịch sẵn

Các tiêu đề được biên dịch sẵn (PCH) có thể giúp bạn tiết kiệm rất nhiều thời gian trong quá trình biên dịch bằng cách biên dịch các tệp tiêu đề của bạn thành một biểu diễn trung gian có thể nhận ra bởi trình biên dịch. Để tạo tệp tiêu đề được biên dịch trước, bạn chỉ cần biên dịch tệp tiêu đề bằng lệnh biên dịch thông thường. Ví dụ: trên GCC:

$ g++ YOUR_HEADER.hpp

Điều này sẽ tạo ra một YOUR_HEADER.hpp.gch file( .gchlà phần mở rộng cho các tệp PCH trong GCC) trong cùng một thư mục. Điều này có nghĩa là nếu bạn đưa YOUR_HEADER.hppvào một số tệp khác, trình biên dịch sẽ sử dụng YOUR_HEADER.hpp.gchthay vì YOUR_HEADER.hpptrong cùng một thư mục trước đó.

Có hai vấn đề với kỹ thuật này:

  1. Bạn phải đảm bảo rằng các tệp tiêu đề đang được biên dịch trước là ổn định và sẽ không thay đổi ( bạn luôn có thể thay đổi tệp thực hiện của mình )
  2. Bạn chỉ có thể bao gồm một PCH cho mỗi đơn vị biên dịch (trên hầu hết các trình biên dịch). Điều này có nghĩa là nếu bạn có nhiều hơn một tệp tiêu đề được biên dịch trước, bạn phải đưa chúng vào một tệp (ví dụ all-my-headers.hpp:). Nhưng điều đó có nghĩa là bạn phải bao gồm tệp mới ở tất cả các nơi. May mắn thay, GCC có một giải pháp cho vấn đề này. Sử dụng -includevà cung cấp cho nó các tập tin tiêu đề mới. Bạn có thể dấu phẩy tách các tệp khác nhau bằng cách sử dụng kỹ thuật này.

Ví dụ:

g++ foo.cc -include all-my-headers.hpp

Sử dụng không gian tên không tên hoặc ẩn danh

Không gian tên không tên (còn gọi là không gian tên ẩn danh) có thể giảm đáng kể kích thước nhị phân được tạo. Các không gian tên không tên sử dụng liên kết bên trong, có nghĩa là các ký hiệu được tạo trong các không gian tên đó sẽ không hiển thị với các TU khác (đơn vị dịch hoặc biên dịch). Trình biên dịch thường tạo tên duy nhất cho các không gian tên không tên. Điều này có nghĩa là nếu bạn có tệp foo.hpp:

namespace {

template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;

Và bạn tình cờ bao gồm tệp này trong hai TU (hai tệp .cc và biên dịch chúng riêng biệt). Hai trường hợp mẫu foo sẽ không giống nhau. Điều này vi phạm Quy tắc Một Định nghĩa (ODR). Vì lý do tương tự, việc sử dụng các không gian tên không tên được khuyến khích trong các tệp tiêu đề. Vui lòng sử dụng chúng trong các .cctệp của bạn để tránh các biểu tượng hiển thị trong các tệp nhị phân của bạn. Trong một số trường hợp, việc thay đổi tất cả các chi tiết nội bộ cho một .cctệp cho thấy giảm 10% kích thước nhị phân được tạo.

Thay đổi tùy chọn hiển thị

Trong các trình biên dịch mới hơn, bạn có thể chọn các biểu tượng của mình để hiển thị hoặc ẩn trong các Đối tượng được chia sẻ động (DSO). Lý tưởng nhất là thay đổi khả năng hiển thị có thể cải thiện hiệu suất của trình biên dịch, tối ưu hóa thời gian liên kết (LTO) và kích thước nhị phân được tạo. Nếu bạn nhìn vào các tệp tiêu đề STL trong GCC, bạn có thể thấy rằng nó được sử dụng rộng rãi. Để cho phép lựa chọn khả năng hiển thị, bạn cần thay đổi mã theo từng hàm, mỗi lớp, mỗi biến và quan trọng hơn là mỗi trình biên dịch.

Với sự trợ giúp của khả năng hiển thị, bạn có thể ẩn các biểu tượng mà bạn coi chúng là riêng tư khỏi các đối tượng chia sẻ được tạo. Trên GCC, bạn có thể kiểm soát mức độ hiển thị của các biểu tượng bằng cách chuyển mặc định hoặc ẩn cho -visibilitytùy chọn trình biên dịch của bạn. Điều này trong một số ý nghĩa tương tự như không gian tên không tên nhưng theo một cách phức tạp hơn và xâm nhập.

Nếu bạn muốn chỉ định mức độ hiển thị cho mỗi trường hợp, bạn phải thêm các thuộc tính sau vào các hàm, biến và lớp của bạn:

__attribute__((visibility("default"))) void  foo1() { }
__attribute__((visibility("hidden")))  void  foo2() { }
__attribute__((visibility("hidden")))  class foo3   { };
void foo4() { }

Khả năng hiển thị mặc định trong GCC là mặc định (công khai), nghĩa là nếu bạn biên dịch ở trên dưới dạng -sharedphương thức thư viện dùng chung ( ) foo2và lớp foo3sẽ không hiển thị trong các TU khác ( foo1foo4sẽ hiển thị). Nếu bạn biên dịch với -visibility=hiddensau đó foo1sẽ chỉ được hiển thị. Thậm chí foo4sẽ được ẩn.

Bạn có thể đọc thêm về khả năng hiển thị trên GCC wiki .


33

Tôi muốn giới thiệu những bài viết này từ "Trò chơi từ bên trong, Lập trình và thiết kế trò chơi Indie":

Cấp, chúng khá cũ - bạn sẽ phải kiểm tra lại mọi thứ với các phiên bản mới nhất (hoặc phiên bản có sẵn cho bạn), để có kết quả thực tế. Dù bằng cách nào, nó là một nguồn tốt cho các ý tưởng.


17

Một kỹ thuật hoạt động khá tốt đối với tôi trong quá khứ: không biên dịch nhiều tệp nguồn C ++ một cách độc lập mà thay vào đó tạo một tệp C ++ bao gồm tất cả các tệp khác, như sau:

// myproject_all.cpp
// Automatically generated file - don't edit this by hand!
#include "main.cpp"
#include "mainwindow.cpp"
#include "filterdialog.cpp"
#include "database.cpp"

Tất nhiên, điều này có nghĩa là bạn phải biên dịch lại tất cả các mã nguồn được bao gồm trong trường hợp bất kỳ nguồn nào thay đổi, do đó cây phụ thuộc trở nên tồi tệ hơn. Tuy nhiên, biên dịch nhiều tệp nguồn dưới dạng một đơn vị dịch nhanh hơn (ít nhất là trong các thử nghiệm của tôi với MSVC và GCC) và tạo ra các nhị phân nhỏ hơn. Tôi cũng nghi ngờ rằng trình biên dịch được cung cấp nhiều tiềm năng hơn cho việc tối ưu hóa (vì nó có thể thấy nhiều mã hơn cùng một lúc).

Kỹ thuật này phá vỡ trong các trường hợp khác nhau; chẳng hạn, trình biên dịch sẽ bảo lãnh trong trường hợp hai hoặc nhiều tệp nguồn khai báo một hàm toàn cục có cùng tên. Tôi không thể tìm thấy kỹ thuật này được mô tả trong bất kỳ câu trả lời nào khác, đó là lý do tại sao tôi đề cập đến nó ở đây.

Để biết giá trị của nó, Dự án KDE đã sử dụng kỹ thuật tương tự chính xác này từ năm 1999 để xây dựng các nhị phân được tối ưu hóa (có thể để phát hành). Việc chuyển sang tập lệnh cấu hình xây dựng đã được gọi --enable-final. Vì lợi ích khảo cổ học, tôi đã đăng bài đăng thông báo tính năng này: http://lists.kde.org/?l=kde-devel&m=92722836009368&w=2


2
Tôi không chắc liệu nó có thực sự giống như vậy không, nhưng tôi đoán bật "Tối ưu hóa toàn bộ chương trình" trong VC ++ ( msdn.microsoft.com/en-us/l Library / 0zza0de8% 28VS.71% 29.aspx ) nên có hiệu ứng tương tự về hiệu suất thời gian chạy hơn những gì bạn đang đề xuất. Biên dịch thời gian, tuy nhiên, chắc chắn có thể tốt hơn trong cách tiếp cận của bạn!
Philipp

1
@Frerich: Bạn đang mô tả các bản dựng Unity được đề cập trong câu trả lời của OJ. Tôi cũng đã thấy chúng được gọi là bản dựng số lượng lớn và bản dựng chính.
idbrii

Vậy làm thế nào để một UB so sánh với WPO / LTCG?
paulm

Điều này có khả năng chỉ hữu ích cho các phần tổng hợp một lần, không phải trong quá trình phát triển khi bạn xoay vòng giữa chỉnh sửa, xây dựng và thử nghiệm. Trong thế giới hiện đại, bốn lõi là chuẩn mực, có thể vài năm sau số lượng lõi là nhiều hơn đáng kể. Nếu trình biên dịch và trình liên kết không thể sử dụng nhiều luồng, thì danh sách các tệp có thể được chia thành các <core-count> + Ndanh sách con được biên dịch song song với Nmột số nguyên phù hợp (tùy thuộc vào bộ nhớ hệ thống và cách sử dụng máy khác).
FooF

15

Có toàn bộ cuốn sách về chủ đề này, có tựa đề Thiết kế phần mềm C ++ quy mô lớn (được viết bởi John Lakos).

Các cuốn sách trước ngày mẫu, do đó, nội dung của cuốn sách đó thêm "sử dụng các mẫu cũng có thể làm cho trình biên dịch chậm hơn".


Cuốn sách thường được đề cập trong các loại chủ đề này nhưng đối với tôi nó rất ít thông tin. Về cơ bản, nó tuyên bố sử dụng các khai báo chuyển tiếp càng nhiều càng tốt và tách rời các phụ thuộc. Đó là một chút nói rõ ràng bên cạnh việc sử dụng thành ngữ pimpl có nhược điểm thời gian chạy.
gast128

@ gast128 Tôi nghĩ rằng quan điểm của nó là sử dụng các thành ngữ mã hóa cho phép biên dịch lại gia tăng, tức là, nếu bạn thay đổi một chút nguồn ở đâu đó, bạn sẽ không phải biên dịch lại mọi thứ.
ChrisW

15

Tôi sẽ chỉ liên kết với câu trả lời khác của tôi: Làm thế nào để BẠN giảm thời gian biên dịch và liên kết thời gian cho các dự án Visual C ++ (C ++ bản địa)? . Một điểm khác tôi muốn thêm, nhưng nguyên nhân thường gây ra vấn đề là sử dụng các tiêu đề được biên dịch trước. Nhưng xin vui lòng, chỉ sử dụng chúng cho các phần mà hầu như không thay đổi (như tiêu đề bộ công cụ GUI). Nếu không, họ sẽ tốn của bạn nhiều thời gian hơn cuối cùng họ tiết kiệm cho bạn.

Một tùy chọn khác là, khi bạn làm việc với GNU make, để bật -j<N>tùy chọn:

  -j [N], --jobs[=N]          Allow N jobs at once; infinite jobs with no arg.

Tôi thường có nó 3vì tôi đã có lõi kép ở đây. Sau đó, nó sẽ chạy các trình biên dịch song song cho các đơn vị dịch thuật khác nhau, miễn là không có sự phụ thuộc giữa chúng. Liên kết không thể được thực hiện song song, vì chỉ có một quá trình liên kết liên kết với nhau tất cả các tệp đối tượng.

Nhưng chính trình liên kết có thể được xâu chuỗi, và đây là những gì GNU gold trình liên kết ELF làm. Mã C ++ được tối ưu hóa được cho là liên kết các tệp đối tượng ELF nhanh hơn so với mã cũ ld(và thực sự được đưa vào binutils ).


Ok, vâng. Xin lỗi, câu hỏi đó không xuất hiện khi tôi tìm kiếm.
Scott Langham

bạn đã không phải xin lỗi. đó là cho Visual C ++. câu hỏi của bạn dường như dành cho bất kỳ trình biên dịch. vậy là tốt rồi :)
Johannes Schaub - litb

12

Đây là một số:

  • Sử dụng tất cả các lõi của bộ xử lý bằng cách bắt đầu một công việc đa biên dịch (make -j2 là một ví dụ tốt).
  • Tắt hoặc giảm tối ưu hóa (ví dụ, GCC là nhanh hơn nhiều với -O1hơn -O2hoặc-O3 ).
  • Sử dụng các tiêu đề được biên dịch trước .

12
FYI, tôi thấy thường bắt đầu nhiều quá trình hơn lõi. Ví dụ, trên hệ thống lõi tứ, tôi thường sử dụng -j8, không phải -j4. Lý do cho điều này là khi một tiến trình bị chặn trên I / O, tiến trình kia có thể được biên dịch.
Ông Fooz

@MrFooz: Tôi đã thử nghiệm điều này vài năm trước bằng cách biên dịch nhân Linux (từ bộ lưu trữ RAM) trên i7-2700k (4 lõi, 8 luồng, tôi đặt hệ số nhân không đổi). Tôi quên kết quả tốt nhất chính xác, nhưng -j12xung quanh -j18nhanh hơn đáng kể so với -j8, như bạn đề xuất. Tôi đang tự hỏi có bao nhiêu lõi bạn có thể có trước khi băng thông bộ nhớ trở thành yếu tố giới hạn ...
Mark K Cowan

@MarkKCowan nó phụ thuộc vào rất nhiều yếu tố. Các máy tính khác nhau có băng thông bộ nhớ rất khác nhau. Với bộ xử lý cao cấp ngày nay, phải mất nhiều lõi để bão hòa bus bộ nhớ. Ngoài ra, có sự cân bằng giữa I / O và CPU. Một số mã rất dễ biên dịch, mã khác có thể chậm (ví dụ với rất nhiều mẫu). Quy tắc hiện tại của tôi là -jvới số lượng nhân gấp đôi.
Ông Fooz

11

Khi bạn đã áp dụng tất cả các thủ thuật mã ở trên (khai báo chuyển tiếp, giảm mức độ bao gồm tiêu đề xuống mức tối thiểu trong các tiêu đề công khai, đẩy hầu hết các chi tiết bên trong tệp triển khai với Pimpl ...) và không có gì khác có thể đạt được bằng ngôn ngữ, hãy xem xét hệ thống xây dựng của bạn . Nếu bạn sử dụng Linux, hãy cân nhắc sử dụng distcc (trình biên dịch phân tán) và ccache (trình biên dịch bộ đệm).

Cái đầu tiên, distcc, thực thi bước tiền xử lý cục bộ và sau đó gửi đầu ra đến trình biên dịch có sẵn đầu tiên trong mạng. Nó yêu cầu cùng một phiên bản trình biên dịch và thư viện trong tất cả các nút được cấu hình trong mạng.

Cái sau, ccache, là một bộ đệm của trình biên dịch. Nó một lần nữa thực thi bộ tiền xử lý và sau đó kiểm tra với cơ sở dữ liệu nội bộ (được giữ trong một thư mục cục bộ) nếu tệp tiền xử lý đó đã được biên dịch với cùng tham số trình biên dịch. Nếu có, nó chỉ bật lên nhị phân và đầu ra từ lần chạy đầu tiên của trình biên dịch.

Cả hai có thể được sử dụng cùng một lúc, do đó, nếu ccache không có bản sao cục bộ, nó có thể gửi nó qua mạng đến một nút khác bằng distcc, nếu không nó chỉ có thể tiêm giải pháp mà không cần xử lý thêm.


2
Tôi không nghĩ rằng distcc yêu cầu các phiên bản thư viện giống nhau trên tất cả các nút được cấu hình. distcc chỉ thực hiện việc biên dịch từ xa, không liên kết. Nó cũng gửi mã được xử lý trước qua dây, vì vậy các tiêu đề có sẵn trên hệ thống từ xa không thành vấn đề.
Frerich Raabe

9

Khi tôi ra khỏi trường đại học, mã C ++ đáng sản xuất thực sự đầu tiên tôi thấy có các chỉ thị #ifndef ... #endif phức tạp này ở giữa chúng, nơi các tiêu đề được xác định. Tôi hỏi anh chàng đang viết mã về những điều bao trùm này một cách rất ngây thơ và được giới thiệu với thế giới lập trình quy mô lớn.

Quay trở lại vấn đề, sử dụng các chỉ thị để ngăn chặn các định nghĩa tiêu đề trùng lặp là điều đầu tiên tôi học được khi nói đến việc giảm thời gian biên dịch.


1
Cũ nhưng quý. đôi khi sự rõ ràng bị lãng quên.
alcor

1
'bao gồm lính canh'
gast128

8

Thêm RAM.

Ai đó đã nói về ổ đĩa RAM trong một câu trả lời khác. Tôi đã làm điều này với 80286Turbo C ++ (hiển thị tuổi) và kết quả thật tuyệt vời. Như mất dữ liệu khi máy gặp sự cố.


trong DOS, bạn không thể có nhiều bộ nhớ
phuclv

6

Sử dụng khai báo chuyển tiếp, nơi bạn có thể. Nếu một khai báo lớp chỉ sử dụng một con trỏ hoặc tham chiếu đến một loại, bạn chỉ có thể chuyển tiếp khai báo nó và bao gồm tiêu đề cho loại trong tệp thực hiện.

Ví dụ:

// T.h
class Class2; // Forward declaration

class T {
public:
    void doSomething(Class2 &c2);
private:
    Class2 *m_Class2Ptr;
};

// T.cpp
#include "Class2.h"
void Class2::doSomething(Class2 &c2) {
    // Whatever you want here
}

Ít hơn bao gồm có nghĩa là ít công việc hơn cho bộ tiền xử lý nếu bạn làm đủ.


Điều này không chỉ quan trọng khi cùng một tiêu đề được bao gồm trong một số đơn vị dịch thuật? Nếu chỉ có một đơn vị dịch thuật (như thường thấy khi các mẫu được sử dụng) thì điều này dường như không có tác động.
Luôn luôn học tập

1
Nếu chỉ có một đơn vị dịch thuật, tại sao lại phải đặt nó trong một tiêu đề? Sẽ không có ý nghĩa hơn nếu chỉ đưa nội dung vào tệp nguồn? Không phải toàn bộ tiêu đề là nó có khả năng được bao gồm bởi nhiều hơn một tệp nguồn?
Evan Teran


5

Sử dụng

#pragma once

ở đầu các tệp tiêu đề, vì vậy nếu chúng được bao gồm nhiều lần trong một đơn vị dịch, văn bản của tiêu đề sẽ chỉ được bao gồm và phân tích cú pháp một lần.


2
Mặc dù được hỗ trợ rộng rãi, #pragma một lần là không chuẩn. Xem en.wikipedia.org/wiki/Pragma_once
ChrisInEdmont

7
Và những ngày này, thường xuyên bao gồm các vệ sĩ có tác dụng tương tự. Chừng nào họ đang ở phía trên cùng của tập tin, trình biên dịch là hoàn toàn có khả năng xử với họ như #pragma một lần
jalf

5

Chỉ để hoàn thiện: một bản dựng có thể chậm vì hệ thống xây dựng bị ngu ngốc cũng như vì trình biên dịch mất nhiều thời gian để thực hiện công việc của nó.

Đọc đệ quy làm cho có hại (PDF) để thảo luận về chủ đề này trong môi trường Unix.


4
  • Nâng cấp máy tính của bạn

    1. Nhận lõi tứ (hoặc hệ thống bốn lõi)
    2. Nhận rất nhiều RAM.
    3. Sử dụng ổ đĩa RAM để giảm đáng kể độ trễ I / O của tệp. (Có những công ty sản xuất ổ đĩa RAM IDE và SATA hoạt động giống như ổ cứng).
  • Sau đó, bạn có tất cả các đề xuất tiêu biểu khác của bạn

    1. Sử dụng các tiêu đề được biên dịch trước nếu có.
    2. Giảm số lượng khớp nối giữa các phần của dự án của bạn. Thay đổi một tệp tiêu đề thường không cần phải biên dịch lại toàn bộ dự án của bạn.

4

Tôi đã có một ý tưởng về việc sử dụng ổ đĩa RAM . Hóa ra, đối với các dự án của tôi, nó không tạo ra nhiều sự khác biệt. Nhưng sau đó họ vẫn còn nhỏ. Thử nó! Tôi muốn nghe về việc nó đã giúp được bao nhiêu.


Huh. Tại sao ai đó bỏ phiếu này? Tôi sẽ thử nó vào ngày mai.
Scott Langham

1
Tôi hy vọng downvote là bởi vì nó không bao giờ tạo ra sự khác biệt lớn. Nếu bạn có đủ RAM chưa sử dụng, hệ điều hành sẽ sử dụng nó một cách thông minh như một bộ đệm đĩa.
MSalters

1
@MSalters - và bao nhiêu sẽ là "đủ"? Tôi biết rằng đó là lý thuyết, nhưng đối với một số lý do sử dụng một RAMdrive không thực sự cung cấp một tăng đáng kể.
Bắt đầu

1
đủ để biên dịch dự án của bạn và vẫn lưu trữ các tệp đầu vào và tạm thời. Rõ ràng bên trong GB sẽ phụ thuộc trực tiếp vào quy mô dự án của bạn. Cần lưu ý rằng bộ nhớ cache của các hệ điều hành cũ (cụ thể là WinXP) khá lười biếng, khiến RAM không được sử dụng.
MSalters

Chắc chắn ổ đĩa ram sẽ nhanh hơn nếu các tệp đã có trong ram chứ không phải thực hiện một loạt IO chậm trước, sau đó chúng có trong ram không? (tăng lặp lại cho các tệp đã thay đổi - ghi chúng trở lại đĩa, v.v.).
paulm

3

Liên kết động (.so) có thể nhanh hơn nhiều so với liên kết tĩnh (.a). Đặc biệt là khi bạn có một ổ đĩa mạng chậm. Điều này là do bạn có tất cả các mã trong tệp .a cần được xử lý và viết ra. Ngoài ra, một tập tin thực thi lớn hơn nhiều cần phải được ghi ra đĩa.


liên kết động ngăn chặn nhiều loại tối ưu hóa thời gian liên kết, do đó, đầu ra có thể chậm hơn trong nhiều trường hợp
phuclv

3

Không phải về thời gian biên dịch, mà là về thời gian xây dựng:

  • Sử dụng ccache nếu bạn phải xây dựng lại các tệp tương tự khi bạn đang làm việc trên các tệp xây dựng của mình

  • Sử dụng ninja xây dựng thay vì thực hiện. Tôi hiện đang biên dịch một dự án với ~ 100 tệp nguồn và mọi thứ được lưu trữ bởi ccache. làm cần 5 phút, ninja dưới 1.

Bạn có thể tạo các tệp ninja của mình từ cmake với -GNinja.


3

Bạn đang dành thời gian ở đâu? Bạn có bị ràng buộc CPU không? Ký ức bị ràng buộc? Đĩa bị ràng buộc? Bạn có thể sử dụng nhiều lõi hơn? Thêm RAM? Bạn có cần RAID không? Bạn chỉ đơn giản muốn cải thiện hiệu quả của hệ thống hiện tại của bạn?

Theo gcc / g ++, bạn đã xem ccache chưa? Nó có thể hữu ích nếu bạn đang làm make clean; makenhiều.


2

Đĩa cứng nhanh hơn.

Trình biên dịch ghi nhiều tệp (và có thể rất lớn) vào đĩa. Làm việc với SSD thay vì ổ cứng thông thường và thời gian biên dịch thấp hơn nhiều.



2

Chia sẻ mạng sẽ làm chậm đáng kể quá trình xây dựng của bạn, vì độ trễ tìm kiếm cao. Đối với một cái gì đó như Boost, nó đã tạo ra một sự khác biệt lớn cho tôi, mặc dù ổ đĩa chia sẻ mạng của chúng tôi khá nhanh. Thời gian biên dịch chương trình Boost đồ chơi đã tăng từ khoảng 1 phút đến 1 giây khi tôi chuyển từ chia sẻ mạng sang SSD cục bộ.


2

Nếu bạn có bộ xử lý đa lõi, cả Visual Studio (2005 trở lên) cũng như GCC đều hỗ trợ biên dịch đa bộ xử lý. Đó là một cái gì đó để kích hoạt nếu bạn có phần cứng, chắc chắn.


2
@Fellman, Xem một số câu trả lời khác - sử dụng tùy chọn -j #.
strager

1

Mặc dù không phải là một "kỹ thuật", tôi không thể tìm ra cách các dự án Win32 với nhiều tệp nguồn được biên dịch nhanh hơn dự án trống "Hello World" của tôi. Vì vậy, tôi hy vọng điều này sẽ giúp một người như nó đã làm tôi.

Trong Visual Studio, một tùy chọn để tăng thời gian biên dịch là Liên kết tăng dần ( / TĂNG CƯỜNG ). Nó không tương thích với Tạo mã thời gian liên kết ( / LTCG ), vì vậy hãy nhớ tắt liên kết gia tăng khi thực hiện các bản dựng phát hành.


1
vô hiệu hóa Tạo mã thời gian liên kết không phải là một gợi ý hay vì nó vô hiệu hóa nhiều tối ưu hóa. Bạn chỉ cần bật /INCREMENTALchế độ gỡ lỗi
phuclv

1

Bắt đầu với Visual Studio 2017, bạn có khả năng có một số số liệu về trình biên dịch về những gì cần có thời gian.

Thêm các tham số đó vào C / C ++ -> Dòng lệnh (Tùy chọn bổ sung) trong cửa sổ thuộc tính dự án: /Bt+ /d2cgsummary /d1reportTime

Bạn có thể có nhiều thông tin hơn trong bài viết này .


0

Sử dụng liên kết động thay vì tĩnh giúp bạn biên dịch nhanh hơn có thể cảm nhận được.

Nếu bạn sử dụng t Cmake, kích hoạt thuộc tính:

set(BUILD_SHARED_LIBS ON)

Xây dựng Phát hành, sử dụng liên kết tĩnh có thể được tối ưu hóa nhiều hơn.

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.