Tránh toán tử gia tăng Postfix


25

Tôi đã đọc rằng tôi nên tránh toán tử gia tăng hậu tố vì lý do hiệu suất (trong một số trường hợp nhất định).

Nhưng điều này không ảnh hưởng đến khả năng đọc mã? Theo ý kiến ​​của tôi:

for(int i = 0; i < 42; i++);
    /* i will never equal 42! */

Có vẻ tốt hơn:

for(int i = 0; i < 42; ++i);
    /* i will never equal 42! */

Nhưng điều này có lẽ chỉ là thói quen. Phải thừa nhận rằng, tôi chưa thấy sử dụng nhiều ++i.

Là hiệu suất xấu để hy sinh khả năng đọc, trong trường hợp này? Hay tôi chỉ bị mù, và ++idễ đọc hơn i++?


1
Tôi đã sử dụng i++trước khi tôi biết nó có thể ảnh hưởng đến hiệu suất hơn ++i, vì vậy tôi đã chuyển đổi. Lúc đầu trông có vẻ hơi lạ, nhưng sau một thời gian tôi đã quen với nó và bây giờ nó cảm thấy tự nhiên như i++.
gablin

15
++ii++làm những việc khác nhau trong những bối cảnh nhất định, đừng cho rằng chúng giống nhau.
Orble

2
Đây là về C hay C ++? Chúng là hai ngôn ngữ rất khác nhau! :-) Trong C ++, vòng lặp for thành ngữ là for (type i = 0; i != 42; ++i). Không chỉ có thể operator++bị quá tải, mà còn có thể operator!=operator<. Gia tăng tiền tố không đắt hơn postfix, không bằng không đắt hơn ít hơn. Những cái nào chúng ta nên sử dụng?
Bo Persson

7
Không nên gọi nó là ++ C?
Armand

21
@Stephen: C ++ có nghĩa là lấy C, thêm vào nó, và sau đó sử dụng cái cũ .
supercat

Câu trả lời:


58

Sự thật:

  1. i ++ và ++ tôi đều dễ đọc như nhau. Bạn không thích nó bởi vì bạn không quen với nó, nhưng về cơ bản không có gì bạn có thể hiểu sai về nó, vì vậy không còn phải đọc hay viết nữa.

  2. Trong ít nhất một số trường hợp, toán tử postfix sẽ kém hiệu quả hơn.

  3. Tuy nhiên, trong 99,99% trường hợp, điều đó không thành vấn đề bởi vì (a) nó sẽ hoạt động theo kiểu đơn giản hoặc nguyên thủy và nó chỉ là vấn đề nếu sao chép một vật thể lớn (b) nó sẽ không hoạt động phần quan trọng của mã (c) bạn không biết trình biên dịch sẽ tối ưu hóa nó hay không, nó có thể làm được.

  4. Vì vậy, tôi khuyên bạn nên sử dụng tiền tố trừ khi bạn đặc biệt cần hậu tố là một thói quen tốt để tham gia, chỉ vì (a) đó là thói quen tốt để chính xác với những thứ khác và (b) một lần trong trăng xanh bạn sẽ có ý định sử dụng tiền tố và hiểu sai về cách làm tròn: nếu bạn luôn viết những gì bạn muốn nói, điều đó ít có khả năng. Luôn có sự đánh đổi giữa hiệu suất và tối ưu hóa.

Bạn nên sử dụng ý thức chung của bạn và không tối ưu hóa vi mô cho đến khi bạn cần, nhưng không phải là không hiệu quả vì lợi ích của nó. Thông thường, điều này có nghĩa là: trước tiên, loại trừ bất kỳ cấu trúc mã nào không hiệu quả ngay cả trong mã không quan trọng về thời gian (thông thường một cái gì đó đại diện cho một lỗi khái niệm cơ bản, như chuyển các đối tượng 500MB theo giá trị mà không có lý do); và thứ hai, trong mọi cách viết mã khác, hãy chọn cách rõ ràng nhất.

Tuy nhiên, ở đây, tôi tin rằng câu trả lời rất đơn giản: Tôi tin rằng viết tiền tố trừ khi bạn đặc biệt cần postfix là (a) rất rõ ràng hơn và (b) rất có khả năng hiệu quả hơn, vì vậy bạn nên luôn viết nó theo mặc định, nhưng không lo lắng về nó nếu bạn quên

Sáu tháng trước, tôi cũng nghĩ giống như bạn, rằng i ++ tự nhiên hơn, nhưng đó hoàn toàn là những gì bạn đã quen.

EDIT 1: Scott Meyers, trong "C ++ hiệu quả hơn", người mà tôi thường tin tưởng vào điều này, nói chung bạn nên tránh sử dụng toán tử postfix trên các loại do người dùng xác định (bởi vì việc triển khai duy nhất của hàm tăng hậu tố là để thực hiện sao chép đối tượng, gọi hàm tăng tiền tố để thực hiện tăng và trả về bản sao, nhưng thao tác sao chép có thể tốn kém).

Vì vậy, chúng tôi không biết liệu có bất kỳ quy tắc chung nào về (a) liệu điều đó có đúng ngày hôm nay hay không, (b) liệu nó có áp dụng (ít hơn) cho các loại nội tại (c) hay không, liệu bạn có nên sử dụng "++" hay không bất cứ điều gì nhiều hơn một lớp lặp nhẹ bao giờ. Nhưng với tất cả các lý do tôi đã mô tả ở trên, điều đó không thành vấn đề, hãy làm những gì tôi đã nói trước đó.

EDIT 2: Điều này đề cập đến thực hành chung. Nếu bạn nghĩ nó KHÔNG quan trọng trong một số trường hợp cụ thể, thì bạn nên lập hồ sơ và xem. Hồ sơ là dễ dàng và giá rẻ và làm việc. Trích từ các nguyên tắc đầu tiên, những gì cần được tối ưu hóa là khó khăn và tốn kém và không hoạt động.


Bài viết của bạn là đúng trên tiền. Trong các biểu thức trong đó toán tử infix + và tăng sau ++ đã bị quá tải, chẳng hạn như aClassInst = someOtherClassInst + yetAotherClassInst ++, trình phân tích cú pháp sẽ tạo mã để thực hiện thao tác phụ gia trước khi tạo mã để thực hiện thao tác tăng sau tạo một bản sao tạm thời. Kẻ giết người hiệu suất ở đây không phải là tăng sau. Đó là việc sử dụng một toán tử infix quá tải. Toán tử Infix tạo ra các thể hiện mới.
bit-twiddler

2
Tôi rất nghi ngờ rằng lý do mọi người 'quen' i++hơn ++ilà vì tên của một ngôn ngữ lập trình phổ biến nhất định được tham chiếu trong câu hỏi / câu trả lời này ...
Shadow

61

Luôn luôn mã cho lập trình viên đầu tiên và thứ hai máy tính.

Nếu có sự khác biệt về hiệu năng, sau khi trình biên dịch đã đưa ra chuyên gia về mã của bạn, bạn có thể đo nó nó quan trọng - thì bạn có thể thay đổi nó.


7
Tuyên bố SIÊU !!!
Dave

8
@Martin: đó chính xác là lý do tại sao tôi sử dụng gia tăng tiền tố. Ngữ nghĩa Postfix ngụ ý giữ giá trị cũ xung quanh và nếu không cần nó, thì việc sử dụng nó là không chính xác.
Matthieu M.

1
Đối với một chỉ số vòng lặp sẽ rõ ràng hơn - nhưng nếu bạn đang lặp lại một mảng bằng cách tăng một con trỏ và sử dụng tiền tố có nghĩa là bắt đầu tại một địa chỉ bất hợp pháp trước khi bắt đầu sẽ không quan tâm đến việc tăng hiệu suất
Martin Beckett

5
@Matthew: Đơn giản là không đúng khi tăng sau ngụ ý giữ một bản sao của giá trị cũ xung quanh. Người ta không thể chắc chắn làm thế nào một trình biên dịch xử lý các giá trị trung gian cho đến khi một trình xem đầu ra của nó. Nếu bạn dành thời gian để xem danh sách ngôn ngữ lắp ráp GCC được chú thích của tôi, bạn sẽ thấy GCC tạo cùng một mã máy cho cả hai vòng. Điều vô lý này về việc ủng hộ tăng trước so với tăng sau vì nó hiệu quả hơn ít hơn so với phỏng đoán.
bit-twiddler

2
@Mathhieu: Mã mà tôi đã đăng được tạo với tối ưu hóa đã tắt. Đặc tả C ++ không nói rõ rằng trình biên dịch phải tạo ra một thể hiện tạm thời của một giá trị khi sử dụng tăng sau. Nó chỉ nêu lên sự ưu tiên của các toán tử trước và sau tăng.
bit-twiddler

13

GCC tạo mã máy giống nhau cho cả hai vòng.

Mã C

int main(int argc, char** argv)
{
    for (int i = 0; i < 42; i++)
            printf("i = %d\n",i);

    for (int i = 0; i < 42; ++i)
        printf("i = %d\n",i);

    return 0;
}

Mã hội (với ý kiến ​​của tôi)

    cstring
LC0:
    .ascii "i = %d\12\0"
    .text
.globl _main
_main:
    pushl   %ebp
    movl    %esp, %ebp
    pushl   %ebx
    subl    $36, %esp
    call    L9
"L00000000001$pb":
L9:
    popl    %ebx
    movl    $0, -16(%ebp)  // -16(%ebp) is "i" for the first loop 
    jmp L2
L3:
    movl    -16(%ebp), %eax   // move i for the first loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub    // call printf
    leal    -16(%ebp), %eax  // make the eax register point to i
    incl    (%eax)           // increment i
L2:
    cmpl    $41, -16(%ebp)  // compare i to the number 41
    jle L3              // jump to L3 if less than or equal to 41
    movl    $0, -12(%ebp)   // -12(%ebp) is "i" for the second loop  
    jmp L5
L6:
    movl    -12(%ebp), %eax   // move i for the second loop to the eax register 
    movl    %eax, 4(%esp)     // push i onto the stack
    leal    LC0-"L00000000001$pb"(%ebx), %eax // load the effective address of the format string into the eax register
    movl    %eax, (%esp)      // push the address of the format string onto the stack
    call    L_printf$stub     // call printf
    leal    -12(%ebp), %eax  // make eax point to i
    incl    (%eax)           // increment i
L5:
    cmpl    $41, -12(%ebp)   // compare i to 41 
    jle L6               // jump to L6 if less than or equal to 41
    movl    $0, %eax
    addl    $36, %esp
    popl    %ebx
    leave
    ret
    .section __IMPORT,__jump_table,symbol_stubs,self_modifying_code+pure_instructions,5
L_printf$stub:
    .indirect_symbol _printf
    hlt ; hlt ; hlt ; hlt ; hlt
    .subsections_via_symbols

Làm thế nào về tối ưu hóa bật?
serv-inc

2
@user: Có lẽ không có thay đổi, nhưng bạn có thực sự mong đợi bit-twiddler quay lại bất cứ lúc nào không?
Ded repeatator

2
Cẩn thận: Mặc dù trong C không có loại do người dùng xác định có toán tử bị quá tải, nhưng trong C ++ thì có, và việc khái quát hóa từ loại cơ bản sang loại do người dùng xác định đơn giản là không hợp lệ .
Ded repeatator

@Ded repeatator: Cảm ơn bạn, cũng đã chỉ ra rằng câu trả lời này không khái quát cho các loại do người dùng xác định. Tôi đã không nhìn vào trang người dùng của anh ấy trước khi hỏi.
tớ-inc

12

Đừng lo lắng về hiệu suất, hãy nói 97% thời gian. Tối ưu hóa sớm là gốc rễ của mọi tội lỗi.

- Donald Knuth

Bây giờ đây là ra khỏi con đường của chúng ta, chúng ta hãy làm cho sự lựa chọn của chúng tôi sanely :

  • ++i: tăng tiền tố , tăng giá trị hiện tại và mang lại kết quả
  • i++: tăng tiền tố , sao chép giá trị, tăng giá trị hiện tại, mang lại bản sao

Trừ khi một bản sao của giá trị cũ là bắt buộc, sử dụng gia tăng hậu tố là cách hoàn thành công việc.

Sự không chính xác xuất phát từ sự lười biếng, luôn sử dụng cấu trúc thể hiện ý định của bạn theo cách trực tiếp nhất, có ít cơ hội hơn người bảo trì trong tương lai có thể hiểu sai ý định ban đầu của bạn.

Mặc dù nó (thực sự) nhỏ ở đây, nhưng có những lúc tôi thực sự bối rối khi đọc mã: Tôi thực sự tự hỏi liệu ý định và biểu hiện thực tế có trùng khớp hay không, và dĩ nhiên, sau vài tháng, họ (hoặc tôi) cũng không nhớ ...

Vì vậy, nó không quan trọng cho dù nó có phù hợp với bạn hay không. Ôm KISS . Trong một vài tháng, bạn sẽ tránh xa các thực hành cũ của bạn.


4

Trong C ++, bạn có thể tạo ra sự khác biệt đáng kể về hiệu năng nếu có quá tải toán tử liên quan, đặc biệt là nếu bạn đang viết mã templated và không biết các trình lặp có thể được truyền vào. Logic đằng sau bất kỳ trình lặp X nào cũng có thể đáng kể và quan trọng- đó là, chậm và không thể tối ưu hóa bởi trình biên dịch.

Nhưng đây không phải là trường hợp trong C, nơi bạn biết nó sẽ chỉ là một loại tầm thường, và sự khác biệt hiệu suất là tầm thường và trình biên dịch có thể dễ dàng tối ưu hóa đi.

Vì vậy, một mẹo: Bạn lập trình bằng C, hoặc bằng C ++ và các câu hỏi liên quan đến cái này hay cái kia, không phải cả hai.


2

Hiệu suất của một trong hai hoạt động phụ thuộc nhiều vào kiến ​​trúc cơ bản. Người ta phải tăng một giá trị được lưu trữ trong bộ nhớ, điều đó có nghĩa là nút cổ chai von Neumann là yếu tố giới hạn trong cả hai trường hợp.

Trong trường hợp của ++ i, chúng ta phải

Fetch i from memory 
Increment i
Store i back to memory
Use i

Trong trường hợp của i ++, chúng ta phải

Fetch i from memory
Use i
Increment i
Store i back to memory

Các toán tử ++ và - theo dõi nguồn gốc của chúng theo tập lệnh PDP-11. PDP-11 có thể thực hiện tăng sau tự động trên một thanh ghi. Nó cũng có thể thực hiện tự động giảm trước trên một địa chỉ hiệu quả có trong một thanh ghi. Trong cả hai trường hợp, trình biên dịch chỉ có thể tận dụng các hoạt động ở cấp độ máy này nếu biến trong câu hỏi là biến "đăng ký".


2

Nếu bạn muốn biết nếu một cái gì đó chậm, hãy kiểm tra nó. Lấy một BigInteger hoặc tương đương, gắn nó vào một vòng lặp tương tự cho cả hai thành ngữ, đảm bảo bên trong vòng lặp không được tối ưu hóa và thời gian cả hai.

Đọc xong bài báo, tôi không thấy nó quá thuyết phục, vì ba lý do. Thứ nhất, trình biên dịch sẽ có thể tối ưu hóa xung quanh việc tạo ra một đối tượng không bao giờ được sử dụng. Thứ hai, i++khái niệm này là thành ngữ cho số cho các vòng lặp , vì vậy các trường hợp tôi có thể thấy thực sự bị ảnh hưởng bị giới hạn. Ba, họ cung cấp một lập luận lý thuyết thuần túy, không có con số để sao lưu nó.

Dựa trên lý do số 1 đặc biệt, tôi đoán là khi bạn thực sự làm đúng thời điểm, chúng sẽ ở ngay cạnh nhau.


-1

Trước hết, nó không ảnh hưởng đến khả năng đọc IMO. Nó không phải là những gì bạn từng thấy nhưng nó sẽ chỉ là một thời gian ngắn trước khi bạn quen với nó.

Thứ hai trừ khi bạn sử dụng một tấn toán tử postfix trong mã của mình, bạn có thể sẽ không thấy nhiều sự khác biệt. Đối số chính cho việc không sử dụng chúng khi có thể là một bản sao của giá trị var gốc phải được giữ cho đến khi kết thúc các đối số mà var gốc vẫn có thể được sử dụng. Đó là 32 bit hoặc 64 bit tùy theo kiến ​​trúc. Điều đó tương đương với 4 hoặc 8 byte hoặc 0,00390625 hoặc 0,0078125 MB. Cơ hội rất cao, trừ khi bạn đang sử dụng một tấn trong số chúng cần được lưu trong một khoảng thời gian rất dài mà với tài nguyên và tốc độ máy tính ngày nay, bạn thậm chí sẽ không nhận thấy sự khác biệt khi chuyển từ tiền tố sang tiền tố.

EDIT: Quên phần còn lại này vì kết luận của tôi đã được chứng minh là sai (ngoại trừ phần ++ i và i ++ không phải lúc nào cũng làm điều tương tự ... điều đó vẫn đúng).

Ngoài ra, nó đã được chỉ ra trước đó rằng họ không làm điều tương tự trong một trường hợp. Hãy cẩn thận về việc thực hiện chuyển đổi nếu bạn quyết định. Tôi chưa bao giờ dùng thử (Tôi luôn sử dụng postfix) vì vậy tôi không biết chắc nhưng tôi nghĩ việc thay đổi từ postfix sang prefix sẽ dẫn đến kết quả khác nhau: (một lần nữa tôi có thể sai ... phụ thuộc vào trình biên dịch / thông dịch viên quá)

for (int i=0; i < 10; i++) //the set of i values here will be {0,1,2,3,4,5,6,7,8,9}
for (int i=0; i < 10; ++i) //the set of i values here will be {1,2,3,4,5,6,7,8,9,10}

4
Hoạt động gia tăng xảy ra ở cuối vòng lặp for, vì vậy chúng sẽ có cùng một đầu ra. Nó không phụ thuộc vào trình biên dịch / trình thông dịch.
jsternberg

@jsternberg ... Cảm ơn tôi đã không chắc chắn khi sự gia tăng xảy ra vì tôi chưa bao giờ thực sự có lý do để đi kiểm tra nó. Đã quá lâu kể từ khi tôi làm trình biên dịch ở trường đại học! lol
Kenneth

Sai sai.
ruohola

-1

Tôi nghĩ về mặt ngữ nghĩa, ++icó ý nghĩa hơn i++, vì vậy tôi đã sử dụng cái đầu tiên, ngoại trừ việc không nên làm như vậy (như trong Java, nơi bạn nên sử dụng i++vì nó được sử dụng rộng rãi).


-2

Đó không chỉ là về hiệu suất.

Đôi khi bạn muốn tránh thực hiện sao chép, bởi vì nó không có ý nghĩa. Và vì việc sử dụng gia tăng tiền tố không phụ thuộc vào điều này, nó đơn giản hơn để bám vào hình thức tiền tố.

Và sử dụng các mức tăng khác nhau cho các loại nguyên thủy và các loại phức tạp ... điều đó thực sự không thể đọc được.


-2

Trừ khi bạn thực sự cần nó, tôi sẽ gắn bó với ++ i. Trong hầu hết các trường hợp, đây là những gì một người dự định. Không thường xuyên bạn cần i ++ và bạn luôn phải suy nghĩ kỹ khi đọc cấu trúc như vậy. Với ++ i, thật dễ dàng: bạn thêm 1, sử dụng nó và sau đó tôi vẫn như vậy.

Vì vậy, tôi đồng ý đầy đủ với @martin beckett: làm cho bản thân dễ dàng hơn, nó đã đủ khó.

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.