Có chi phí nào để khai báo một biến trong vòng lặp không? (C ++)


157

Tôi chỉ tự hỏi liệu có mất tốc độ hoặc hiệu quả không nếu bạn làm điều gì đó như sau:

int i = 0;
while(i < 100)
{
    int var = 4;
    i++;
}

mà tuyên bố int varmột trăm lần. Đối với tôi dường như sẽ có, nhưng tôi không chắc. thay vào đó sẽ thực tế hơn / nhanh hơn:

int i = 0;
int var;
while(i < 100)
{
    var = 4;
    i++;
}

hay chúng giống nhau, cùng tốc độ và hiệu quả?


7
Để rõ ràng, đoạn mã trên không "khai báo" var một trăm lần.
jason

1
@Rabarberski: Câu hỏi được tham chiếu không phải là bản sao chính xác vì nó không chỉ định ngôn ngữ. Câu hỏi này dành riêng cho C ++ . Nhưng theo câu trả lời được đăng cho câu hỏi tham khảo của bạn, câu trả lời phụ thuộc vào ngôn ngữ và có thể là trình biên dịch.
DavidRR

2
@jason Nếu đoạn mã đầu tiên không khai báo biến 'var' một trăm lần, bạn có thể giải thích điều gì đang xảy ra không? Nó chỉ khai báo biến một lần và khởi tạo nó 100 lần? Tôi đã nghĩ rằng mã khai báo và khởi tạo biến 100 lần, vì mọi thứ trong vòng lặp được thực thi 100 lần. Cảm ơn.
randomUser47534

Câu trả lời:


193

Không gian ngăn xếp cho các biến cục bộ thường được cấp phát trong phạm vi hàm. Vì vậy, không có điều chỉnh con trỏ ngăn xếp nào xảy ra bên trong vòng lặp, chỉ cần gán 4 cho var. Do đó hai đoạn mã này có cùng chi phí.


50
Tôi ước những người dạy ở trường đại học ít nhất cũng biết điều cơ bản này. Một lần anh ấy cười nhạo tôi khi khai báo một biến bên trong một vòng lặp và tôi đã tự hỏi có chuyện gì xảy ra cho đến khi anh ấy trích dẫn hiệu suất là lý do để không làm như vậy và tôi giống như "WTF !?".
Mehrdad Afshari

18
Bạn có chắc rằng bạn nên nói về không gian ngăn xếp ngay lập tức. Một biến như thế này cũng có thể nằm trong một thanh ghi.
toto

3
@toto Một biến như thế này cũng có thể không ở đâu - varbiến được khởi tạo nhưng không bao giờ được sử dụng, vì vậy một trình tối ưu hợp lý có thể loại bỏ nó hoàn toàn (ngoại trừ đoạn mã thứ hai nếu biến được sử dụng ở đâu đó sau vòng lặp).
CiaPan

@Mehrdad Afshari một biến trong vòng lặp nhận hàm tạo của nó được gọi một lần mỗi lần lặp. CHỈNH SỬA - Tôi thấy bạn đã đề cập điều này bên dưới, nhưng tôi nghĩ nó cũng đáng được đề cập trong câu trả lời được chấp nhận.
hoodaticus

105

Đối với các kiểu nguyên thủy và kiểu POD, nó không có gì khác biệt. Trình biên dịch sẽ cấp phát không gian ngăn xếp cho biến ở đầu hàm và phân bổ nó khi hàm trả về trong cả hai trường hợp.

Đối với các loại lớp không phải POD có các hàm tạo không tầm thường, nó SẼ tạo ra sự khác biệt - trong trường hợp đó, việc đặt biến bên ngoài vòng lặp sẽ chỉ gọi hàm tạo và hàm hủy một lần và toán tử gán mỗi lần lặp, trong khi đặt nó vào bên trong vòng lặp sẽ gọi hàm tạo và hàm hủy cho mỗi lần lặp lại của vòng lặp. Tùy thuộc vào hàm tạo, hàm hủy và toán tử gán của lớp làm gì, điều này có thể mong muốn hoặc không.


41
Ý kiến ​​đúng sai lý do. Biến ngoài vòng lặp. Được tạo một lần, bị hủy một lần nhưng toán tử asignment áp dụng mỗi lần lặp. Biến bên trong vòng lặp. Constructe / Desatructor đã áp dụng mọi lần lặp nhưng không có hoạt động gán.
Martin York

8
Đây là câu trả lời tốt nhất nhưng những bình luận này là khó hiểu. Có một sự khác biệt lớn giữa việc gọi một hàm tạo và một toán tử gán.
Andrew Grant

1
Đúng nếu thân vòng lặp thực hiện việc gán, không chỉ để khởi tạo. Và nếu chỉ có một khởi tạo không đổi / độc lập với body, trình tối ưu hóa có thể nâng nó lên.
peterchen

7
@Andrew Grant: Tại sao. Toán tử gán thường được định nghĩa là cấu trúc sao chép vào tmp, theo sau là hoán đổi (để ngoại lệ an toàn) theo sau là hủy tmp. Vì vậy, toán tử gán không khác với chu trình xây dựng / phá hủy ở trên. Xem stackoverflow.com/questions/255612/… để biết ví dụ về toán tử gán điển hình.
Martin York

1
Nếu hàm tạo / hủy đắt tiền, tổng chi phí của chúng là giới hạn trên hợp lý đối với chi phí của toán tử =. Nhưng công việc thực sự có thể rẻ hơn. Ngoài ra, khi chúng ta mở rộng cuộc thảo luận này từ kiểu ints sang kiểu C ++, người ta có thể khái quát 'var = 4' như một số thao tác khác chứ không phải 'gán biến từ một giá trị cùng kiểu'.
greggo

68

Cả hai đều giống nhau và đây là cách bạn có thể tìm hiểu, bằng cách xem trình biên dịch làm gì (ngay cả khi không đặt tối ưu hóa thành cao):

Hãy xem trình biên dịch (gcc 4.0) làm gì với các ví dụ đơn giản của bạn:

1.c:

main(){ int var; while(int i < 100) { var = 4; } }

gcc -S 1.c

1.s:

_main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    $0, -16(%ebp)
    jmp L2
L3:
    movl    $4, -12(%ebp)
L2:
    cmpl    $99, -16(%ebp)
    jle L3
    leave
    ret

2.c

main() { while(int i < 100) { int var = 4; } }

gcc -S 2.c

2.s:

_main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $24, %esp
        movl    $0, -16(%ebp)
        jmp     L2
L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3
        leave
        ret

Từ những điều này, bạn có thể thấy hai điều: thứ nhất, mã giống nhau ở cả hai.

Thứ hai, bộ nhớ cho var được cấp phát bên ngoài vòng lặp:

         subl    $24, %esp

Và cuối cùng, điều duy nhất trong vòng lặp là việc gán và kiểm tra điều kiện:

L3:
        movl    $4, -12(%ebp)
L2:
        cmpl    $99, -16(%ebp)
        jle     L3

Điều này hiệu quả nhất có thể mà không cần loại bỏ hoàn toàn vòng lặp.


2
"Hiệu quả nhất có thể mà không cần loại bỏ hoàn toàn vòng lặp" Không hoàn toàn. Mở một phần vòng lặp (thực hiện nó nói 4 lần mỗi lần vượt qua) sẽ tăng tốc độ đáng kể. Có lẽ có nhiều cách khác để tối ưu hóa ... mặc dù hầu hết các trình biên dịch hiện đại có lẽ sẽ nhận ra rằng không có ích gì khi lặp lại. Nếu 'i' được sử dụng sau này, nó sẽ chỉ đơn giản là đặt 'i' = 100.
darron 27/09/09

đó là giả sử mã được thay đổi thành 'i' tăng dần ... vì nó chỉ là một vòng lặp mãi mãi.
darron 27/09/09

Như là bài gốc!
Alex Brown

2
Tôi thích câu trả lời mà lý thuyết trở lại với bằng chứng! Rất vui khi thấy ASM kết xuất sao lưu lý thuyết về các mã bằng nhau. +1
Xavi Montero

1
Tôi thực sự đã tạo ra kết quả bằng cách tạo mã máy cho mỗi phiên bản. Không cần phải chạy nó.
Alex Brown,

13

Ngày nay, tốt hơn là khai báo nó bên trong vòng lặp trừ khi nó là một hằng số vì trình biên dịch sẽ có thể tối ưu hóa mã tốt hơn (giảm phạm vi biến).

CHỈNH SỬA: Câu trả lời này hầu hết đã lỗi thời bây giờ. Với sự gia tăng của các trình biên dịch hậu cổ điển, các trường hợp mà trình biên dịch không thể tìm ra nó ngày càng hiếm. Tôi vẫn có thể xây dựng chúng nhưng hầu hết mọi người sẽ phân loại cấu trúc là mã xấu.


4
Tôi nghi ngờ nó sẽ ảnh hưởng đến việc tối ưu hóa - nếu trình biên dịch thực hiện bất kỳ loại phân tích luồng dữ liệu nào, nó có thể phát hiện ra rằng nó không bị sửa đổi bên ngoài vòng lặp, vì vậy nó sẽ tạo ra cùng một mã được tối ưu hóa trong cả hai trường hợp.
Adam Rosenfield

3
Mặc dù vậy, nó sẽ không tìm ra nếu bạn có hai vòng lặp khác nhau sử dụng cùng một tên biến tạm thời.
Joshua

11

Hầu hết các trình biên dịch hiện đại sẽ tối ưu hóa điều này cho bạn. Điều đó đã được nói rằng tôi sẽ sử dụng ví dụ đầu tiên của bạn vì tôi thấy nó dễ đọc hơn.


3
Tôi không thực sự coi đó là một tối ưu hóa. Vì chúng là các biến cục bộ nên không gian ngăn xếp chỉ được cấp phát ở phần đầu của hàm. Không có "sự sáng tạo" thực sự nào liên quan đến việc gây hại cho hiệu suất (trừ khi một hàm tạo đang được gọi, đây hoàn toàn là một câu chuyện khác).
Mehrdad Afshari

Bạn nói đúng, "tối ưu hóa" là từ sai nhưng tôi không biết từ nào tốt hơn.
Andrew Hare

vấn đề là một trình tối ưu hóa như vậy sẽ sử dụng phân tích phạm vi trực tiếp và cả hai biến đều đã chết.
MSalters

Làm thế nào về "trình biên dịch sẽ không thấy bất kỳ sự khác biệt nào giữa chúng khi nó thực hiện phân tích luồng dữ liệu". Cá nhân tôi thích rằng phạm vi của một biến nên được giới hạn ở nơi nó được sử dụng, không phải vì hiệu quả mà để rõ ràng.
greggo

9

Đối với kiểu tích hợp có thể sẽ không có sự khác biệt giữa 2 kiểu (có thể là ngay từ mã được tạo).

Tuy nhiên, nếu biến là một lớp có hàm tạo / hủy không tầm thường thì có thể có sự khác biệt lớn về chi phí thời gian chạy. Nói chung, tôi sẽ định phạm vi biến vào bên trong vòng lặp (để giữ phạm vi càng nhỏ càng tốt), nhưng nếu điều đó hóa ra có tác động hoàn hảo, tôi sẽ tìm cách di chuyển biến lớp ra ngoài phạm vi của vòng lặp. Tuy nhiên, làm điều đó cần một số phân tích bổ sung vì ngữ nghĩa của đường dẫn ode có thể thay đổi, do đó, điều này chỉ có thể được thực hiện nếu các đoạn văn cho phép.

Một lớp RAII có thể cần hành vi này. Ví dụ, một lớp quản lý thời gian truy cập tệp có thể cần phải được tạo và hủy trên mỗi lần lặp vòng lặp để quản lý quyền truy cập tệp đúng cách.

Giả sử bạn có một LockMgrlớp có được một phần quan trọng khi nó được xây dựng và giải phóng nó khi bị phá hủy:

while (i< 100) {
    LockMgr lock( myCriticalSection); // acquires a critical section at start of
                                      //    each loop iteration

    // do stuff...

}   // critical section is released at end of each loop iteration

khá khác với:

LockMgr lock( myCriticalSection);
while (i< 100) {

    // do stuff...

}

6

Cả hai vòng lặp đều có cùng hiệu suất. Cả hai đều sẽ mất một khoảng thời gian vô hạn :) Có thể là một ý tưởng hay để tăng i bên trong các vòng lặp.


À vâng, tôi đã quên giải quyết vấn đề hiệu quả về không gian - không sao cả - 2 int cho cả hai. Đối với tôi, có vẻ kỳ lạ rằng các lập trình viên đang bỏ lỡ khu rừng cho cái cây - tất cả những đề xuất này về một số mã không chấm dứt.
Larry Watanabe

Sẽ ổn nếu họ không chấm dứt. Không ai trong số họ được gọi. :-)
Nosredna

2

Tôi đã từng chạy một số bài kiểm tra hiệu suất, và thật ngạc nhiên, tôi thấy rằng trường hợp 1 thực sự nhanh hơn! Tôi cho rằng điều này có thể là do việc khai báo biến bên trong vòng lặp làm giảm phạm vi của nó, vì vậy nó được giải phóng sớm hơn. Tuy nhiên, đó là một thời gian dài trước đây, trên một trình biên dịch rất cũ. Tôi chắc chắn rằng các trình biên dịch hiện đại làm tốt hơn công việc tối ưu hóa loại bỏ các tùy chọn, nhưng vẫn không có hại gì nếu giữ phạm vi biến của bạn càng ngắn càng tốt.


Sự khác biệt có lẽ đến từ sự khác biệt về phạm vi. Phạm vi càng nhỏ thì trình biên dịch càng có nhiều khả năng loại bỏ việc tuần tự hóa biến. Trong phạm vi vòng lặp nhỏ, biến có thể được đặt trong một thanh ghi và không được lưu trên khung ngăn xếp. Nếu bạn gọi một hàm trong vòng lặp hoặc bỏ tham chiếu một con trỏ, trình biên dịch không thực sự biết nó trỏ tới đâu, nó sẽ làm tràn biến vòng lặp nếu nó nằm trong phạm vi hàm (con trỏ có thể chứa &i).
Patrick Schlüter

Vui lòng đăng thiết lập và kết quả của bạn.
jxramos

2
#include <stdio.h>
int main()
{
    for(int i = 0; i < 10; i++)
    {
        int test;
        if(i == 0)
            test = 100;
        printf("%d\n", test);
    }
}

Mã trên luôn in 100 10 lần, có nghĩa là biến cục bộ bên trong vòng lặp chỉ được cấp phát một lần cho mỗi lần gọi hàm.


0

Cách duy nhất để chắc chắn là tính thời gian cho chúng. Nhưng sự khác biệt, nếu có, sẽ rất nhỏ, vì vậy bạn sẽ cần một vòng lặp thời gian lớn.

Thêm vào đó, cái đầu tiên là kiểu tốt hơn vì nó khởi tạo biến var, trong khi cái còn lại để nó chưa được khởi tạo. Điều này và hướng dẫn mà người ta nên xác định các biến càng gần điểm sử dụng của chúng càng tốt, có nghĩa là dạng đầu tiên thường được ưu tiên hơn.


"Cách duy nhất để chắc chắn là tính thời gian cho chúng." -1 không đúng sự thật. Xin lỗi nhưng một bài đăng khác đã chứng minh điều này sai bằng cách so sánh ngôn ngữ máy được tạo và thấy nó về cơ bản giống hệt nhau. Tôi không có vấn đề gì với câu trả lời của bạn nói chung, nhưng không sai vì -1 là gì?
Bill K

Kiểm tra mã được phát ra chắc chắn hữu ích và trong trường hợp đơn giản như thế này có thể là đủ. Tuy nhiên, trong những trường hợp phức tạp hơn, các vấn đề như định vị tham chiếu ở phía sau đầu của chúng, và chúng chỉ có thể được kiểm tra bằng cách định thời gian thực hiện.

-1

Chỉ với hai biến, trình biên dịch có thể sẽ được gán một thanh ghi cho cả hai. Dù sao thì các thanh ghi này cũng ở đó nên việc này không mất thời gian. Có 2 lệnh ghi thanh ghi và một lệnh đọc thanh ghi trong cả hai trường hợp.


-2

Tôi nghĩ rằng hầu hết các câu trả lời đều thiếu một điểm chính cần xem xét, đó là: "Nó có rõ ràng không" và rõ ràng trong tất cả các cuộc thảo luận, sự thật là; không có nó không phải là. Tôi muốn đề xuất trong hầu hết các mã vòng lặp, hiệu quả là khá nhiều không phải là vấn đề (trừ khi bạn tính toán cho một tàu đổ bộ sao hỏa), vì vậy thực sự câu hỏi duy nhất là cái gì trông hợp lý hơn, dễ đọc và có thể bảo trì - trong trường hợp này, tôi khuyên bạn nên khai báo biến lên phía trước và bên ngoài vòng lặp - điều này chỉ đơn giản làm cho nó rõ ràng hơn. Sau đó, những người như bạn và tôi thậm chí sẽ không buồn mất thời gian kiểm tra trực tuyến để xem nó có hợp lệ hay không.


-6

điều đó không đúng có chi phí đầu tư tuy nhiên chi phí bỏ qua của nó có thể chi phí.

Mặc dù có thể chúng sẽ kết thúc ở cùng một vị trí trên ngăn xếp Nó vẫn chỉ định nó. Nó sẽ gán vị trí bộ nhớ trên ngăn xếp cho int đó và sau đó giải phóng nó ở cuối}. Không phải theo nghĩa tự do heap theo nghĩa nó sẽ di chuyển sp (con trỏ ngăn xếp) bằng 1. Và trong trường hợp của bạn nếu xét nó chỉ có một biến cục bộ, nó sẽ chỉ đơn giản là tương đương fp (con trỏ khung) và sp

Câu trả lời ngắn gọn sẽ là: KHÔNG CHĂM SÓC CÁCH NÀO LÀM VIỆC NHƯ THẾ NÀO.

Nhưng hãy thử đọc thêm về cách tổ chức ngăn xếp. Trường đại học của tôi có những bài giảng khá hay về vấn đề đó Nếu bạn muốn đọc thêm, hãy xem tại đây http://www.cs.utk.edu/~plank/plank/classes/cs360/360/notes/Assembler1/lecture.html


Một lần nữa, -1 không đúng sự thật. Đọc bài đã nhìn vào hội.
Bill K

không, bạn đã sai. nhìn vào mã trình hợp dịch được tạo bằng mã đó
grobartn
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.