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 nm
lệ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=d
cho 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 nm
lệ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 CXXFLAGS
trong 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 T
và U
có 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.cc
và tất cả họ bao gồm tuple
từ STL . Bạn có thể tạo một foo-all.cc
hì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
( .gch
là 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.hpp
vào một số tệp khác, trình biên dịch sẽ sử dụng YOUR_HEADER.hpp.gch
thay vì YOUR_HEADER.hpp
trong cùng một thư mục trước đó.
Có hai vấn đề với kỹ thuật này:
- 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 )
- 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 -include
và 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 .cc
tệ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 .cc
tệ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 -visibility
tù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 -shared
phương thức thư viện dùng chung ( ) foo2
và lớp foo3
sẽ không hiển thị trong các TU khác ( foo1
và foo4
sẽ hiển thị). Nếu bạn biên dịch với -visibility=hidden
sau đó foo1
sẽ chỉ được hiển thị. Thậm chí foo4
sẽ được ẩn.
Bạn có thể đọc thêm về khả năng hiển thị trên GCC wiki .