Trong C ++, tôi có nên bận tâm đến các biến bộ nhớ cache, hay để trình biên dịch thực hiện việc tối ưu hóa? (Răng cưa)


114

Hãy xem xét đoạn mã sau ( pthuộc loại unsigned char*bitmap->widththuộc một số loại số nguyên, chính xác là không xác định và phụ thuộc vào phiên bản nào của một số thư viện bên ngoài mà chúng tôi đang sử dụng):

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Tối ưu nó có đáng không [..]

Có thể có trường hợp nào mà điều này có thể mang lại kết quả hiệu quả hơn bằng cách viết:

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

... hay điều này là tầm thường để trình biên dịch tối ưu hóa?

Bạn sẽ coi điều gì là mã "tốt hơn"?

Lưu ý từ người biên tập (Ike): đối với những người thắc mắc về văn bản gạch ngang, câu hỏi ban đầu, như đã được diễn giải, rất gần với lãnh thổ lạc đề và rất gần với việc bị đóng mặc dù có phản hồi tích cực. Những thứ này đã bị loại bỏ. Tuy nhiên, vui lòng không trừng phạt những người trả lời đã giải quyết những phần khó khăn này của câu hỏi.


19
Nếu *pthuộc cùng loại với widththì việc tối ưu hóa không phải là chuyện nhỏ, vì pcó thể trỏ đến widthvà sửa đổi nó bên trong vòng lặp.
emlai

31
Hỏi về việc liệu trình biên dịch có tối ưu hóa một hoạt động cụ thể hay không thường là câu hỏi sai. Điều cuối cùng bạn (thường) quan tâm là phiên bản nào chạy nhanh hơn, bạn chỉ cần đo lường.
SirGuy

4
@GuyGreer Tôi đồng ý, mặc dù tôi muốn nói rằng câu hỏi là hay, hoặc ít nhất là thú vị, tôi nghĩ không may câu trả lời là "bạn phải đo lường nó, theo từng trường hợp sử dụng". Lý do là chức năng là di động nhưng hiệu suất thì không. Vì vậy, nó thực sự phụ thuộc vào mọi phần của quá trình xây dựng, bắt đầu trên trình biên dịch và kết thúc tại trang đích (kết hợp os / phần cứng). Và tất nhiên dự đoán tốt nhất là trình biên dịch thông minh hơn con người ở việc này.
luk32

19
Nếu tôi là một trình biên dịch, tôi sẽ thấy rằng hai ví dụ của bạn không giống nhau. Nó có thể ptrỏ đến cùng một bộ nhớ như bitmap->width. Vì vậy, tôi không thể tối ưu hóa hợp pháp ví dụ đầu tiên thành ví dụ thứ hai.
Mysticial

4
"P" được lưu trữ ở đâu? Tôi gợi ý rằng bạn có thể giành được một chiến thắng hiệu suất thực sự lớn bằng cách làm một cái gì đó như "char * limit p2 = p;" và sau đó sử dụng "p2" thay vì "p" trong vòng lặp của bạn. Sau đó, nếu bạn muốn các thay đổi đối với "p2" được áp dụng trở lại p, hãy sử dụng "p + = (p2-p);". Lưu ý rằng không có con trỏ nào được viết trong thời gian tồn tại của p2 bởi một con trỏ không được sao chép ở dạng p2 có thể được đọc bằng con trỏ được sao chép từ p2 và ngược lại, và không có bản sao của p2 nào có thể được sử dụng cho bất kỳ mục đích nào sau thời gian tồn tại của p2, nhưng trình biên dịch có thể sử dụng những sự kiện để cho phép tối ưu hóa mà không thể thực hiện được bằng bất kỳ phương tiện nào khác.
supercat

Câu trả lời:


81

Thoạt nhìn, tôi nghĩ rằng trình biên dịch có thể tạo lắp ráp tương đương cho cả hai phiên bản với cờ tối ưu hóa được kích hoạt. Khi tôi kiểm tra nó, tôi đã rất ngạc nhiên khi thấy kết quả:

Nguồn unoptimized.cpp

lưu ý: mã này không có nghĩa là được thực thi.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    for (unsigned x = 0 ; x < static_cast<unsigned>(bitmap.width) ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Nguồn optimized.cpp

lưu ý: mã này không có nghĩa là được thực thi.

struct bitmap_t
{
    long long width;
} bitmap;

int main(int argc, char** argv)
{
    const unsigned width = static_cast<unsigned>(bitmap.width);
    for (unsigned x = 0 ; x < width ; ++x)
    {
        argv[x][0] = '\0';
    }
    return 0;
}

Tổng hợp

  • $ g++ -s -O3 unoptimized.cpp
  • $ g++ -s -O3 optimized.cpp

Assembly (chưa tối ưu hóa.s)

    .file   "unoptimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    mov %eax, %edx
    addl    $1, %eax
    movq    (%rsi,%rdx,8), %rdx
    movb    $0, (%rdx)
    cmpl    bitmap(%rip), %eax
    jb  .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

Assembly (tối ưu hóa.s)

    .file   "optimized.cpp"
    .text
    .p2align 4,,15
.globl main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    .cfi_personality 0x3,__gxx_personality_v0
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
    subl    $1, %eax
    leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
.L3:
    movq    (%rsi,%rax), %rdx
    addq    $8, %rax
    cmpq    %rcx, %rax
    movb    $0, (%rdx)
    jne .L3
.L2:
    xorl    %eax, %eax
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
.globl bitmap
    .bss
    .align 8
    .type   bitmap, @object
    .size   bitmap, 8
bitmap:
    .zero   8
    .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-16)"
    .section    .note.GNU-stack,"",@progbits

khác biệt

$ diff -uN unoptimized.s optimized.s
--- unoptimized.s   2015-11-24 16:11:55.837922223 +0000
+++ optimized.s 2015-11-24 16:12:02.628922941 +0000
@@ -1,4 +1,4 @@
-   .file   "unoptimized.cpp"
+   .file   "optimized.cpp"
    .text
    .p2align 4,,15
 .globl main
@@ -10,16 +10,17 @@
    movl    bitmap(%rip), %eax
    testl   %eax, %eax
    je  .L2
+   subl    $1, %eax
+   leaq    8(,%rax,8), %rcx
    xorl    %eax, %eax
    .p2align 4,,10
    .p2align 3
 .L3:
-   mov %eax, %edx
-   addl    $1, %eax
-   movq    (%rsi,%rdx,8), %rdx
+   movq    (%rsi,%rax), %rdx
+   addq    $8, %rax
+   cmpq    %rcx, %rax
    movb    $0, (%rdx)
-   cmpl    bitmap(%rip), %eax
-   jb  .L3
+   jne .L3
 .L2:
    xorl    %eax, %eax
    ret

Hợp ngữ được tạo cho phiên bản được tối ưu hóa thực sự tải ( lea) widthhằng số không giống như phiên bản chưa được tối ưu hóa để tính toán độ widthlệch ở mỗi lần lặp (movq ).

Khi tôi có thời gian, cuối cùng tôi sẽ đăng một số điểm chuẩn về điều đó. Câu hỏi hay.


3
Sẽ rất thú vị khi xem mã có được tạo theo cách khác hay không nếu bạn truyền tới const unsignedthay vì chỉ unsignedtrong trường hợp không được tối ưu hóa.
Mark Ransom

2
@MarkRansom Tôi đoán nó không nên tạo sự khác biệt: Những "lời hứa" trở const là chỉ trong sự so sánh đơn lẻ, không phải cho toàn bộ vòng lặp
Hagen von Eitzen

13
Vui lòng KHÔNG BAO GIỜ sử dụng chức năng mainđể kiểm tra tối ưu hóa. Gcc cố tình đánh dấu nó là lạnh và do đó vô hiệu hóa một số tối ưu hóa cho nó. Tôi không biết có phải như vậy ở đây không, nhưng đó là một thói quen quan trọng cần có.
Marc Glisse

3
@MarcGlisse Bạn đúng 100%. Tôi đã viết vội, tôi sẽ cải thiện điều đó.
YSC

3
Đây là liên kết đến cả hai chức năng trong một đơn vị biên dịch trên Godbolt , giả sử bitmaplà toàn cầu. Phiên bản không phải CSEd sử dụng toán hạng bộ nhớ cmp, đây không phải là vấn đề đối với hiệu suất trong trường hợp này. Nếu nó là một local, trình biên dịch có thể cho rằng các con trỏ khác không thể "biết về" nó và trỏ vào nó. Không phải là một ý tưởng tồi khi lưu trữ các biểu thức liên quan đến các khối cầu trong các biến tạm thời, miễn là nó cải thiện (hoặc không ảnh hưởng đến) khả năng đọc hoặc nếu hiệu suất là rất quan trọng. Trừ khi có nhiều chuyện xảy ra, những người dân địa phương như vậy thường có thể chỉ sống trong sổ đăng ký và không bao giờ bị đổ.
Peter Cordes

38

Thực sự không có đủ thông tin từ đoạn mã của bạn để có thể cho biết và một điều mà tôi có thể nghĩ đến là răng cưa. Theo quan điểm của chúng tôi, rõ ràng là bạn không muốn pbitmaptrỏ đến cùng một vị trí trong bộ nhớ, nhưng trình biên dịch không biết điều đó và (vì pthuộc loại char*) trình biên dịch phải làm cho mã này hoạt động ngay cả khi pbitmapchồng lên nhau.

Điều này có nghĩa là trong trường hợp này nếu vòng lặp thay đổi bitmap->widththông qua con trỏ pthì điều đó phải được nhìn thấy khi đọc lại bitmap->widthsau này, điều này có nghĩa là lưu trữ nó trong một biến cục bộ sẽ là bất hợp pháp.

Điều đó đang được nói, tôi tin rằng một số trình biên dịch đôi khi thực sự sẽ tạo ra hai phiên bản của cùng một mã (tôi đã thấy bằng chứng cụ thể về điều này, nhưng chưa bao giờ trực tiếp tìm kiếm thông tin về những gì trình biên dịch đang làm trong trường hợp này) và nhanh chóng kiểm tra xem các con trỏ bí danh và chạy mã nhanh hơn nếu nó xác định là ổn.

Điều đó đang được nói, tôi đứng về nhận xét của mình về việc chỉ đơn giản là đo hiệu suất của hai phiên bản, tiền của tôi là không thấy bất kỳ sự khác biệt hiệu suất nhất quán nào giữa hai phiên bản mã.

Theo tôi, những câu hỏi như thế này là ổn nếu mục đích của bạn là tìm hiểu về các lý thuyết và kỹ thuật tối ưu hóa trình biên dịch, nhưng thật lãng phí thời gian (một sự tối ưu hóa vi mô vô ích) nếu mục tiêu cuối cùng của bạn ở đây là làm cho chương trình chạy nhanh hơn.


1
@GuyGreer: Nó là một trình chặn tối ưu hóa chính; Tôi cho rằng điều đáng tiếc là các quy tắc ngôn ngữ tập trung vào các quy tắc về các loại hiệu quả, thay vì xác định các tình huống mà việc viết và đọc các mục khác nhau được hoặc không có câu trả lời. Các quy tắc được viết bằng thuật ngữ như vậy có thể đáp ứng tốt hơn nhiều nhu cầu của trình biên dịch và lập trình viên so với các quy tắc hiện tại.
supercat

3
@GuyGreer - không phải một restrictbộ định tính là câu trả lời cho vấn đề răng cưa trong trường hợp này sao?
LThode

4
Theo kinh nghiệm của tôi, restrictphần lớn là hit-and-miss. MSVC là trình biên dịch duy nhất mà tôi đã thấy dường như làm điều đó đúng cách. ICC mất thông tin răng cưa thông qua các lệnh gọi hàm ngay cả khi chúng được nội tuyến. Và GCC thường không nhận được bất kỳ lợi ích nào trừ khi bạn khai báo mọi tham số đầu vào là restrict(bao gồm cả thiscác hàm thành viên).
Mysticial

1
@Mystical: Một điều cần nhớ là charbí danh tất cả các loại, vì vậy nếu bạn có ký tự * thì bạn phải sử dụng restricttrên mọi thứ. Hoặc nếu bạn đã buộc tắt các quy tắc -fno-strict-aliasingbí danh nghiêm ngặt của GCC thì mọi thứ được coi là bí danh khả thi.
Zan Lynx

1
@Ray Đề xuất gần đây nhất cho restrictngữ nghĩa-như trong C ++ là N4150 .
TC

24

Ok, các bạn, vì vậy tôi đã đo, với GCC -O3(sử dụng GCC 4.9 trên Linux x64).

Hóa ra, phiên bản thứ hai chạy nhanh hơn 54%!

Vì vậy, tôi đoán răng cưa là thứ, tôi đã không nghĩ về nó.

[Biên tập]

Tôi đã thử lại phiên bản đầu tiên với tất cả các con trỏ được xác định bằng __restrict__và kết quả giống nhau. Kỳ lạ .. Răng cưa không phải là vấn đề, hoặc vì lý do nào đó, trình biên dịch không tối ưu hóa nó tốt ngay cả với__restrict__ .

[Chỉnh sửa 2]

Ok, tôi nghĩ rằng tôi đã có thể chứng minh rằng răng cưa là vấn đề. Tôi đã lặp lại thử nghiệm ban đầu của mình, lần này bằng cách sử dụng một mảng thay vì một con trỏ:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Và được đo (phải sử dụng "-mcmodel = large" để liên kết nó). Sau đó, tôi đã thử:

const std::size_t n = 0x80000000ull;
bitmap->width = n;
static unsigned char d[n*3];
std::size_t i=0;
unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x < width;  ++x)
{
    d[i++] = 0xAA;
    d[i++] = 0xBB;
    d[i++] = 0xCC;
}

Các kết quả đo đều giống nhau - Có vẻ như trình biên dịch đã có thể tự tối ưu hóa nó.

Sau đó, tôi đã thử các mã gốc (với một con trỏ p), lần này plà loại std::uint16_t*. Một lần nữa, kết quả vẫn giống nhau - do răng cưa nghiêm ngặt. Sau đó, tôi đã thử xây dựng bằng "-fno-precision-aliasing" và một lần nữa lại thấy sự khác biệt về thời gian.


4
Đây có vẻ như là một nhận xét, mặc dù về mặt kỹ thuật nó trả lời câu hỏi. Cũng lưu ý, rất tiếc là bạn đã không chứng minh được rằng răng cưa là thứ. Nó có vẻ khả dĩ, chắc chắn là hợp lý, nhưng điều đó khác với kết luận rằng đó là nó.
SirGuy

@GuyGreer: Xem [chỉnh sửa 2] của tôi - bây giờ tôi nghĩ nó đã được chứng minh khá nhiều.
Yaron Cohen-Tal

2
Tôi chỉ tự hỏi tại sao bạn lại bắt đầu sử dụng biến "i" khi bạn có "x" trong vòng lặp của mình?
Jesper Madsen

1
Có phải tôi chỉ thấy cụm từ khó hiểu nhanh hơn 54% không? Ý bạn là tốc độ của nó gấp 1,54 lần so với tốc độ chưa được tối ưu hóa hay cái gì khác?
Roddy

3
@ YaronCohen-Tal nhanh hơn gấp đôi? Ấn tượng, nhưng không phải những gì tôi đã hiểu "nhanh hơn 54%" có nghĩa là!
Roddy

24

Các câu trả lời khác đã chỉ ra rằng việc đưa thao tác con trỏ ra khỏi vòng lặp có thể thay đổi hành vi đã xác định do các quy tắc răng cưa cho phép char thành bí danh bất kỳ thứ gì và do đó không phải là một tối ưu hóa được phép cho trình biên dịch mặc dù trong hầu hết các trường hợp, nó rõ ràng là chính xác đối với con người. người lập trình.

Họ cũng đã chỉ ra rằng việc nâng hoạt động ra khỏi vòng lặp thường nhưng không phải lúc nào cũng là một cải tiến từ quan điểm hiệu suất và thường là tiêu cực từ quan điểm dễ đọc.

Tôi muốn chỉ ra rằng thường có một "cách thứ ba". Thay vì đếm đến số lần lặp lại bạn muốn, bạn có thể đếm ngược đến không. Điều này có nghĩa là số lần lặp chỉ cần thiết một lần khi bắt đầu vòng lặp, nó không phải được lưu trữ sau đó. Vẫn tốt hơn ở cấp trình hợp ngữ, nó thường loại bỏ sự cần thiết phải so sánh rõ ràng vì hoạt động giảm thường sẽ đặt các cờ cho biết liệu bộ đếm có bằng không cả trước (cờ mang) và sau (cờ không) giảm.

for (unsigned x = static_cast<unsigned>(bitmap->width);x > 0;  x--)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Lưu ý rằng phiên bản này của vòng lặp cung cấp giá trị x trong phạm vi 1.. width thay vì phạm vi 0 .. (width-1). Điều đó không quan trọng trong trường hợp của bạn vì bạn không thực sự sử dụng x cho bất cứ điều gì nhưng đó là điều cần lưu ý. Nếu bạn muốn một vòng lặp đếm ngược với các giá trị x trong phạm vi 0 .. (width-1) bạn có thể làm.

for (unsigned x = static_cast<unsigned>(bitmap->width); x-- > 0;)
{
    *p++ = 0xAA;
    *p++ = 0xBB;
    *p++ = 0xCC;
}

Bạn cũng có thể loại bỏ các phôi trong các ví dụ trên nếu bạn muốn mà không cần lo lắng về việc nó ảnh hưởng đến các quy tắc so sánh vì tất cả những gì bạn đang làm với bitmap-> width là gán nó trực tiếp cho một biến.


2
Tôi đã thấy trường hợp thứ hai được định dạng là x --> 0, dẫn đến toán tử "downto". Khá vui. Tái bút Tôi không coi việc tạo một biến cho điều kiện cuối là một biến âm cho khả năng đọc được, nó thực sự có thể ngược lại.
Mark Ransom

Nó thực sự phụ thuộc, đôi khi một câu lệnh trở nên kinh khủng đến mức chia nó thành nhiều câu lệnh sẽ cải thiện khả năng đọc nhưng tôi không tin đó là trường hợp ở đây.
plugwash

1
+1 Quan sát tốt, mặc dù tôi sẽ tranh luận rằng việc nâng static_cast<unsigned>(bitmap->width)và sử dụng widththay thế trong vòng lặp thực sự là một cải tiến cho khả năng đọc bởi vì bây giờ có ít thứ hơn để người đọc phân tích cú pháp trên mỗi dòng. Tuy nhiên, quan điểm của những người khác có thể khác nhau.
SirGuy

1
Có nhiều tình huống khác mà việc đếm từ dưới lên cao hơn (ví dụ khi xóa các mục khỏi danh sách). Tôi không biết tại sao điều này không được thực hiện thường xuyên hơn.
Ian Goldby

3
Nếu bạn muốn viết các vòng lặp giống asm tối ưu hơn, hãy sử dụng do { } while(), vì trong ASM bạn tạo các vòng lặp với một nhánh có điều kiện ở cuối. Các vòng lặp for(){}while(){}vòng lặp thông thường yêu cầu thêm hướng dẫn để kiểm tra điều kiện vòng lặp một lần trước vòng lặp, nếu trình biên dịch không thể chứng minh rằng nó luôn chạy ít nhất một lần. Bằng mọi cách, hãy sử dụng for()hoặc while()khi hữu ích để kiểm tra xem vòng lặp có nên chạy một lần hay khi nó dễ đọc hơn.
Peter Cordes

11

Điều duy nhất ở đây có thể ngăn chặn việc tối ưu hóa là quy tắc răng cưa nghiêm ngặt . Tóm lại :

"Bí danh nghiêm ngặt là một giả định, được thực hiện bởi trình biên dịch C (hoặc C ++), rằng các con trỏ tham chiếu đến các đối tượng thuộc các kiểu khác nhau sẽ không bao giờ tham chiếu đến cùng một vị trí bộ nhớ (tức là các bí danh khác nhau.)"

[…]

Ngoại lệ đối với quy tắc là a char*, được phép trỏ đến bất kỳ loại nào.

Ngoại lệ cũng áp dụng cho unsignedsigned char con trỏ.

Đây là trường hợp trong mã của bạn: Bạn đang sửa đổi *pthông qua pđó là một unsigned char*, vì vậy trình biên dịch phải giả định rằng nó có thể trỏ đến bitmap->width. Do đó, bộ nhớ đệm của bitmap->widthlà một tối ưu hóa không hợp lệ. Hành vi ngăn chặn tối ưu hóa này được thể hiện trong câu trả lời của YSC .

Nếu và chỉ khi được ptrỏ đến không phải charvà không phải decltype(bitmap->width)loại, thì bộ nhớ đệm có thể là một tối ưu hóa khả thi.


10

Câu hỏi ban đầu được hỏi:

Nó có giá trị tối ưu hóa nó không?

Và câu trả lời của tôi cho điều đó (thu được sự kết hợp tốt của cả phiếu bầu chọn lên và xuống ..)

Hãy để trình biên dịch lo lắng về nó.

Trình biên dịch gần như chắc chắn sẽ làm tốt hơn bạn. Và không có gì đảm bảo rằng 'tối ưu hóa' của bạn tốt hơn bất kỳ mã 'hiển nhiên' nào - bạn đã đo lường nó chưa ??

Quan trọng hơn, bạn có bất kỳ bằng chứng nào cho thấy mã bạn đang tối ưu hóa có bất kỳ tác động nào đến hiệu suất chương trình của bạn không?

Bất chấp những phản đối (và bây giờ đang gặp vấn đề về răng cưa), tôi vẫn hài lòng với đó là câu trả lời hợp lệ. Nếu bạn không biết liệu nó có đáng để tối ưu hóa thứ gì đó hay không, thì có lẽ là không.

Tất nhiên, một câu hỏi khá khác sẽ là:

Làm cách nào để biết liệu nó có đáng để tối ưu hóa một đoạn mã hay không?

Đầu tiên, ứng dụng hoặc thư viện của bạn có cần chạy nhanh hơn hiện tại không? Người dùng có tiếp tục chờ đợi quá lâu không? Phần mềm của bạn có dự báo thời tiết ngày hôm qua thay vì ngày mai không?

Chỉ bạn mới thực sự có thể nói điều này, dựa trên phần mềm của bạn dùng để làm gì và người dùng của bạn mong đợi gì.

Giả sử phần mềm của bạn cần một số tối ưu hóa, điều tiếp theo cần làm là bắt đầu đo lường. Người làm hồ sơ sẽ cho bạn biết mã của bạn dành thời gian ở đâu. Nếu mảnh vỡ của bạn không hiển thị như một nút cổ chai, tốt nhất là bạn nên để yên. Máy định danh và các công cụ đo lường khác cũng sẽ cho bạn biết liệu các thay đổi của bạn có tạo ra sự khác biệt hay không. Có thể dành hàng giờ đồng hồ để tối ưu hóa mã, chỉ để thấy rằng bạn không tạo ra sự khác biệt rõ ràng nào.

Dù sao thì bạn hiểu 'tối ưu hóa' là gì?

Nếu bạn không viết mã 'tối ưu hóa', thì mã của bạn phải rõ ràng, gọn gàng và ngắn gọn nhất có thể. Lập luận "Tối ưu hóa sớm là xấu" không phải là lời bào chữa cho mã cẩu thả hoặc không hiệu quả.

Mã tối ưu hóa thường hy sinh một số thuộc tính ở trên cho hiệu suất. Nó có thể liên quan đến việc giới thiệu các biến cục bộ bổ sung, có các đối tượng có phạm vi rộng hơn dự kiến ​​hoặc thậm chí đảo ngược thứ tự vòng lặp bình thường. Tất cả những điều này có thể ít rõ ràng hoặc ngắn gọn hơn, vì vậy hãy ghi lại mã (ngắn gọn!) Về lý do bạn làm điều này.

Nhưng thông thường, với mã 'chậm', những tối ưu hóa vi mô này là phương sách cuối cùng. Nơi đầu tiên cần xem xét là các thuật toán và cấu trúc dữ liệu. Có cách nào để tránh thực hiện công việc không? Có thể thay thế tìm kiếm tuyến tính bằng tìm kiếm nhị phân không? Ở đây một danh sách được liên kết có nhanh hơn một vectơ không? Hay một bảng băm? Tôi có thể lưu kết quả vào bộ nhớ cache không? Việc đưa ra các quyết định 'hiệu quả' ở đây thường có thể ảnh hưởng đến hiệu suất theo một mức độ lớn hoặc hơn!


12
Khi bạn đang lặp qua chiều rộng của hình ảnh bitmap, logic lặp có thể chiếm một phần đáng kể thời gian dành cho vòng lặp. Thay vì lo lắng về việc tối ưu hóa quá sớm, tốt hơn hết trong trường hợp này là phát triển các phương pháp hay nhất hiệu quả ngay từ đầu.
Mark Ransom

4
@MarkRansom đã đồng ý một phần: Nhưng "phương pháp hay nhất" sẽ là: sử dụng thư viện hoặc lệnh gọi API hiện có để lấp đầy hình ảnh hoặc b: yêu cầu GPU làm việc đó cho bạn. Nó không bao giờ nên là loại tối ưu hóa vi mô không đo được mà OP đề xuất. Và làm thế nào để bạn biết mã này đã từng được thực thi nhiều lần, hoặc với các ảnh bitmap lớn hơn 16 pixel ...?
Roddy

@Veedrac. Đánh giá cao sự biện minh cho -1. Lực đẩy của câu hỏi đã thay đổi một cách tinh tế và đáng kể kể từ khi tôi trả lời. Nếu bạn cho rằng câu trả lời (mở rộng) vẫn không hữu ích, thì đã đến lúc tôi xóa nó ... "Có đáng không ..." luôn chủ yếu dựa trên ý kiến.
Roddy

@Roddy Tôi đánh giá cao các chỉnh sửa, họ giúp đỡ (và bình luận của tôi có lẽ nghe quá khắc nghiệt). Mặc dù vậy, tôi vẫn đang ở trong hàng rào, vì đây thực sự là câu trả lời cho một câu hỏi không phù hợp với Stack Overflow. Có vẻ như một câu trả lời thích hợp sẽ dành riêng cho đoạn trích, như các câu trả lời được bình chọn cao ở đây.
Veedrac

6

Tôi sử dụng mẫu sau trong tình huống như thế này. Nó gần như ngắn gọn như trường hợp đầu tiên của bạn và tốt hơn trường hợp thứ hai, bởi vì nó giữ biến tạm thời cục bộ cho vòng lặp.

for (unsigned int x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
{
  *p++ = 0xAA;
  *p++ = 0xBB;
  *p++ = 0xCC;
}

Điều này sẽ nhanh hơn với một trình biên dịch nhỏ hơn thông minh, bản dựng gỡ lỗi hoặc một số cờ biên dịch nhất định.

Chỉnh sửa1 : Đặt một hoạt động liên tục bên ngoài vòng lặp là một mẫu lập trình tốt . Nó cho thấy sự hiểu biết cơ bản về hoạt động của máy, đặc biệt là trong C / C ++. Tôi cho rằng nỗ lực chứng tỏ bản thân nên dành cho những người không tuân theo thông lệ này. Nếu trình biên dịch trừng phạt đối với một mẫu tốt, đó là một lỗi trong trình biên dịch.

Edit2:: Tôi đã đo lường đề xuất của mình so với mã gốc trên vs2013, đã cải thiện% 1. Chúng ta có thể làm tốt hơn không? Tối ưu hóa thủ công đơn giản giúp cải thiện 3 lần so với vòng lặp ban đầu trên máy x64 mà không cần sử dụng đến các hướng dẫn kỳ lạ. Đoạn mã dưới đây giả định hệ thống endian nhỏ và bitmap được căn chỉnh đúng cách. TEST 0 là bản gốc (9 giây), TEST 1 nhanh hơn (3 giây). Tôi cá là ai đó có thể làm điều này nhanh hơn nữa và kết quả của bài kiểm tra sẽ phụ thuộc vào kích thước của bitmap. Chắc chắn trong tương lai, trình biên dịch sẽ có thể tạo ra mã nhanh nhất một cách nhất quán. Tôi e rằng đây sẽ là tương lai khi trình biên dịch cũng sẽ là AI của lập trình viên, vì vậy chúng tôi sẽ mất việc. Nhưng hiện tại, chỉ cần viết mã cho thấy rằng bạn biết rằng thao tác bổ sung trong vòng lặp là không cần thiết.

#include <memory>
#include <time.h>

struct Bitmap_line
{
  int blah;
  unsigned int width;
  Bitmap_line(unsigned int w)
  {
    blah = 0;
    width = w;
  }
};

#define TEST 0 //define 1 for faster test

int main(int argc, char* argv[])
{
  unsigned int size = (4 * 1024 * 1024) / 3 * 3; //makes it divisible by 3
  unsigned char* pointer = (unsigned char*)malloc(size);
  memset(pointer, 0, size);
  std::unique_ptr<Bitmap_line> bitmap(new Bitmap_line(size / 3));
  clock_t told = clock();
#if TEST == 0
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    for (unsigned x = 0; x < static_cast<unsigned>(bitmap->width); ++x)
    //for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#else
  for (int iter = 0; iter < 10000; iter++)
  {
    unsigned char* p = pointer;
    unsigned x = 0;
    for (const unsigned n = static_cast<unsigned>(bitmap->width) - 4; x < n; x += 4)
    {
      *(int64_t*)p = 0xBBAACCBBAACCBBAALL;
      p += 8;
      *(int32_t*)p = 0xCCBBAACC;
      p += 4;
    }

    for (const unsigned n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      *p++ = 0xAA;
      *p++ = 0xBB;
      *p++ = 0xCC;
    }
  }
#endif
  double ms = 1000.0 * double(clock() - told) / CLOCKS_PER_SEC;
  printf("time %0.3f\n", ms);

  {
    //verify
    unsigned char* p = pointer;
    for (unsigned x = 0, n = static_cast<unsigned>(bitmap->width); x < n; ++x)
    {
      if ((*p++ != 0xAA) || (*p++ != 0xBB) || (*p++ != 0xCC))
      {
        printf("EEEEEEEEEEEEERRRRORRRR!!!\n");
        abort();
      }
    }
  }

  return 0;
}

Bạn có thể tiết kiệm 25% khác trên 64bit nếu Bạn sử dụng ba int64_t thay vì int64_t và int32_t.
Antonín Lejsek

5

Có hai điều cần xem xét.

A) Tối ưu hóa sẽ chạy bao lâu một lần?

Nếu câu trả lời không thường xuyên, như chỉ khi người dùng nhấp vào một nút, thì đừng bận tâm nếu nó khiến mã của bạn không thể đọc được. Nếu câu trả lời là 1000 lần một giây thì có thể bạn sẽ muốn thực hiện tối ưu hóa. Nếu nó thậm chí là một chút phức tạp, hãy chắc chắn đưa ra một bình luận để giải thích những gì đang xảy ra để giúp anh chàng tiếp theo đi cùng.

B) Điều này có làm cho mã khó bảo trì / khắc phục sự cố không?

Nếu bạn không thấy hiệu suất tăng đáng kể thì việc làm cho mã của bạn trở nên khó hiểu chỉ để tiết kiệm một vài tích tắc đồng hồ không phải là một ý kiến ​​hay. Rất nhiều người sẽ nói với bạn rằng bất kỳ lập trình viên giỏi nào cũng có thể xem mã và tìm ra điều gì đang xảy ra. Đây là sự thật. Vấn đề là trong thế giới kinh doanh, việc tính toán thêm thời gian sẽ tốn tiền. Vì vậy, nếu bạn có thể làm cho nó đẹp hơn để đọc thì hãy làm điều đó. Bạn bè của bạn sẽ cảm ơn bạn vì nó.

Điều đó nói rằng cá nhân tôi muốn sử dụng ví dụ B.


4

Trình biên dịch có thể tối ưu hóa rất nhiều thứ. Đối với ví dụ của bạn, bạn nên xem xét khả năng đọc, khả năng tin cậy và những gì tuân theo tiêu chuẩn mã của bạn. Để biết thêm thông tin về những gì có thể được tối ưu hóa (với GCC), hãy xem bài đăng trên blog này .


4

Theo nguyên tắc chung, hãy để trình biên dịch thực hiện việc tối ưu hóa cho bạn, cho đến khi bạn xác định rằng bạn nên tiếp quản. Logic của điều này không liên quan gì đến hiệu suất, mà là khả năng đọc của con người. Trong bao la đa số trường hợp, khả năng đọc chương trình của bạn là quan trọng hơn hiệu quả của nó. Bạn nên đặt mục tiêu viết mã dễ đọc hơn cho con người và sau đó chỉ lo lắng về việc tối ưu hóa khi bạn tin rằng hiệu suất quan trọng hơn khả năng bảo trì mã của bạn.

Khi bạn thấy rằng hiệu suất quan trọng, bạn nên chạy một trình mô tả về mã để xác định những vòng lặp nào đang hoạt động kém hiệu quả và tối ưu hóa những vòng lặp đó một cách riêng lẻ. Thực sự có thể có những trường hợp bạn muốn thực hiện việc tối ưu hóa đó (đặc biệt nếu bạn chuyển sang C ++, nơi mà các vùng chứa STL có liên quan), nhưng chi phí về khả năng đọc là rất lớn.

Ngoài ra, tôi có thể nghĩ đến các tình huống bệnh lý mà nó thực sự có thể làm chậm mã. Ví dụ, hãy xem xét trường hợp trình biên dịch không thể chứng minh điều đó bitmap->widthlà không đổi trong suốt quá trình. Bằng cách thêm widthbiến, bạn buộc trình biên dịch duy trì một biến cục bộ trong phạm vi đó. Nếu, vì một số lý do cụ thể của nền tảng, biến bổ sung đó ngăn cản một số tối ưu hóa không gian ngăn xếp, nó có thể phải tổ chức lại cách nó phát ra các mã byte và tạo ra thứ gì đó kém hiệu quả hơn.

Ví dụ: trên Windows x64, người ta có nghĩa vụ gọi một lệnh gọi API đặc biệt, __chkstktrong phần mở đầu của hàm nếu hàm sẽ sử dụng nhiều hơn 1 trang biến cục bộ. Chức năng này cho phép các cửa sổ quản lý các trang bảo vệ mà chúng sử dụng để mở rộng ngăn xếp khi cần thiết. Nếu biến phụ của bạn đẩy mức sử dụng ngăn xếp lên từ dưới 1 trang lên bằng hoặc cao hơn 1 trang, thì hàm của bạn bây giờ có nghĩa vụ gọi __chkstkmỗi khi nó được nhập. Nếu bạn tối ưu hóa vòng lặp này trên một con đường chậm, bạn thực sự có thể làm chậm con đường nhanh hơn nhiều hơn những gì bạn đã lưu trên con đường chậm!

Chắc chắn, nó hơi bệnh hoạn, nhưng điểm của ví dụ đó là bạn thực sự có thể làm chậm trình biên dịch. Nó chỉ cho thấy rằng bạn phải lập hồ sơ công việc của mình để xác định vị trí tối ưu hóa. Trong thời gian này, vui lòng không hy sinh khả năng đọc theo bất kỳ cách nào để tối ưu hóa có thể có hoặc có thể không quan trọng.


4
Tôi ước C và C ++ sẽ cung cấp nhiều cách hơn để xác định rõ ràng những thứ mà lập trình viên không quan tâm. Chúng không chỉ cung cấp nhiều cơ hội hơn cho các trình biên dịch để tối ưu hóa mọi thứ, mà còn giúp các lập trình viên khác đọc mã khỏi phải đoán xem liệu nó có thể kiểm tra lại bitmap-> width mỗi lần để đảm bảo rằng các thay đổi đối với nó ảnh hưởng đến vòng lặp hay không, hoặc cho dù nó có thể là bitmap-> độ rộng trong bộ nhớ đệm để đảm bảo rằng những thay đổi đối với nó không ảnh hưởng đến vòng lặp. Có một phương tiện để nói "Cache this or not - Tôi không quan tâm" sẽ làm rõ lý do cho sự lựa chọn của lập trình viên.
supercat

@supercat Tôi hết lòng đồng ý, vì người ta có thể thấy nếu ai đó nhìn vào đống ngôn ngữ thất bại rách nát mà tôi đã tìm cách viết để giải quyết vấn đề này. Tôi nhận thấy rất khó để định nghĩa "cái gì" mà ai đó không quan tâm nếu không có quá nhiều cú pháp vô duyên đến mức nó không đáng. Tôi tiếp tục tìm kiếm trong vô vọng.
Cort Ammon

Không thể xác định nó trong mọi trường hợp, nhưng tôi nghĩ rằng có rất nhiều trường hợp mà hệ thống loại có thể giúp ích. Chính vì vậy C đã quyết định đặt các loại ký tự là "bộ truy cập phổ quát" thay vì có bộ định loại kiểu lỏng hơn một chút so với "dễ bay hơi" có thể được áp dụng cho bất kỳ loại nào , với ngữ nghĩa mà các truy cập thuộc các loại như vậy sẽ được xử lý theo trình tự với các truy cập của loại tương đương không đủ điều kiện và cũng có quyền truy cập của tất cả các loại biến có cùng định tính đó. Điều đó sẽ giúp làm rõ liệu ai bị sử dụng các loại vật vì người ta cần những ...
supercat

... hành vi răng cưa, hoặc liệu một người có đang sử dụng chúng vì chúng có kích thước phù hợp với nhu cầu của một người hay không. Nó cũng sẽ hữu ích nếu có các rào cản răng cưa giải thích mà trong nhiều trường hợp có thể được đặt bên ngoài các vòng lặp, không giống như các rào cản ngầm liên quan đến truy cập kiểu ký tự.
supercat

1
Đây là một cuộc nói chuyện khôn ngoan, nhưng nói chung, nếu bạn đã chọn C cho nhiệm vụ của mình, có lẽ hiệu suất là rất quan trọng và các quy tắc khác nhau nên áp dụng. Nếu không thì tốt hơn nên sử dụng Ruby, Java, Python hoặc những thứ tương tự.
Audrius Meskauskas

4

Các so sánh là sai kể từ khi hai đoạn mã

for (unsigned x = 0;  x < static_cast<unsigned>(bitmap->width);  ++x)

unsigned width(static_cast<unsigned>(bitmap->width));
for (unsigned x = 0;  x<width ;  ++x)

không tương đương

Trong trường hợp đầu tiên widthlà phụ thuộc và không phải const, và người ta không thể giả định rằng nó có thể không thay đổi giữa các lần lặp tiếp theo. Vì vậy, nó không thể được tối ưu hóa, mà phải được kiểm tra ở mọi vòng lặp .

Trong trường hợp được tối ưu hóa của bạn, một biến cục bộ được gán giá trị bitmap->widthtại một số thời điểm trong quá trình thực thi chương trình. Trình biên dịch có thể xác minh rằng điều này không thực sự thay đổi.

Bạn đã nghĩ đến đa luồng hay giá trị có thể bị phụ thuộc vào bên ngoài để giá trị của nó dễ thay đổi. Làm thế nào người ta mong đợi trình biên dịch tìm ra tất cả những điều này nếu bạn không nói?

Trình biên dịch chỉ có thể làm tốt khi mã của bạn cho phép nó.


2

Trừ khi bạn biết chính xác cách trình biên dịch tối ưu hóa mã, tốt hơn là bạn nên tự tối ưu hóa bằng cách giữ cho mã dễ đọc và thiết kế. Trên thực tế, rất khó để kiểm tra mã lắp ráp cho mọi chức năng chúng tôi viết cho các phiên bản trình biên dịch mới.


1

Trình biên dịch không thể tối ưu hóa bitmap->widthvì giá trị của widthcó thể được thay đổi giữa các lần lặp. Có một số lý do phổ biến nhất:

  1. Đa luồng. Trình biên dịch không thể dự đoán nếu luồng khác sắp thay đổi giá trị.
  2. Sửa đổi vòng lặp bên trong, đôi khi không đơn giản để biết liệu biến có bị thay đổi bên trong vòng lặp hay không.
  3. Đó là lời gọi hàm, ví dụ iterator::end(), container::size()thật khó để dự đoán liệu nó có luôn trả về cùng một kết quả hay không.

Tóm lại (ý kiến ​​cá nhân của tôi) đối với những nơi yêu cầu mức độ tối ưu hóa cao bạn cần tự làm điều đó, những nơi khác cứ để vậy, trình biên dịch có thể tối ưu hóa nó hoặc không, nếu không có sự khác biệt lớn thì khả năng đọc mã là mục tiêu chính.

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.