Thực hành mã hóa cho phép trình biên dịch / tối ưu hóa tạo chương trình nhanh hơn


116

Nhiều năm trước, các trình biên dịch C không đặc biệt thông minh. Như một cách giải quyết khác, K&R đã phát minh ra từ khóa register , để gợi ý cho trình biên dịch, rằng có lẽ nên giữ biến này trong một thanh ghi nội bộ. Họ cũng tạo ra toán tử cấp ba để giúp tạo ra mã tốt hơn.

Thời gian trôi qua, các trình biên dịch đã trưởng thành. Họ trở nên rất thông minh trong việc phân tích luồng của họ cho phép họ đưa ra quyết định tốt hơn về những giá trị cần giữ trong sổ đăng ký hơn bạn có thể làm. Từ khóa đăng ký trở nên không quan trọng.

FORTRAN có thể nhanh hơn C đối với một số loại hoạt động, do các vấn đề về bí danh . Về lý thuyết với việc mã hóa cẩn thận, người ta có thể vượt qua hạn chế này để cho phép trình tối ưu hóa tạo mã nhanh hơn.

Có những phương pháp mã hóa nào có thể cho phép trình biên dịch / tối ưu hóa tạo mã nhanh hơn?

  • Việc xác định nền tảng và trình biên dịch bạn sử dụng sẽ được đánh giá cao.
  • Tại sao kỹ thuật này dường như hoạt động?
  • Mã mẫu được khuyến khích.

Đây là một câu hỏi liên quan

[Chỉnh sửa] Câu hỏi này không nói về quy trình tổng thể để lập hồ sơ và tối ưu hóa. Giả sử rằng chương trình đã được viết chính xác, được biên dịch với sự tối ưu hóa đầy đủ, đã được thử nghiệm và đưa vào sản xuất. Có thể có cấu trúc trong mã của bạn cấm trình tối ưu hóa thực hiện công việc tốt nhất có thể. Bạn có thể làm gì để cấu trúc lại sẽ loại bỏ các lệnh cấm này và cho phép trình tối ưu hóa tạo mã nhanh hơn nữa?

[Chỉnh sửa] Bù đắp liên kết liên quan


7
Có thể là một ứng cử viên tốt cho cộng đồng wiki IMHO vì không có câu trả lời dứt khoát 'duy nhất' để câu hỏi này (thú vị) ...
ChristopheD

Lần nào tôi cũng nhớ. Cảm ơn vì chỉ ra điều ấy.
EvilTeach

"Tốt hơn" có nghĩa là bạn chỉ đơn giản là "nhanh hơn" hay bạn có tiêu chí xuất sắc khác trong đầu?
hiệu suất cao

1
Khá khó để viết một trình cấp phát thanh ghi tốt, đặc biệt là tính di động và cấp phát thanh ghi là hoàn toàn cần thiết đối với hiệu suất và kích thước mã. registerthực sự đã làm cho mã nhạy cảm với hiệu suất trở nên linh hoạt hơn bằng cách chống lại các trình biên dịch kém.
Potatoswatter

1
@EvilTeach: wiki cộng đồng không có nghĩa là "không có câu trả lời dứt khoát", nó không đồng nghĩa với thẻ chủ quan. Wiki cộng đồng có nghĩa là bạn muốn chuyển bài đăng của mình cho cộng đồng để người khác có thể chỉnh sửa. Đừng cảm thấy áp lực khi lướt qua các câu hỏi của bạn nếu bạn không cảm thấy thích.
Juliet

Câu trả lời:


54

Ghi vào các biến cục bộ chứ không phải đối số đầu ra! Điều này có thể là một trợ giúp rất lớn để khắc phục sự chậm lại của răng cưa. Ví dụ: nếu mã của bạn trông giống như

void DoSomething(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    for (int i=0; i<numFoo, i++)
    {
         barOut.munge(foo1, foo2[i]);
    }
}

trình biên dịch không biết rằng foo1! = barOut, và do đó phải tải lại foo1 mỗi lần qua vòng lặp. Nó cũng không thể đọc foo2 [i] cho đến khi quá trình ghi vào barOut hoàn tất. Bạn có thể bắt đầu bối rối với các con trỏ bị hạn chế, nhưng nó cũng hiệu quả (và rõ ràng hơn nhiều) để làm điều này:

void DoSomethingFaster(const Foo& foo1, const Foo* foo2, int numFoo, Foo& barOut)
{
    Foo barTemp = barOut;
    for (int i=0; i<numFoo, i++)
    {
         barTemp.munge(foo1, foo2[i]);
    }
    barOut = barTemp;
}

Nghe có vẻ ngớ ngẩn, nhưng trình biên dịch có thể xử lý biến cục bộ thông minh hơn nhiều, vì nó không thể trùng lặp trong bộ nhớ với bất kỳ đối số nào. Điều này có thể giúp bạn tránh tải-hit-store đáng sợ (được đề cập bởi Francis Boivin trong chủ đề này).


7
Điều này có lợi ích bổ sung là thường làm cho mọi thứ dễ đọc / hiểu hơn đối với các lập trình viên, vì họ cũng không phải lo lắng về các tác dụng phụ không rõ ràng có thể xảy ra.
Michael Burr

Hầu hết các IDE hiển thị các biến cục bộ theo mặc định, vì vậy không ít gõ
EvilTeach

9
bạn cũng có thể kích hoạt tính năng tối ưu hóa mà bằng cách sử dụng con trỏ hạn chế
Ben Voigt

4
@Ben - đó là sự thật, nhưng tôi nghĩ cách này rõ ràng hơn. Ngoài ra, nếu đầu vào và đầu ra trùng lặp, tôi tin rằng kết quả là không xác định với các con trỏ bị hạn chế (có thể có hành vi khác nhau giữa gỡ lỗi và phát hành), trong khi cách này ít nhất sẽ nhất quán. Đừng hiểu lầm tôi, tôi thích sử dụng hạn chế, nhưng tôi thậm chí không cần nó hơn.
celion

Bạn chỉ cần hy vọng rằng Foo không có hoạt động sao chép được xác định là sao chép một vài meg dữ liệu ;-)
Skizz

76

Đây là phương pháp viết mã để giúp trình biên dịch tạo mã nhanh — mọi ngôn ngữ, mọi nền tảng, mọi trình biên dịch, mọi vấn đề:

Đừng không sử dụng bất kỳ thủ đoạn thông minh mà lực lượng, hoặc thậm chí khuyến khích, trình biên dịch để đặt các biến trong bộ nhớ (bao gồm cả bộ nhớ cache và đăng ký) như bạn nghĩ tốt nhất. Đầu tiên hãy viết một chương trình chính xác và có thể bảo trì được.

Tiếp theo, lập hồ sơ mã của bạn.

Sau đó, và chỉ sau đó, bạn có thể muốn bắt đầu điều tra tác động của việc cho trình biên dịch biết cách sử dụng bộ nhớ. Thực hiện 1 thay đổi tại một thời điểm và đo lường tác động của nó.

Mong đợi để thất vọng và thực sự phải làm việc rất chăm chỉ để cải thiện hiệu suất nhỏ. Các trình biên dịch hiện đại cho các ngôn ngữ trưởng thành như Fortran và C là rất, rất tốt. Nếu bạn đọc một tài khoản về một 'thủ thuật' để đạt được hiệu suất tốt hơn từ mã, hãy nhớ rằng những người viết trình biên dịch cũng đã đọc về nó và nếu nó đáng làm, có thể đã thực hiện nó. Họ có thể đã viết những gì bạn đọc ngay từ đầu.


20
Các nhà phát triển Compiier có thời gian hữu hạn, giống như những người khác. Không phải tất cả các tối ưu hóa sẽ đưa nó vào trình biên dịch. Giống như &so %với quyền hạn của hai (hiếm khi, nếu đã từng được tối ưu hóa, nhưng có thể có tác động hiệu suất đáng kể). Nếu bạn đọc một mẹo về hiệu suất, cách duy nhất để biết liệu nó có hiệu quả hay không là thực hiện thay đổi và đo lường tác động. Đừng bao giờ cho rằng trình biên dịch sẽ tối ưu hóa thứ gì đó cho bạn.
Dave Jarvis

22
& và% luôn được tối ưu hóa, cùng với hầu hết các thủ thuật số học rẻ và miễn phí khác. Những gì không được tối ưu hóa là trường hợp toán hạng bên phải là một biến luôn luôn xảy ra là lũy thừa của hai.
Potatoswatter

8
Để làm rõ, tôi dường như đã làm một số độc giả bối rối: lời khuyên trong quá trình viết mã mà tôi đề xuất là trước tiên hãy phát triển một mã đơn giản mà không sử dụng các hướng dẫn bố trí bộ nhớ để thiết lập đường cơ sở về hiệu suất. Sau đó, hãy thử từng thứ một và đo lường tác động của chúng. Tôi đã không đưa ra bất kỳ lời khuyên nào về việc thực hiện các hoạt động.
Hiệu suất cao đánh dấu

17
Đối với lũy thừa hai không đổi n, gcc thay thế % nbằng & (n-1) ngay cả khi tối ưu hóa bị tắt . Đó không hẳn là "hiếm khi, nếu đã từng" ...
Porculus

12
% KHÔNG THỂ được tối ưu hóa khi & khi kiểu được ký, do các quy tắc ngu ngốc của C đối với phép chia số nguyên âm (làm tròn về 0 và có phần dư âm, thay vì làm tròn xuống và luôn có phần dư dương). Và hầu hết thời gian, những người lập trình thiếu hiểu biết sử dụng các loại có chữ ký ...
R .. GitHub NGỪNG TRỢ GIÚP

47

Thứ tự bạn duyệt qua bộ nhớ có thể có tác động sâu sắc đến hiệu suất và các trình biên dịch không thực sự giỏi trong việc tìm ra và sửa chữa nó. Bạn phải chú ý đến các mối quan tâm về địa phương bộ nhớ cache khi bạn viết mã nếu bạn quan tâm đến hiệu suất. Ví dụ, mảng hai chiều trong C được phân bổ ở định dạng hàng-chính. Việc chuyển ngang các mảng ở định dạng cột chính sẽ có xu hướng khiến bạn bỏ lỡ nhiều bộ nhớ cache hơn và khiến chương trình của bạn bị ràng buộc bộ nhớ nhiều hơn giới hạn bộ xử lý:

#define N 1000000;
int matrix[N][N] = { ... };

//awesomely fast
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[i][j];
  }
}

//painfully slow
long sum = 0;
for(int i = 0; i < N; i++){
  for(int j = 0; j < N; j++){
    sum += matrix[j][i];
  }
}

Nói một cách chính xác, đây không phải là vấn đề về trình tối ưu hóa, mà là vấn đề về tối ưu hóa.
EvilTeach

10
Chắc chắn đó là một vấn đề về trình tối ưu hóa. Mọi người đã viết bài về tối ưu hóa trao đổi vòng lặp tự động trong nhiều thập kỷ.
Phil Miller

20
@Potatoswatter Bạn đang nói gì vậy? Trình biên dịch C có thể làm bất cứ điều gì nó muốn miễn là quan sát được cùng một kết quả cuối cùng, và thực sự thì GCC 4.4 -floop-interchangesẽ lật một vòng lặp bên trong và bên ngoài nếu trình tối ưu hóa cho rằng nó có lợi.
ephemient

2
Huh, bạn đi rồi. Ngữ nghĩa C thường bị hủy hoại bởi các vấn đề về răng cưa. Tôi đoán lời khuyên thực sự ở đây là hãy vượt qua lá cờ đó!
Potatoswatter

36

Tối ưu hóa Chung

Đây là một số tối ưu hóa yêu thích của tôi. Tôi đã thực sự tăng thời gian thực thi và giảm kích thước chương trình bằng cách sử dụng chúng.

Khai báo các hàm nhỏ dưới dạng inlinehoặc macro

Mỗi lệnh gọi đến một hàm (hoặc phương thức) phải chịu phí tổn, chẳng hạn như đẩy các biến vào ngăn xếp. Một số chức năng cũng có thể phát sinh chi phí khi hoàn vốn. Một hàm hoặc phương thức không hiệu quả có ít câu lệnh trong nội dung của nó hơn là tổng chi phí được kết hợp. Đây là những ứng cử viên tốt cho nội tuyến, cho dù nó là #definemacro hoặc inlinehàm. (Vâng, tôi biết inlinechỉ là một gợi ý, nhưng trong trường hợp này, tôi coi đó như một lời nhắc nhở cho trình biên dịch.)

Loại bỏ mã chết và mã thừa

Nếu mã không được sử dụng hoặc không đóng góp vào kết quả của chương trình, hãy loại bỏ nó.

Đơn giản hóa thiết kế các thuật toán

Tôi đã từng loại bỏ rất nhiều mã hợp ngữ và thời gian thực thi khỏi một chương trình bằng cách viết ra phương trình đại số mà nó đang tính toán và sau đó đơn giản hóa biểu thức đại số. Việc triển khai biểu thức đại số đơn giản chiếm ít không gian và thời gian hơn so với hàm ban đầu.

Mở vòng lặp

Mỗi vòng lặp có chi phí kiểm tra gia tăng và kết thúc. Để có được ước tính về hệ số hiệu suất, hãy đếm số lượng lệnh trong chi phí (tối thiểu 3: tăng, kiểm tra, bắt đầu vòng lặp) và chia cho số câu lệnh bên trong vòng lặp. Con số càng thấp thì càng tốt.

Chỉnh sửa: cung cấp một ví dụ về việc mở vòng lặp Trước khi:

unsigned int sum = 0;
for (size_t i; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Sau khi giải nén:

unsigned int sum = 0;
size_t i = 0;
**const size_t STATEMENTS_PER_LOOP = 8;**
for (i = 0; i < BYTES_TO_CHECKSUM; **i = i / STATEMENTS_PER_LOOP**)
{
    sum += *buffer++; // 1
    sum += *buffer++; // 2
    sum += *buffer++; // 3
    sum += *buffer++; // 4
    sum += *buffer++; // 5
    sum += *buffer++; // 6
    sum += *buffer++; // 7
    sum += *buffer++; // 8
}
// Handle the remainder:
for (; i < BYTES_TO_CHECKSUM; ++i)
{
    sum += *buffer++;
}

Trong lợi thế này, một lợi ích thứ cấp đạt được: nhiều câu lệnh được thực thi hơn trước khi bộ xử lý phải tải lại bộ đệm lệnh.

Tôi đã có kết quả đáng kinh ngạc khi tôi mở vòng lặp đến 32 câu lệnh. Đây là một trong những điểm nghẽn vì chương trình phải tính toán tổng kiểm tra trên tệp 2GB. Việc tối ưu hóa này kết hợp với việc đọc khối đã cải thiện hiệu suất từ ​​1 giờ lên 5 phút. Việc giải nén vòng lặp cũng mang lại hiệu suất tuyệt vời trong hợp ngữ, của tôi memcpynhanh hơn rất nhiều so với trình biên dịch memcpy. - TM

Giảm bớt các ifcâu lệnh

Bộ xử lý ghét các nhánh hoặc nhảy, vì nó buộc bộ xử lý tải lại hàng đợi lệnh của nó.

Boolean Arithmetic ( Đã chỉnh sửa: áp dụng định dạng mã cho đoạn mã, thêm ví dụ)

Chuyển đổi ifcâu lệnh thành phép gán boolean. Một số bộ xử lý có thể thực thi các lệnh một cách có điều kiện mà không cần phân nhánh:

bool status = true;
status = status && /* first test */;
status = status && /* second test */;

Các mạch ngắn của logic AND điều hành ( &&) ngăn cản thực hiện các bài kiểm tra nếu statusfalse.

Thí dụ:

struct Reader_Interface
{
  virtual bool  write(unsigned int value) = 0;
};

struct Rectangle
{
  unsigned int origin_x;
  unsigned int origin_y;
  unsigned int height;
  unsigned int width;

  bool  write(Reader_Interface * p_reader)
  {
    bool status = false;
    if (p_reader)
    {
       status = p_reader->write(origin_x);
       status = status && p_reader->write(origin_y);
       status = status && p_reader->write(height);
       status = status && p_reader->write(width);
    }
    return status;
};

Phân bổ biến nhân tố bên ngoài vòng lặp

Nếu một biến được tạo nhanh chóng bên trong một vòng lặp, hãy di chuyển việc tạo / cấp phát đến trước vòng lặp. Trong hầu hết các trường hợp, biến không cần phải được cấp phát trong mỗi lần lặp.

Biểu thức hằng số thừa số bên ngoài vòng lặp

Nếu một phép tính hoặc giá trị biến không phụ thuộc vào chỉ số vòng lặp, hãy di chuyển nó ra bên ngoài (trước) vòng lặp.

I / O theo khối

Đọc và ghi dữ liệu trong các khối (khối) lớn. Càng to càng tốt. Ví dụ: đọc một octect tại một thời điểm kém hiệu quả hơn đọc 1024 octet với một lần đọc.
Thí dụ:

static const char  Menu_Text[] = "\n"
    "1) Print\n"
    "2) Insert new customer\n"
    "3) Destroy\n"
    "4) Launch Nasal Demons\n"
    "Enter selection:  ";
static const size_t Menu_Text_Length = sizeof(Menu_Text) - sizeof('\0');
//...
std::cout.write(Menu_Text, Menu_Text_Length);

Hiệu quả của kỹ thuật này có thể được chứng minh trực quan. :-)

Không sử dụng printf gia đình cho dữ liệu liên tục

Dữ liệu không đổi có thể được xuất ra bằng cách ghi khối. Ghi có định dạng sẽ lãng phí thời gian quét văn bản để định dạng ký tự hoặc xử lý lệnh định dạng. Xem ví dụ mã ở trên.

Định dạng vào bộ nhớ, sau đó ghi

Định dạng thành một charmảng bằng cách sử dụng nhiều sprintf, sau đó sử dụng fwrite. Điều này cũng cho phép chia bố cục dữ liệu thành "phần không đổi" và phần biến. Hãy nghĩ đến tính năng trộn thư .

Khai báo văn bản không đổi (chuỗi ký tự) dưới dạng static const

Khi các biến được khai báo mà không có static, một số trình biên dịch có thể phân bổ không gian trên ngăn xếp và sao chép dữ liệu từ ROM. Đây là hai thao tác không cần thiết. Điều này có thể được khắc phục bằng cách sử dụng statictiền tố.

Cuối cùng, Mã giống như trình biên dịch sẽ

Đôi khi, trình biên dịch có thể tối ưu hóa một số câu lệnh nhỏ tốt hơn một phiên bản phức tạp. Ngoài ra, viết mã để giúp trình biên dịch tối ưu hóa cũng giúp ích. Nếu tôi muốn trình biên dịch sử dụng các hướng dẫn chuyển khối đặc biệt, tôi sẽ viết mã có vẻ như nó nên sử dụng các hướng dẫn đặc biệt.


2
Điều thú vị là bạn có thể cung cấp một ví dụ trong đó bạn có mã tốt hơn với một vài câu lệnh nhỏ, thay vì câu lệnh lớn hơn. Bạn có thể chỉ ra một ví dụ về việc viết lại một if, sử dụng boolean. Nói chung, tôi sẽ để vòng lặp không cuộn vào trình biên dịch, vì nó có thể có cảm giác tốt hơn về kích thước bộ nhớ cache. Tôi hơi ngạc nhiên về ý tưởng chạy nước rút, sau đó là viết lách. Tôi sẽ nghĩ rằng fprintf thực sự làm điều đó dưới mui xe. Bạn có thể cho biết thêm một chút chi tiết ở đây?
EvilTeach

1
Không có gì đảm bảo rằng fprintfcác định dạng thành một bộ đệm riêng biệt sau đó xuất ra bộ đệm. Một fprintfchuỗi được sắp xếp hợp lý (để sử dụng bộ nhớ) sẽ xuất ra tất cả văn bản chưa được định dạng, sau đó định dạng và đầu ra, và lặp lại cho đến khi toàn bộ chuỗi định dạng được xử lý, do đó thực hiện 1 lệnh gọi đầu ra cho mỗi loại đầu ra (được định dạng so với chưa được định dạng). Các triển khai khác sẽ cần phân bổ bộ nhớ động cho mỗi lệnh gọi để giữ toàn bộ chuỗi mới (điều này không tốt trong môi trường hệ thống nhúng). Đề xuất của tôi làm giảm số lượng đầu ra.
Thomas Matthews

3
Tôi đã từng có một sự cải thiện hiệu suất đáng kể bằng cách cuộn lên một vòng. Sau đó, tôi đã tìm ra cách cuộn nó chặt chẽ hơn bằng cách sử dụng một số chuyển hướng, và chương trình nhanh hơn đáng kể. (Hồ sơ cho thấy chức năng cụ thể này chiếm 60-80% thời gian chạy và tôi đã kiểm tra hiệu suất một cách cẩn thận trước và sau đó.) Tôi tin rằng sự cải thiện là do vị trí tốt hơn, nhưng tôi không hoàn toàn chắc chắn về điều đó.
David Thornley

16
Nhiều người trong số này là tối ưu hóa của lập trình viên chứ không phải là cách để lập trình viên giúp trình biên dịch tối ưu hóa, đó là lực đẩy của câu hỏi ban đầu. Ví dụ: hủy cuộn vòng lặp. Có, bạn có thể tự mình thực hiện thao tác mở cuộn, nhưng tôi nghĩ sẽ thú vị hơn khi tìm ra những rào cản đối với trình biên dịch khi mở cuộn cho bạn và loại bỏ những rào cản đó.
Adrian McCarthy

26

Bạn thực sự không kiểm soát được hiệu suất của chương trình của bạn. Sử dụng các thuật toán và cấu trúc thích hợp và hồ sơ, hồ sơ, hồ sơ.

Điều đó nói rằng, bạn không nên lặp lại nội bộ trên một hàm nhỏ từ một tệp trong tệp khác, vì điều đó ngăn nó được nội dòng.

Tránh lấy địa chỉ của một biến nếu có thể. Yêu cầu con trỏ không phải là "miễn phí" vì nó có nghĩa là biến cần được giữ trong bộ nhớ. Thậm chí một mảng có thể được giữ trong các thanh ghi nếu bạn tránh các con trỏ - điều này rất cần thiết cho việc vectơ hóa.

Dẫn đến điểm tiếp theo, hãy đọc hướng dẫn sử dụng ^ # $ @ ! GCC có thể vectơ hóa mã C đơn giản nếu bạn đặt __restrict__ở đây và __attribute__( __aligned__ )ở đó. Nếu bạn muốn một cái gì đó thật cụ thể từ trình tối ưu hóa, bạn có thể phải thật cụ thể.


14
Đây là một câu trả lời hay, nhưng lưu ý rằng việc tối ưu hóa toàn bộ chương trình đang trở nên phổ biến hơn và trên thực tế có thể nội tuyến các hàm trên các đơn vị dịch.
Phil Miller

1
@Novelocrat Yep - không cần phải nói tôi đã rất ngạc nhiên khi lần đầu tiên tôi nhìn thấy thứ gì đó A.cđược đưa vào B.c.
Jonathon Reinhart

18

Trên hầu hết các bộ vi xử lý hiện đại, điểm nghẽn lớn nhất là bộ nhớ.

Aliasing: Load-Hit-Store có thể tàn phá trong một vòng lặp chặt chẽ. Nếu bạn đang đọc một vị trí bộ nhớ và ghi vào một vị trí bộ nhớ khác và biết rằng chúng rời rạc, cẩn thận đặt một từ khóa bí danh trên các tham số hàm thực sự có thể giúp trình biên dịch tạo mã nhanh hơn. Tuy nhiên, nếu các vùng bộ nhớ trùng lặp và bạn đã sử dụng 'bí danh', bạn sẽ có một phiên gỡ lỗi tốt về các hành vi không xác định!

Cache-miss: Không thực sự chắc chắn làm thế nào bạn có thể giúp trình biên dịch vì nó chủ yếu là thuật toán, nhưng có những bản chất để tìm nạp trước bộ nhớ.

Cũng đừng cố chuyển đổi giá trị dấu phẩy động thành int và ngược lại quá nhiều vì chúng sử dụng các thanh ghi khác nhau và chuyển đổi từ kiểu này sang kiểu khác có nghĩa là gọi lệnh chuyển đổi thực tế, ghi giá trị vào bộ nhớ và đọc lại trong tập thanh ghi thích hợp .


4
+1 cho các cửa hàng có lượt truy cập tải và các loại đăng ký khác nhau. Tôi không chắc nó lớn như thế nào trong x86, nhưng họ đang đầu tư vào PowerPC (ví dụ: Xbox360 và Playstation3).
celion

Hầu hết các bài báo về kỹ thuật tối ưu hóa vòng lặp trình biên dịch đều giả định lồng hoàn hảo, có nghĩa là phần thân của mỗi vòng lặp ngoại trừ phần trong cùng chỉ là một vòng lặp khác. Những bài báo này chỉ đơn giản là không thảo luận về các bước cần thiết để khái quát hóa như vậy, ngay cả khi rất rõ ràng rằng chúng có thể được. Vì vậy, tôi mong đợi nhiều triển khai không thực sự hỗ trợ những khái quát đó, vì cần thêm nỗ lực. Do đó, nhiều thuật toán để tối ưu hóa việc sử dụng bộ nhớ cache trong các vòng lặp có thể hoạt động tốt hơn rất nhiều trên các tổ hoàn hảo so với các tổ không hoàn hảo.
Phil Miller

11

Phần lớn mã mà mọi người viết sẽ bị ràng buộc I / O (tôi tin rằng tất cả các mã tôi đã viết để kiếm tiền trong 30 năm qua đều bị ràng buộc như vậy), vì vậy các hoạt động của trình tối ưu hóa đối với hầu hết mọi người sẽ mang tính học thuật.

Tuy nhiên, tôi sẽ nhắc mọi người rằng để mã được tối ưu hóa, bạn phải yêu cầu trình biên dịch tối ưu hóa nó - rất nhiều người (kể cả tôi khi tôi quên) đăng các điểm chuẩn C ++ ở đây là vô nghĩa nếu không bật trình tối ưu hóa.


7
Thú thực là tôi rất kỳ lạ - tôi làm việc trên các mã phân tích số khoa học lớn có giới hạn băng thông bộ nhớ. Đối với dân số chung của các chương trình tôi đồng ý với Neil.
hiệu suất cao

6
Thật; nhưng rất nhiều mã ràng buộc I / O ngày nay được viết bằng những ngôn ngữ thực tế là những ngôn ngữ bi quan - những ngôn ngữ thậm chí không có trình biên dịch. Tôi nghi ngờ rằng các khu vực mà C và C ++ vẫn được sử dụng sẽ có xu hướng là các khu vực quan trọng hơn để tối ưu hóa thứ gì đó (sử dụng CPU, sử dụng bộ nhớ, kích thước mã ...)
Porculus

3
Tôi đã dành gần hết 30 năm qua để làm việc trên mã với rất ít I / O. Tiết kiệm trong 2 năm làm cơ sở dữ liệu. Đồ họa, hệ thống điều khiển, mô phỏng - không có I / O nào ràng buộc. Nếu I / O là nút thắt cổ chai của hầu hết mọi người, chúng tôi sẽ không chú ý nhiều đến Intel và AMD.
phkahler

2
Vâng, tôi không thực sự mua lập luận này - nếu không thì chúng tôi (trong công việc của tôi) sẽ không tìm cách dành nhiều thời gian tính toán hơn để thực hiện I / O. Ngoài ra, hầu hết các phần mềm liên kết I / O mà tôi đã sử dụng đều bị ràng buộc I / O bởi vì I / O được thực hiện một cách cẩu thả; nếu một người tối ưu hóa các mẫu truy cập (giống như với bộ nhớ), người ta có thể nhận được lợi ích lớn về hiệu suất.
dash-tom-bang

3
Gần đây tôi đã phát hiện ra rằng hầu như không có mã nào được viết bằng ngôn ngữ C ++ bị ràng buộc I / O. Chắc chắn, nếu bạn đang gọi một chức năng OS để chuyển đĩa hàng loạt, chuỗi của bạn có thể chuyển sang trạng thái chờ I / O (nhưng với bộ nhớ đệm, thậm chí điều đó còn đáng nghi ngờ). Nhưng các chức năng thư viện I / O thông thường, những chức năng mà mọi người khuyên dùng vì chúng tiêu chuẩn và di động, thực sự chậm một cách thảm hại so với công nghệ đĩa hiện đại (ngay cả những thứ có giá vừa phải). Rất có thể, I / O chỉ là nút thắt cổ chai nếu bạn đang xả toàn bộ vào đĩa sau khi ghi chỉ vài byte. OTOH, UI là một vấn đề khác, con người chúng ta chậm chạp.
Ben Voigt

11

sử dụng độ đúng của const càng nhiều càng tốt trong mã của bạn. Nó cho phép trình biên dịch tối ưu hóa tốt hơn nhiều.

Trong tài liệu này có vô số mẹo tối ưu hóa khác: Tối ưu hóa CPP (mặc dù tài liệu hơi cũ)

điểm nổi bật:

  • sử dụng danh sách khởi tạo hàm tạo
  • sử dụng toán tử tiền tố
  • sử dụng các hàm tạo rõ ràng
  • hàm nội tuyến
  • tránh những đồ vật tạm thời
  • nhận thức được chi phí của các chức năng ảo
  • trả về các đối tượng thông qua các tham số tham chiếu
  • xem xét phân bổ mỗi lớp
  • xem xét trình phân bổ vùng chứa stl
  • tối ưu hóa 'thành viên trống'
  • Vân vân

8
Không nhiều, hiếm. Tuy nhiên, nó cải thiện tính đúng đắn thực tế.
Potatoswatter

5
Trong C và C ++, trình biên dịch không thể sử dụng const để tối ưu hóa bởi vì truyền nó đi là hành vi được xác định rõ.
dsimcha

+1: const là một ví dụ điển hình về điều gì đó sẽ tác động trực tiếp đến mã đã biên dịch. Re @ dsimcha's comment - một trình biên dịch tốt sẽ kiểm tra xem điều này có xảy ra hay không. Tất nhiên, một trình biên dịch tốt sẽ "tìm thấy" yếu tố const mà không tuyên bố như vậy dù sao ...
Hogan

@dsimcha: Tuy nhiên, việc thay đổi con trỏ đủ điều kiện const restrict không được xác định. Vì vậy, một trình biên dịch có thể tối ưu hóa khác nhau trong trường hợp như vậy.
Dietrich Epp

6
@dsimcha truyền đi consttrên một consttham chiếu hoặc constcon trỏ tới một constđối tượng không phải là đối tượng được xác định rõ ràng. sửa đổi một constđối tượng thực tế (tức là một đối tượng được khai báo như constban đầu) thì không.
Stephen Lin

9

Cố gắng lập trình bằng cách sử dụng chỉ định đơn tĩnh càng nhiều càng tốt. SSA giống hệt như những gì bạn kết thúc với hầu hết các ngôn ngữ lập trình chức năng và đó là thứ mà hầu hết các trình biên dịch chuyển đổi mã của bạn sang để thực hiện tối ưu hóa của chúng vì nó dễ làm việc hơn. Bằng cách làm này, những nơi mà trình biên dịch có thể bị nhầm lẫn được đưa ra ánh sáng. Nó cũng làm cho tất cả trừ các trình cấp phát thanh ghi tồi tệ nhất hoạt động tốt như các trình cấp phát thanh ghi tốt nhất và cho phép bạn gỡ lỗi dễ dàng hơn vì bạn hầu như không bao giờ phải tự hỏi biến lấy giá trị từ đâu vì chỉ có một nơi mà nó được gán.
Tránh các biến toàn cục.

Khi làm việc với dữ liệu bằng tham chiếu hoặc con trỏ kéo dữ liệu đó vào các biến cục bộ, hãy thực hiện công việc của bạn, rồi sao chép lại. (trừ khi bạn có lý do chính đáng để không)

Sử dụng phép so sánh gần như miễn phí với 0 mà hầu hết các bộ xử lý cung cấp cho bạn khi thực hiện các phép toán hoặc phép toán logic. Bạn hầu như luôn nhận được một cờ cho == 0 và <0, từ đó bạn có thể dễ dàng nhận được 3 điều kiện:

x= f();
if(!x){
   a();
} else if (x<0){
   b();
} else {
   c();
}

hầu như luôn rẻ hơn so với thử nghiệm các hằng số khác.

Một mẹo khác là sử dụng phép trừ để loại bỏ một phép so sánh trong kiểm tra phạm vi.

#define FOO_MIN 8
#define FOO_MAX 199
int good_foo(int foo) {
    unsigned int bar = foo-FOO_MIN;
    int rc = ((FOO_MAX-FOO_MIN) < bar) ? 1 : 0;
    return rc;
} 

Điều này rất thường xuyên có thể tránh được sự nhảy vọt trong các ngôn ngữ làm chập mạch các biểu thức boolean và tránh việc trình biên dịch phải cố gắng tìm ra cách xử lý để theo kịp kết quả của phép so sánh đầu tiên trong khi thực hiện phép so sánh thứ hai và sau đó kết hợp chúng. Điều này có vẻ như nó có khả năng sử dụng hết một thanh ghi bổ sung, nhưng nó hầu như không bao giờ xảy ra. Thường thì bạn không cần foo nữa, và nếu bạn làm vậy thì rc vẫn chưa được sử dụng để nó có thể đến đó.

Khi sử dụng các hàm chuỗi trong c (strcpy, memcpy, ...) hãy nhớ những gì chúng trả về - đích đến! Bạn thường có thể nhận được mã tốt hơn bằng cách 'bỏ quên' bản sao của con trỏ tới đích và chỉ cần lấy lại nó khi trả về các hàm này.

Đừng bao giờ bỏ qua cơ hội trả về chính xác thứ mà hàm cuối cùng bạn đã gọi đã trả về. Các trình biên dịch không giỏi đến nỗi:

foo_t * make_foo(int a, int b, int c) {
        foo_t * x = malloc(sizeof(foo));
        if (!x) {
             // return NULL;
             return x; // x is NULL, already in the register used for returns, so duh
        }
        x->a= a;
        x->b = b;
        x->c = c;
        return x;
}

Tất nhiên, bạn có thể đảo ngược logic về điều đó nếu và chỉ có một điểm quay lại.

(thủ thuật tôi nhớ lại sau này)

Khai báo các hàm dưới dạng tĩnh khi bạn có thể luôn là một ý kiến ​​hay. Nếu trình biên dịch có thể tự chứng minh rằng nó đã tính đến mọi trình gọi của một hàm cụ thể thì nó có thể phá vỡ các quy ước gọi cho hàm đó với danh nghĩa tối ưu hóa. Các trình biên dịch thường có thể tránh di chuyển các tham số vào các thanh ghi hoặc các vị trí ngăn xếp mà được gọi là các hàm thường mong đợi các tham số của chúng ở trong (nó phải lệch trong cả hàm được gọi và vị trí của tất cả các trình gọi để thực hiện điều này). Trình biên dịch cũng có thể thường tận dụng lợi thế của việc biết bộ nhớ và đăng ký mà hàm được gọi sẽ cần và tránh tạo mã để bảo toàn các giá trị biến trong thanh ghi hoặc vị trí bộ nhớ mà hàm được gọi không làm phiền. Điều này đặc biệt hiệu quả khi có ít lệnh gọi đến một hàm.


2
Thực sự không cần thiết phải sử dụng phép trừ khi kiểm tra phạm vi, LLVM, GCC và trình biên dịch của tôi ít nhất làm điều này tự động. Rất ít người có thể hiểu mã với phép trừ làm gì và thậm chí còn ít hơn tại sao nó thực sự hoạt động.
Gratian Lup

trong ví dụ trên, b () không thể được gọi vì nếu (x <0) thì a () sẽ được gọi.
EvilTeach

@EvilTeach Không, nó sẽ không. Việc so sánh các kết quả trong các cuộc gọi đến một () là x!
nategoose

@nategoose. nếu x là -3 thì! x đúng.
EvilTeach

@EvilTeach Trong C 0 là sai và mọi thứ khác là đúng, vì vậy -3 là đúng, vì vậy -3 là sai!
nategoose

9

Tôi đã viết một trình biên dịch C tối ưu hóa và đây là một số điều rất hữu ích cần xem xét:

  1. Làm cho hầu hết các chức năng tĩnh. Điều này cho phép lan truyền hằng số liên thủ tục và phân tích bí danh thực hiện công việc của nó, nếu không trình biên dịch cần phải giả định rằng hàm có thể được gọi từ bên ngoài đơn vị dịch với các giá trị hoàn toàn chưa biết cho các tham số. Nếu bạn nhìn vào các thư viện mã nguồn mở nổi tiếng, tất cả chúng đều đánh dấu các chức năng là tĩnh ngoại trừ những chức năng thực sự cần thiết.

  2. Nếu các biến toàn cục được sử dụng, hãy đánh dấu chúng là tĩnh và không đổi nếu có thể. Nếu chúng được khởi tạo một lần (chỉ đọc), tốt hơn nên sử dụng danh sách trình khởi tạo như static const int VAL [] = {1,2,3,4}, nếu không trình biên dịch có thể không phát hiện ra rằng các biến thực sự là hằng số được khởi tạo và sẽ không thể thay thế các tải từ biến bằng các hằng số.

  3. KHÔNG BAO GIỜ sử dụng goto vào bên trong vòng lặp, vòng lặp sẽ không được nhận dạng bởi hầu hết các trình biên dịch nữa và không có tối ưu hóa quan trọng nào được áp dụng.

  4. Chỉ sử dụng các tham số con trỏ nếu cần thiết và đánh dấu chúng là giới hạn nếu có thể. Điều này giúp ích cho việc phân tích bí danh rất nhiều vì lập trình viên đảm bảo không có bí danh (phân tích bí danh liên thủ tục thường rất sơ khai). Các đối tượng cấu trúc rất nhỏ phải được chuyển bằng giá trị, không phải bằng tham chiếu.

  5. Sử dụng mảng thay vì con trỏ bất cứ khi nào có thể, đặc biệt là các vòng lặp bên trong (a [i]). Một mảng thường cung cấp nhiều thông tin hơn để phân tích bí danh và sau một số tối ưu hóa, mã tương tự vẫn sẽ được tạo (tìm kiếm sự giảm độ mạnh của vòng lặp nếu tò mò). Điều này cũng làm tăng cơ hội cho chuyển động mã bất biến vòng lặp được áp dụng.

  6. Cố gắng di chuyển bên ngoài các cuộc gọi vòng lặp đến các chức năng lớn hoặc các chức năng bên ngoài không có tác dụng phụ (không phụ thuộc vào lần lặp vòng lặp hiện tại). Trong nhiều trường hợp, các hàm nhỏ được nội tuyến hoặc được chuyển đổi thành bản chất để dễ dàng di chuyển, nhưng các hàm lớn dường như khiến trình biên dịch có tác dụng phụ khi chúng thực sự không có. Các tác dụng phụ đối với các hàm bên ngoài hoàn toàn không được biết đến, ngoại trừ một số hàm từ thư viện chuẩn đôi khi được mô hình hóa bởi một số trình biên dịch, làm cho mã chuyển động vòng lặp có thể bất biến.

  7. Khi viết các bài kiểm tra với nhiều điều kiện, hãy đặt điều kiện có nhiều khả năng nhất trước. if (a || b || c) nên if (b || a || c) if b có nhiều khả năng đúng hơn các câu khác. Các trình biên dịch thường không biết gì về các giá trị có thể có của các điều kiện và những nhánh nào được lấy nhiều hơn (chúng có thể được biết bằng cách sử dụng thông tin hồ sơ, nhưng ít lập trình viên sử dụng nó).

  8. Sử dụng một công tắc sẽ nhanh hơn thực hiện một thử nghiệm như if (a || b || ... || z). Trước tiên, hãy kiểm tra xem trình biên dịch của bạn có thực hiện điều này tự động hay không, một số thì làm và nếu có if thì dễ đọc hơn .


7

Trong trường hợp hệ thống nhúng và mã được viết bằng C / C ++, tôi cố gắng tránh cấp phát bộ nhớ động càng nhiều càng tốt. Lý do chính tôi làm điều này không nhất thiết là hiệu suất nhưng quy tắc ngón tay cái này có ý nghĩa về hiệu suất.

Các thuật toán được sử dụng để quản lý heap nổi tiếng là chậm trong một số nền tảng (ví dụ: vxworks). Tệ hơn nữa, thời gian cần để trả về từ một cuộc gọi đến malloc phụ thuộc nhiều vào trạng thái hiện tại của đống. Do đó, bất kỳ hàm nào gọi malloc sẽ nhận được một kết quả hiệu suất mà không thể dễ dàng tính được. Lần truy cập hiệu suất đó có thể là tối thiểu nếu heap vẫn sạch nhưng sau khi thiết bị đó chạy một thời gian, heap có thể bị phân mảnh. Các cuộc gọi sẽ mất nhiều thời gian hơn và bạn không thể dễ dàng tính toán hiệu suất sẽ suy giảm như thế nào theo thời gian. Bạn thực sự không thể đưa ra một ước tính trường hợp tồi tệ hơn. Trình tối ưu hóa cũng không thể cung cấp cho bạn bất kỳ trợ giúp nào trong trường hợp này. Để làm cho vấn đề thậm chí còn tồi tệ hơn, nếu heap trở nên quá phân mảnh, các cuộc gọi sẽ bắt đầu thất bại hoàn toàn. Giải pháp là sử dụng vùng nhớ (ví dụ:lát mỏng ) thay vì đống. Các cuộc gọi phân bổ sẽ nhanh hơn nhiều và mang tính xác định nếu bạn làm đúng.


Quy tắc chung của tôi là nếu bạn phải cấp phát động, hãy lấy một mảng để bạn không cần phải làm lại. Định vị trước chúng vectơ.
EvilTeach

7

Một mẹo nhỏ ngớ ngẩn nhưng sẽ giúp bạn tiết kiệm một số lượng nhỏ tốc độ và mã.

Luôn truyền các đối số của hàm theo cùng một thứ tự.

Nếu bạn có f_1 (x, y, z) gọi f_2, hãy khai báo f_2 là f_2 (x, y, z). Không khai báo nó là f_2 (x, z, y).

Lý do cho điều này là nền tảng C / C ++ ABI (quy ước gọi AKA) hứa hẹn truyền các đối số trong các thanh ghi cụ thể và các vị trí ngăn xếp. Khi các đối số đã nằm trong các thanh ghi chính xác thì nó không cần phải di chuyển chúng xung quanh.

Trong khi đọc mã được tháo rời, tôi đã thấy một số đăng ký xáo trộn vô lý vì mọi người không tuân theo quy tắc này.


2
Cả C và C ++ đều không đưa ra bất kỳ đảm bảo nào về, hoặc thậm chí đề cập đến việc chuyển các thanh ghi hoặc vị trí ngăn xếp cụ thể. Đó là ABI (ví dụ Linux ELF) xác định chi tiết của việc truyền tham số.
Emmet

5

Hai kỹ thuật mã hóa mà tôi không thấy trong danh sách trên:

Bỏ qua trình liên kết bằng cách viết mã dưới dạng một nguồn duy nhất

Mặc dù biên dịch riêng biệt thực sự tốt cho thời gian biên dịch, nhưng nó lại rất tệ khi bạn nói về tối ưu hóa. Về cơ bản, trình biên dịch không thể tối ưu hóa ngoài đơn vị biên dịch, đó là miền dành riêng cho trình liên kết.

Nhưng nếu bạn thiết kế tốt chương trình của mình, bạn cũng có thể biên dịch nó thông qua một nguồn chung duy nhất. Đó là thay vì biên dịch unit1.c và unit2.c sau đó liên kết cả hai đối tượng, biên dịch all.c chỉ đơn thuần là #include unit1.c và unit2.c. Vì vậy, bạn sẽ được hưởng lợi từ tất cả các tối ưu hóa trình biên dịch.

Nó rất giống như viết các chương trình chỉ tiêu đề trong C ++ (và thậm chí còn dễ dàng hơn để làm trong C).

Kỹ thuật này đủ dễ dàng nếu bạn viết chương trình của mình để kích hoạt nó ngay từ đầu, nhưng bạn cũng phải biết rằng nó thay đổi một phần ngữ nghĩa của C và bạn có thể gặp một số vấn đề như biến tĩnh hoặc va chạm macro. Đối với hầu hết các chương trình, nó đủ dễ dàng để khắc phục các sự cố nhỏ xảy ra. Cũng lưu ý rằng việc biên dịch dưới dạng một nguồn duy nhất sẽ chậm hơn và có thể chiếm dung lượng lớn bộ nhớ (thường không phải là vấn đề với các hệ thống hiện đại).

Sử dụng kỹ thuật đơn giản này, tôi đã tình cờ làm cho một số chương trình mà tôi đã viết nhanh hơn mười lần!

Giống như từ khóa đăng ký, thủ thuật này cũng có thể sớm trở nên lỗi thời. Tối ưu hóa thông qua trình liên kết bắt đầu được hỗ trợ bởi trình biên dịch gcc: Tối ưu hóa thời gian liên kết .

Các nhiệm vụ nguyên tử riêng biệt trong các vòng lặp

Cái này khó hơn. Đó là về sự tương tác giữa thiết kế thuật toán và cách trình tối ưu hóa quản lý bộ nhớ cache và phân bổ đăng ký. Thông thường các chương trình phải lặp lại một số cấu trúc dữ liệu và đối với mỗi mục thực hiện một số hành động. Thông thường, các hành động được thực hiện có thể được tách thành hai nhiệm vụ độc lập về mặt logic. Nếu trường hợp đó xảy ra, bạn có thể viết chính xác cùng một chương trình với hai vòng lặp trên cùng một ranh giới thực hiện chính xác một nhiệm vụ. Trong một số trường hợp, viết nó theo cách này có thể nhanh hơn vòng lặp duy nhất (chi tiết phức tạp hơn, nhưng có thể giải thích rằng với trường hợp tác vụ đơn giản, tất cả các biến có thể được giữ trong thanh ghi bộ xử lý và với một số phức tạp hơn thì không thể và một số thanh ghi phải được ghi vào bộ nhớ và đọc lại sau đó và chi phí cao hơn so với điều khiển luồng bổ sung).

Hãy cẩn thận với điều này (biểu diễn tiểu sử có sử dụng thủ thuật này hay không) cũng giống như sử dụng đăng ký, nó cũng có thể cho hiệu suất kém hơn so với biểu diễn được cải thiện.


2
Vâng, đến giờ, LTO đã làm cho nửa đầu của bài đăng này trở nên thừa và có lẽ là lời khuyên tồi.
underscore_d

@underscore_d: vẫn còn một số vấn đề (chủ yếu liên quan đến khả năng hiển thị của các ký hiệu được xuất), nhưng từ quan điểm hiệu suất đơn thuần, có lẽ không còn vấn đề nào nữa.
kriss

4

Tôi đã thực sự thấy điều này được thực hiện trong SQLite và họ cho rằng nó dẫn đến hiệu suất tăng ~ 5%: Đặt tất cả mã của bạn vào một tệp hoặc sử dụng bộ tiền xử lý để làm điều tương tự. Bằng cách này, trình tối ưu hóa sẽ có quyền truy cập vào toàn bộ chương trình và có thể thực hiện nhiều tối ưu hóa liên thủ tục hơn.


5
Việc đặt các hàm được sử dụng cùng nhau trong khoảng vật lý gần nhau trong nguồn làm tăng khả năng chúng ở gần nhau trong tệp đối tượng và gần nhau trong tệp thực thi của bạn. Vị trí hướng dẫn được cải thiện này có thể giúp tránh bỏ lỡ bộ nhớ cache của lệnh trong khi chạy.
paxos1977

Trình biên dịch AIX có một công tắc trình biên dịch để khuyến khích hành vi đó -qipa [= <suboptions_list>] | -qnoipa Bật hoặc tùy chỉnh một lớp tối ưu hóa được gọi là phân tích liên thủ tục (IPA).
EvilTeach

4
Tốt nhất là có cách phát triển không đòi hỏi điều này. Sử dụng thực tế này như một cái cớ để viết mã không mô-đun nhìn chung sẽ chỉ dẫn đến mã chậm và có vấn đề về bảo trì.
Hogan

3
Tôi nghĩ thông tin này hơi cũ. Về lý thuyết, các tính năng tối ưu hóa toàn bộ chương trình được tích hợp trong nhiều trình biên dịch hiện nay (ví dụ: "Tối ưu hóa thời gian liên kết" trong gcc) cho phép mang lại những lợi ích tương tự, nhưng với quy trình làm việc hoàn toàn tiêu chuẩn (cộng với thời gian biên dịch lại nhanh hơn so với việc đưa tất cả vào một tệp !)
Ponkadoodle

@Wallacoloo Chắc chắn, đây là ngày hết hạn của faaar. FWIW, tôi vừa mới sử dụng LTO của GCC lần đầu tiên hôm nay và - tất cả những thứ khác đều bằng nhau -O3- nó đã thổi bay 22% kích thước ban đầu so với chương trình của tôi. (Nó không bị ràng buộc bởi CPU, vì vậy tôi không có nhiều điều để nói về tốc độ.)
underscore_d

4

Hầu hết các trình biên dịch hiện đại nên làm tốt việc tăng tốc đệ quy đuôi , vì các lệnh gọi hàm có thể được tối ưu hóa.

Thí dụ:

int fac2(int x, int cur) {
  if (x == 1) return cur;
  return fac2(x - 1, cur * x); 
}
int fac(int x) {
  return fac2(x, 1);
}

Tất nhiên ví dụ này không có bất kỳ kiểm tra giới hạn nào.

Chỉnh sửa muộn

Trong khi tôi không có kiến ​​thức trực tiếp về mã; rõ ràng là các yêu cầu của việc sử dụng CTE trên SQL Server được thiết kế đặc biệt để nó có thể tối ưu hóa thông qua đệ quy đuôi-end.


1
câu hỏi là về C. C không loại bỏ đệ quy đuôi, vì vậy đệ quy đuôi hoặc đệ quy khác, ngăn xếp có thể bị nổ nếu đệ quy đi quá sâu.
Toad

1
Tôi đã tránh được vấn đề quy ước gọi điện bằng cách sử dụng goto. Có ít chi phí hơn theo cách đó.
EvilTeach

2
@hogan: cái này mới đối với tôi. Bạn có thể chỉ vào bất kỳ trình biên dịch nào làm được điều này không? Và làm thế nào bạn có thể chắc chắn rằng nó thực sự tối ưu hóa nó? Nếu nó làm được điều này, người ta thực sự cần phải chắc chắn rằng nó làm được. Nó không phải là điều bạn hy vọng tôi ưu hoa biên dịch chọn lên trên (như nội tuyến mà có thể hoặc có thể không làm việc)
Toad

6
@hogan: Tôi đứng sửa lại. Bạn nói đúng rằng Gcc và MSVC đều thực hiện tối ưu hóa đệ quy đuôi.
Toad

5
Ví dụ này không phải là đệ quy đuôi vì nó không phải là lệnh gọi đệ quy cuối cùng, đó là phép nhân.
Brian Young

4

Đừng làm đi làm lại cùng một công việc!

Một phản vật chất phổ biến mà tôi thấy đi dọc theo những dòng sau:

void Function()
{
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomething();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingElse();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingCool();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingReallyNeat();
   MySingleton::GetInstance()->GetAggregatedObject()->DoSomethingYetAgain();
}

Trình biên dịch thực sự phải gọi tất cả các hàm đó mọi lúc. Giả sử bạn, lập trình viên, biết rằng đối tượng tổng hợp không thay đổi trong quá trình thực hiện các lệnh gọi này, vì tình yêu của tất cả những gì thánh thiện ...

void Function()
{
   MySingleton* s = MySingleton::GetInstance();
   AggregatedObject* ao = s->GetAggregatedObject();
   ao->DoSomething();
   ao->DoSomethingElse();
   ao->DoSomethingCool();
   ao->DoSomethingReallyNeat();
   ao->DoSomethingYetAgain();
}

Trong trường hợp của singleton getter, các cuộc gọi có thể không quá tốn kém, nhưng nó chắc chắn là một chi phí (thông thường, "kiểm tra xem đối tượng đã được tạo chưa, nếu chưa, hãy tạo, sau đó trả lại). chuỗi cửa ngõ này trở nên phức tạp hơn, chúng ta sẽ có nhiều thời gian lãng phí hơn.


3
  1. Sử dụng phạm vi cục bộ nhất có thể cho tất cả các khai báo biến.

  2. Sử dụng constbất cứ khi nào có thể

  3. Không sử dụng đăng ký trừ khi bạn định lập hồ sơ cả khi có và không có nó

2 đầu tiên trong số này, đặc biệt là # 1 giúp trình tối ưu hóa phân tích mã. Nó đặc biệt sẽ giúp nó đưa ra lựa chọn tốt về những biến nào cần giữ trong thanh ghi.

Việc sử dụng từ khóa register một cách mù quáng có khả năng giúp ích nhiều hơn cho việc tối ưu hóa của bạn, Thật khó để biết điều gì sẽ quan trọng cho đến khi bạn nhìn vào đầu ra hoặc hồ sơ lắp ráp.

Có những thứ khác quan trọng để đạt được hiệu suất tốt ngoài mã; thiết kế cấu trúc dữ liệu của bạn để tối đa hóa đồng tiền bộ nhớ cache. Nhưng câu hỏi là về trình tối ưu hóa.



3

Tôi đã được nhắc nhở về một điều mà tôi đã gặp phải một lần, trong đó triệu chứng đơn giản là chúng tôi sắp hết bộ nhớ, nhưng kết quả là hiệu suất tăng lên đáng kể (cũng như giảm đáng kể dung lượng bộ nhớ).

Vấn đề trong trường hợp này là phần mềm chúng tôi đang sử dụng có rất ít phân bổ. Giống như, phân bổ bốn byte ở đây, sáu byte ở đó, v.v. Rất nhiều đối tượng nhỏ cũng chạy trong phạm vi 8-12 byte. Vấn đề không phải là chương trình cần rất nhiều thứ nhỏ, mà là nó đã phân bổ rất nhiều thứ nhỏ riêng lẻ, khiến mỗi phân bổ tăng lên (trên nền tảng cụ thể này) 32 byte.

Một phần của giải pháp là tập hợp một nhóm đối tượng nhỏ kiểu Alexandrescu, nhưng mở rộng nó để tôi có thể phân bổ các mảng các đối tượng nhỏ cũng như các mục riêng lẻ. Điều này cũng giúp ích rất nhiều cho hiệu suất vì nhiều mục hơn nằm trong bộ nhớ cache cùng một lúc.

Một phần khác của giải pháp là thay thế việc sử dụng tràn lan các thành viên char * được quản lý thủ công bằng một chuỗi SSO (tối ưu hóa chuỗi nhỏ). Phân bổ tối thiểu là 32 byte, tôi đã xây dựng một lớp chuỗi có bộ đệm 28 ký tự được nhúng phía sau ký tự *, vì vậy 95% chuỗi của chúng tôi không cần thực hiện phân bổ bổ sung (và sau đó tôi đã thay thế thủ công hầu hết mọi giao diện của char * trong thư viện này với lớp học mới này, điều đó có vui hay không). Điều này cũng giúp ích rất nhiều cho việc phân mảnh bộ nhớ, sau đó tăng vị trí tham chiếu cho các đối tượng trỏ tới khác và tương tự như vậy, hiệu suất cũng tăng lên.


3

Một kỹ thuật gọn gàng mà tôi học được từ @MSalters nhận xét về câu trả lời này cho phép trình biên dịch thực hiện sao chép ngay cả khi trả về các đối tượng khác nhau theo một số điều kiện:

// before
BigObject a, b;
if(condition)
  return a;
else
  return b;

// after
BigObject a, b;
if(condition)
  swap(a,b);
return a;

2

Nếu bạn có các chức năng nhỏ mà bạn gọi nhiều lần, thì trước đây tôi đã có được lợi nhuận lớn bằng cách đặt chúng trong tiêu đề là "nội tuyến tĩnh". Các lệnh gọi hàm trên ix86 đắt một cách đáng ngạc nhiên.

Việc thực hiện lại các hàm đệ quy theo cách không đệ quy bằng cách sử dụng một ngăn xếp rõ ràng cũng có thể thu được rất nhiều, nhưng khi đó bạn thực sự đang ở trong lĩnh vực của thời gian phát triển so với lợi ích.


Chuyển đổi đệ quy thành một ngăn xếp là một tối ưu hóa giả định trên ompf.org, dành cho những người đang phát triển raytracer và viết các thuật toán kết xuất khác.
Tom

... Tôi nên thêm vào điều này, rằng chi phí lớn nhất trong dự án raytracer cá nhân của tôi là đệ quy dựa trên vtable thông qua hệ thống phân cấp khối lượng giới hạn bằng cách sử dụng mẫu Composite. Nó thực sự chỉ là một loạt các hộp lồng nhau có cấu trúc như một cây, nhưng việc sử dụng mẫu này gây ra sự cồng kềnh dữ liệu (con trỏ bảng ảo) và giảm đồng thời lệnh (những gì có thể là một vòng lặp nhỏ / chặt chẽ bây giờ là một chuỗi các lệnh gọi hàm)
Tom

2

Đây là lời khuyên tối ưu hóa thứ hai của tôi. Như với lời khuyên đầu tiên của tôi, đây là mục đích chung, không phải ngôn ngữ hoặc bộ xử lý cụ thể.

Đọc kỹ hướng dẫn sử dụng trình biên dịch và hiểu những gì nó nói với bạn. Sử dụng trình biên dịch một cách tối đa.

Tôi đồng ý với một hoặc hai trong số những người trả lời khác đã xác định việc chọn thuật toán phù hợp là rất quan trọng để loại bỏ hiệu suất của một chương trình. Ngoài ra, tỷ lệ hoàn vốn (được đo bằng sự cải thiện khả năng thực thi mã) trên thời gian bạn đầu tư vào việc sử dụng trình biên dịch cao hơn nhiều so với tỷ lệ hoàn vốn trong việc tinh chỉnh mã.

Đúng vậy, những người viết trình biên dịch không đến từ một cuộc đua của những gã khổng lồ về mã hóa và những trình biên dịch có chứa những sai lầm và theo hướng dẫn sử dụng và theo lý thuyết trình biên dịch thì điều gì phải làm cho mọi thứ nhanh hơn đôi khi lại khiến mọi thứ chậm hơn. Đó là lý do tại sao bạn phải thực hiện từng bước một và đo lường hiệu suất trước và sau khi tinh chỉnh.

Và vâng, cuối cùng, bạn có thể phải đối mặt với sự bùng nổ tổ hợp của các cờ trình biên dịch, vì vậy bạn cần có một hoặc hai tập lệnh để chạy với các cờ trình biên dịch khác nhau, xếp hàng các công việc trên cụm lớn và thu thập thống kê thời gian chạy. Nếu đó chỉ là bạn và Visual Studio trên PC, bạn sẽ hết hứng thú trước khi thử đủ cách kết hợp đủ các cờ trình biên dịch.

Trân trọng

dấu

Khi tôi chọn một đoạn mã lần đầu tiên, tôi thường có thể nhận được hệ số hiệu suất cao hơn 1,4 - 2,0 lần (tức là phiên bản mới của mã chạy bằng 1 / 1,4 hoặc 1/2 thời gian của phiên bản cũ) trong một ngày hoặc hai ngày bằng cách mày mò với các cờ trình biên dịch. Đúng vậy, đó có thể là một nhận xét về sự thiếu hiểu biết về trình biên dịch của các nhà khoa học, những người bắt nguồn phần lớn mã mà tôi làm việc, hơn là một dấu hiệu cho thấy sự xuất sắc của tôi. Để đặt cờ trình biên dịch thành tối đa (và hiếm khi chỉ là -O3), có thể mất hàng tháng làm việc chăm chỉ để có được một hệ số khác là 1,05 hoặc 1,1


2

Khi DEC ra mắt bộ xử lý alpha, có khuyến nghị giữ số lượng đối số cho một hàm dưới 7, vì trình biên dịch sẽ luôn cố gắng tự động đặt tối đa 6 đối số trong thanh ghi.


bit x86-64 cũng cho phép rất nhiều tham số đăng ký được truyền, có thể có ảnh hưởng đáng kể đến chi phí gọi hàm.
Tom

1

Đối với hiệu suất, trước tiên hãy tập trung vào việc viết mã có thể bảo trì - được thành phần hóa, ghép nối lỏng lẻo, v.v., vì vậy khi bạn phải tách một phần để viết lại, tối ưu hóa hoặc đơn giản hóa hồ sơ, bạn có thể làm điều đó mà không cần nỗ lực nhiều.

Trình tối ưu hóa sẽ giúp hiệu suất chương trình của bạn một chút.


3
Điều đó chỉ hoạt động nếu bản thân các "giao diện" ghép nối có thể điều chỉnh để tối ưu hóa. Một giao diện có thể "chậm" vốn có, ví dụ như bằng cách buộc các tra cứu hoặc tính toán dư thừa hoặc buộc truy cập bộ nhớ cache kém.
Tom

1

Bạn nhận được câu trả lời tốt ở đây, nhưng họ cho rằng chương trình của bạn đã khá gần đến mức tối ưu để bắt đầu và bạn nói

Giả sử rằng chương trình đã được viết chính xác, được biên dịch với sự tối ưu hóa đầy đủ, đã được thử nghiệm và đưa vào sản xuất.

Theo kinh nghiệm của tôi, một chương trình có thể được viết đúng, nhưng điều đó không có nghĩa là nó gần như tối ưu. Phải làm việc thêm để đạt được điểm đó.

Nếu tôi có thể đưa ra một ví dụ, câu trả lời này cho thấy một chương trình trông hoàn toàn hợp lý đã được thực hiện nhanh hơn 40 lần bằng cách tối ưu hóa macro . Tốc độ lớn không thể được thực hiện trong mọi kinh nghiệm của tôi, chương trình như lần đầu tiên được viết, nhưng trong nhiều chương trình (ngoại trừ các chương trình rất nhỏ), nó có thể.

Sau khi hoàn thành, việc tối ưu hóa vi mô (của các điểm nóng) có thể mang lại cho bạn một khoản lợi nhuận tốt.


1

tôi sử dụng trình biên dịch intel. trên cả Windows và Linux.

khi ít nhiều hoàn thành, tôi lập hồ sơ mã. sau đó treo vào các điểm phát sóng và cố gắng thay đổi mã để cho phép trình biên dịch thực hiện công việc tốt hơn.

nếu mã là mã tính toán và chứa nhiều vòng lặp - báo cáo vectơ hóa trong trình biên dịch intel rất hữu ích - hãy tìm 'vec-report' để được trợ giúp.

vì vậy ý ​​tưởng chính - đánh bóng mã quan trọng về hiệu suất. đối với phần còn lại - ưu tiên là chính xác và có thể bảo trì - các chức năng ngắn, mã rõ ràng có thể hiểu được 1 năm sau.


Bạn sắp trả lời được câu hỏi ..... bạn làm những việc gì với mã, để giúp trình biên dịch có thể thực hiện những loại tối ưu hóa đó?
EvilTeach

1
Cố gắng viết nhiều hơn trong C-style (so với C ++), ví dụ như tránh các hàm ảo mà thực sự cần thiết, đặc biệt nếu chúng sẽ được gọi thường xuyên, tránh AddRefs .. và tất cả những thứ hay ho (một lần nữa trừ khi nó thực sự cần thiết). Viết mã dễ dàng cho nội dòng - ít tham số hơn, ít "if" -s hơn. Không sử dụng các biến toàn cục trừ khi thực sự cần thiết. Trong cấu trúc dữ liệu - đặt các trường rộng hơn trước (double, int64 đi trước int) - vì vậy trình biên dịch căn chỉnh struct trên kích thước tự nhiên của trường đầu tiên - căn chỉnh tốt cho hiệu suất.
jf.

1
Bố cục và truy cập dữ liệu là hoàn toàn quan trọng đối với hiệu suất. Vì vậy, sau khi lập hồ sơ - đôi khi tôi chia một cấu trúc thành một số cấu trúc sau vị trí truy cập. Một mẹo chung nữa - sử dụng int hoặc size-t so với char - ngay cả các giá trị dữ liệu cũng nhỏ - tránh các lỗi khác nhau. hình phạt lưu trữ để chặn tải, các vấn đề với các quầy hàng đăng ký một phần. tất nhiên điều này không áp dụng khi cần những mảng dữ liệu lớn như vậy.
jf.

Thêm một - tránh các cuộc gọi hệ thống, trừ khi có nhu cầu thực sự :) - chúng RẤT đắt
jf.

2
@jf: Tôi đã +1 câu trả lời của bạn, nhưng bạn có thể chuyển câu trả lời từ phần nhận xét sang phần trả lời được không? Nó sẽ dễ dàng hơn để đọc.
kriss

1

Một tối ưu hóa mà tôi đã sử dụng trong C ++ là tạo một phương thức khởi tạo không làm gì cả. Người ta phải gọi thủ công một init () để đưa đối tượng vào trạng thái hoạt động.

Điều này có lợi trong trường hợp tôi cần một vector lớn của các lớp này.

Tôi gọi Reserve () để phân bổ không gian cho vectơ, nhưng hàm tạo không thực sự chạm vào trang bộ nhớ mà đối tượng đang ở trên. Vì vậy, tôi đã dành một số không gian địa chỉ, nhưng không thực sự tiêu tốn nhiều bộ nhớ vật lý. Tôi tránh các lỗi trang liên quan đến chi phí xây dựng liên quan.

Khi tôi tạo các đối tượng để lấp đầy vectơ, tôi đặt chúng bằng init (). Điều này hạn chế tổng số lỗi trang của tôi và tránh phải thay đổi kích thước () vectơ trong khi điền nó.


6
Tôi tin rằng cách triển khai điển hình của std :: vector không thực sự tạo ra nhiều đối tượng hơn khi bạn dự trữ () nhiều dung lượng hơn. Nó chỉ phân bổ các trang. Các hàm tạo được gọi sau đó, sử dụng vị trí mới, khi bạn thực sự thêm các đối tượng vào vectơ - tức là (có lẽ) ngay trước khi bạn gọi init (), vì vậy bạn không thực sự cần hàm init () riêng biệt. Cũng nên nhớ rằng ngay cả khi phương thức khởi tạo của bạn "trống" trong mã nguồn, phương thức khởi tạo đã biên dịch có thể chứa mã để khởi tạo những thứ như bảng ảo và RTTI, vì vậy dù sao thì các trang cũng được chạm vào lúc xây dựng.
Wyzard

1
Vâng. Trong trường hợp của chúng tôi, chúng tôi sử dụng push_back để điền vectơ. Các đối tượng không có bất kỳ chức năng ảo nào, vì vậy nó không phải là một vấn đề. Lần đầu tiên chúng tôi thử nó với constructor, chúng tôi đã rất ngạc nhiên về số lượng lỗi trang. Tôi nhận ra điều gì đã xảy ra, và chúng tôi đã giật dây người xây dựng, và vấn đề lỗi trang đã biến mất.
EvilTeach

Điều đó làm tôi ngạc nhiên. Bạn đang sử dụng triển khai C ++ và STL nào?
David Thornley

3
Tôi đồng ý với những người khác, điều này có vẻ như là một triển khai không tốt của std :: vector. Ngay cả khi các đối tượng của bạn có vtables, chúng sẽ không được xây dựng cho đến khi bạn push_back. Bạn có thể kiểm tra điều này bằng cách khai báo hàm tạo mặc định là riêng tư, vì tất cả các vectơ sẽ cần là hàm tạo bản sao cho push_back.
Tom

1
@David - Việc triển khai trên AIX.
EvilTeach

1

Một điều tôi đã làm là cố gắng giữ các hành động đắt tiền ở những nơi mà người dùng có thể mong đợi chương trình chậm trễ một chút. Hiệu suất tổng thể liên quan đến khả năng phản hồi, nhưng không hoàn toàn giống nhau và đối với nhiều thứ, khả năng phản hồi là phần quan trọng hơn của hiệu suất.

Lần cuối cùng tôi thực sự phải cải thiện hiệu suất tổng thể, tôi đã để mắt đến các thuật toán chưa tối ưu và tìm kiếm những nơi có khả năng gặp sự cố về bộ nhớ cache. Tôi đã lập hồ sơ và đo lường hiệu suất trước tiên, và một lần nữa sau mỗi lần thay đổi. Sau đó công ty sụp đổ, nhưng dù sao đây cũng là công việc thú vị và mang tính hướng dẫn.


0

Tôi đã nghi ngờ từ lâu, nhưng chưa bao giờ chứng minh rằng việc khai báo các mảng sao cho chúng có lũy thừa là 2, là số phần tử, cho phép trình tối ưu hóa thực hiện giảm độ mạnh bằng cách thay thế một nhân bằng một dịch chuyển với một số bit, khi tra cứu các yếu tố riêng lẻ.


6
Điều đó đã từng là sự thật, ngày nay nó đã trở thành sự thật. Trên thực tế, điều hoàn toàn ngược lại là đúng. Nếu bạn khai báo các mảng của mình với lũy thừa là hai, bạn sẽ rất có thể gặp phải tình huống rằng bạn làm việc trên hai con trỏ có lũy thừa là hai trong bộ nhớ. Vấn đề là, bộ nhớ đệm của CPU được tổ chức giống như vậy và bạn có thể kết thúc với việc hai mảng chiến đấu xung quanh một dòng bộ nhớ cache. Bạn có được hiệu suất khủng khiếp theo cách đó. Có một trong những con trỏ phía trước một vài byte (ví dụ: không phải lũy thừa của hai) ngăn chặn tình trạng này.
Nils Pipenbrinck

+1 Nils và một lần xuất hiện cụ thể của điều này là "răng cưa 64k" trên phần cứng Intel.
Tom

Nhân tiện, đây là thứ dễ bị bác bỏ khi nhìn vào phần tháo rời. Tôi đã rất ngạc nhiên, cách đây nhiều năm, khi thấy cách gcc sẽ tối ưu hóa tất cả các loại phép nhân không đổi với sự thay đổi và cộng gộp. Ví dụ: val * 7biến thành những gì sẽ trông như thế nào khác (val << 3) - val.
dash-tom-bang

0

Đặt các hàm nhỏ và / hoặc thường được gọi ở đầu tệp nguồn. Điều đó giúp trình biên dịch dễ dàng tìm thấy cơ hội cho nội tuyến hơn.


Có thật không? Bạn có thể trích dẫn cơ sở lý luận và ví dụ cho điều này không? Không nói nó không đúng sự thật, chỉ là nghe có vẻ khó hiểu rằng vị trí sẽ quan trọng.
underscore_d

@underscore_d nó không thể nội dòng một cái gì đó cho đến khi định nghĩa hàm được biết. Mặc dù các trình biên dịch hiện đại có thể thực hiện nhiều lần để định nghĩa được biết đến tại thời điểm tạo mã, tôi không cho là vậy.
Mark Ransom

Tôi đã giả định rằng các trình biên dịch làm việc với các đồ thị cuộc gọi trừu tượng hơn là thứ tự hàm vật lý, có nghĩa là điều này sẽ không thành vấn đề. Chắc chắn, tôi cho rằng không có hại gì khi phải cẩn thận hơn - đặc biệt là khi, sang một bên về hiệu suất, IMO có vẻ hợp lý hơn khi xác định các hàm được gọi trước những hàm gọi chúng. Tôi sẽ phải kiểm tra hiệu suất nhưng sẽ ngạc nhiên nếu nó quan trọng, nhưng cho đến lúc đó, tôi vẫn ngạc nhiên!
underscore_d
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.