Là đếm ngược nhanh hơn so với đếm ngược?


131

Giáo viên khoa học máy tính của chúng tôi đã từng nói rằng vì một số lý do, việc đếm ngược hiệu quả hơn là đếm ngược. Ví dụ: nếu bạn cần sử dụng vòng lặp FOR và chỉ mục vòng lặp không được sử dụng ở đâu đó (như in một dòng N * lên màn hình) Tôi có nghĩa là mã như thế này:

for (i = N; i >= 0; i--)  
  putchar('*');  

tốt hơn:

for (i = 0; i < N; i++)  
  putchar('*');  

Có thật không? Và nếu vậy, có ai biết tại sao không?


6
Nhà khoa học máy tính nào? Trong ấn phẩm nào?
bmargulies

26
Có thể hình dung rằng bạn có thể tiết kiệm một nano giây mỗi lần lặp, hoặc nhiều như một sợi tóc trên một gia đình voi ma mút lông. Việc putcharnày đang sử dụng 99.9999% thời gian (cho hoặc nhận).
Mike Dunlavey

38
Tối ưu hóa sớm là gốc rễ của mọi tội lỗi. Sử dụng bất kỳ hình thức nào có vẻ phù hợp với bạn, bởi vì (như bạn đã biết) chúng tương đương về mặt logic. Phần khó nhất của lập trình là truyền đạt lý thuyết của chương trình cho các lập trình viên khác (và chính bạn!). Sử dụng một cấu trúc làm cho bạn hoặc một số lập trình viên khác từng nhìn vào nó trong hơn một giây là mất mạng. Bạn sẽ không bao giờ lấy lại thời gian bất cứ ai dành thời gian suy nghĩ "tại sao điều này lại đếm ngược?"
David M

61
Vòng lặp đầu tiên rõ ràng là chậm hơn, vì nó gọi putar 11 lần, trong khi vòng thứ hai chỉ gọi nó là 10 lần.
Paul Kuliniewicz

17
Bạn có nhận thấy rằng nếu ikhông dấu, vòng lặp đầu tiên là một vòng lặp vô hạn?
Shahbaz

Câu trả lời:


371

Có thật không? và nếu vậy có ai biết tại sao không?

Vào thời xa xưa, khi máy tính vẫn bị sứt mẻ bằng silica nung chảy bằng tay, khi vi điều khiển 8 bit đi lang thang trên Trái đất và khi giáo viên của bạn còn trẻ (hoặc giáo viên của giáo viên bạn còn trẻ), có một hướng dẫn máy phổ biến được gọi là giảm và bỏ qua nếu không (DSZ). Các lập trình viên lắp ráp Hotshot đã sử dụng hướng dẫn này để thực hiện các vòng lặp. Các máy sau này có các hướng dẫn dễ hiểu hơn, nhưng vẫn còn khá nhiều bộ xử lý trên đó rẻ hơn khi so sánh thứ gì đó bằng 0 so với so với bất kỳ thứ gì khác. (Điều đó đúng ngay cả trên một số máy RISC hiện đại, như PPC hoặc SPARC, dự trữ toàn bộ đăng ký luôn luôn bằng không.)

Vì vậy, nếu bạn sử dụng các vòng lặp để so sánh với số 0 thay vì N, điều gì có thể xảy ra?

  • Bạn có thể lưu một đăng ký
  • Bạn có thể nhận được một hướng dẫn so sánh với mã hóa nhị phân nhỏ hơn
  • Nếu một lệnh trước đó xảy ra để đặt cờ (chỉ có thể trên các máy gia đình x86), bạn thậm chí có thể không cần một hướng dẫn so sánh rõ ràng

Những khác biệt này có khả năng dẫn đến bất kỳ cải tiến có thể đo lường được trên các chương trình thực trên bộ xử lý không theo thứ tự hiện đại không? Bất thường. Trên thực tế, tôi rất ấn tượng nếu bạn có thể cho thấy sự cải thiện có thể đo được ngay cả trên một microbenchmark.

Tóm tắt: Tôi đập giáo viên của bạn lộn ngược đầu! Bạn không nên học những giả thuyết lỗi thời về cách tổ chức các vòng lặp. Bạn nên học rằng điều quan trọng nhất về các vòng lặp là đảm bảo rằng chúng chấm dứt , đưa ra câu trả lời chính xácdễ đọc . Tôi ước giáo viên của bạn sẽ tập trung vào những thứ quan trọng chứ không phải thần thoại.


3
++ Và bên cạnh đó, putcharmất nhiều đơn đặt hàng có cường độ dài hơn so với chi phí vòng lặp.
Mike Dunlavey

41
Nó không hoàn toàn là thần thoại: nếu anh ta đang thực hiện một số hệ thống thời gian thực được tối ưu hóa uber, nó sẽ có ích. Nhưng loại tin tặc đó có lẽ đã biết tất cả những điều này và chắc chắn sẽ không gây nhầm lẫn cho các sinh viên CS cấp độ đầu vào với arcana.
Paul Nathan

4
@Joshua: Bằng cách nào tối ưu hóa này sẽ được phát hiện? Như người hỏi đã nói, chỉ số vòng lặp không được sử dụng trong chính vòng lặp, do đó, với điều kiện số lần lặp là như nhau, không có thay đổi trong hành vi. Về mặt bằng chứng về tính chính xác, việc thay thế biến j=N-icho thấy hai vòng lặp là tương đương.
psmears

7
+1 cho Tóm tắt. Đừng đổ mồ hôi vì trên phần cứng hiện đại, nó hầu như không có sự khác biệt. Nó hầu như không có sự khác biệt 20 năm trước. Nếu bạn nghĩ rằng bạn phải quan tâm, thời gian cả hai cách, không thấy sự khác biệt rõ ràng và quay lại viết mã rõ ràng và chính xác .
Donal Fellows

3
Tôi không biết tôi nên upvote cho cơ thể hay downvote cho tóm tắt.
Thủy thủ Danubian

29

Đây là những gì có thể xảy ra trên một số phần cứng tùy thuộc vào những gì trình biên dịch có thể suy ra về phạm vi các số bạn đang sử dụng: với vòng lặp tăng dần bạn phải kiểm tra i<Nmỗi lần vòng quanh vòng lặp. Đối với phiên bản giảm dần, cờ mang (được đặt làm hiệu ứng phụ của phép trừ) có thể tự động cho bạn biết nếu i>=0. Điều đó tiết kiệm một bài kiểm tra mỗi lần vòng vòng.

Trong thực tế, trên phần cứng bộ xử lý đường ống hiện đại, công cụ này gần như chắc chắn không liên quan vì không có ánh xạ 1-1 đơn giản từ hướng dẫn đến chu kỳ xung nhịp. (Mặc dù tôi có thể tưởng tượng nó sẽ xuất hiện nếu bạn đang làm những việc như tạo tín hiệu video được định thời chính xác từ vi điều khiển. Nhưng sau đó bạn sẽ viết bằng ngôn ngữ lắp ráp.)


2
đó có phải là cờ không và không phải là cờ mang không?
Bob

2
@Bob Trong trường hợp này, bạn có thể muốn đạt đến số 0, in kết quả, giảm thêm và sau đó tìm thấy bạn đã đi một số dưới 0 gây ra mang (hoặc mượn). Nhưng được viết hơi khác một vòng lặp giảm có thể sử dụng cờ zero thay thế.
sigfpe

1
Chỉ cần hoàn toàn có phạm vi, không phải tất cả các phần cứng hiện đại đều được lắp đặt. Bộ xử lý nhúng sẽ có liên quan nhiều hơn đến loại vi mô hóa này.
Paul Nathan

@Paul Khi tôi có một số kinh nghiệm với Atmel AVR, tôi đã không quên đề cập đến các bộ vi điều khiển ...
sigfpe

27

Trong tập lệnh Intel x86, việc xây dựng một vòng lặp để đếm ngược về 0 thường có thể được thực hiện với ít hướng dẫn hơn một vòng lặp đếm đến điều kiện thoát khác không. Cụ thể, thanh ghi ECX theo truyền thống được sử dụng làm bộ đếm vòng lặp trong x86 asm và tập lệnh Intel có lệnh nhảy jcxz đặc biệt để kiểm tra thanh ghi ECX bằng 0 và nhảy dựa trên kết quả kiểm tra.

Tuy nhiên, sự khác biệt hiệu suất sẽ không đáng kể trừ khi vòng lặp của bạn đã rất nhạy cảm với số chu kỳ đồng hồ. Đếm xuống 0 có thể loại bỏ 4 hoặc 5 chu kỳ đồng hồ khỏi mỗi lần lặp của vòng lặp so với đếm ngược, vì vậy nó thực sự là một điều mới lạ hơn là một kỹ thuật hữu ích.

Ngoài ra, một trình biên dịch tối ưu hóa tốt hiện nay sẽ có thể chuyển đổi mã nguồn vòng lặp đếm ngược của bạn thành đếm ngược thành mã máy bằng không (tùy thuộc vào cách bạn sử dụng biến chỉ số vòng lặp) vì vậy thực sự không có lý do nào để viết vòng lặp của bạn vào những cách kỳ lạ chỉ để ép một hoặc hai chu kỳ ở đây và đó.


2
Tôi đã thấy trình biên dịch C ++ của Microsoft từ vài năm trước thực hiện tối ưu hóa đó. Có thể thấy rằng chỉ số vòng lặp không được sử dụng, vì vậy nó sắp xếp lại nó ở dạng nhanh nhất.
Đánh dấu tiền chuộc

1
@Mark: Trình biên dịch Delphi cũng vậy, bắt đầu từ năm 1996.
dthorpe

4
@MarkRansom Trên thực tế, trình biên dịch có thể thực hiện vòng lặp bằng cách sử dụng đếm ngược ngay cả khi biến chỉ số vòng lặp được sử dụng, tùy thuộc vào cách sử dụng nó trong vòng lặp. Nếu biến chỉ số vòng lặp chỉ được sử dụng để lập chỉ mục thành các mảng tĩnh (mảng có kích thước đã biết tại thời gian biên dịch), thì việc lập chỉ mục mảng có thể được thực hiện dưới dạng ptr + kích thước mảng - chỉ số vòng lặp var, vẫn có thể là một lệnh đơn trong x86. Thật là hoang dã khi gỡ lỗi trình biên dịch chương trình và thấy vòng lặp đếm ngược nhưng các chỉ số mảng sẽ tăng lên!
dthorpe

1
Trên thực tế ngày nay trình biên dịch của bạn có thể sẽ không sử dụng các hướng dẫn vòng lặp và jecxz vì chúng chậm hơn một cặp dec / jnz.
fuz

1
@FUZxxl Tất cả lý do nhiều hơn để không viết vòng lặp của bạn theo những cách lạ. Viết mã rõ ràng có thể đọc được của con người và để trình biên dịch thực hiện công việc của nó.
dthorpe

23

Đúng..!!

Đếm từ N xuống 0 nhanh hơn một chút so với Đếm từ 0 đến N theo nghĩa phần cứng sẽ xử lý so sánh như thế nào ..

Lưu ý so sánh trong mỗi vòng lặp

i>=0
i<N

Hầu hết các bộ xử lý có so sánh với lệnh zero..với cái đầu tiên sẽ được dịch sang mã máy là:

  1. Tải i
  2. So sánh và nhảy nếu nhỏ hơn hoặc bằng 0

Nhưng cái thứ hai cần tải N bộ nhớ mỗi lần

  1. tải tôi
  2. tải N
  3. Tiểu i và N
  4. So sánh và nhảy nếu nhỏ hơn hoặc bằng 0

Vì vậy, nó không phải là vì đếm ngược hoặc lên .. Nhưng vì cách mã của bạn sẽ được dịch thành mã máy ..

Vì vậy, đếm từ 10 đến 100 cũng giống như đếm từ 100 đến 10
Nhưng đếm từ i = 100 đến 0 nhanh hơn từ i = 0 đến 100 - trong hầu hết các trường hợp
Và đếm từ i = N đến 0 nhanh hơn từ i = 0 đến N

  • Lưu ý rằng ngày nay trình biên dịch có thể thực hiện tối ưu hóa này cho bạn (nếu nó đủ thông minh)
  • Cũng lưu ý rằng đường ống có thể gây ra hiệu ứng giống như dị thường của Belarou (không thể chắc chắn điều gì sẽ tốt hơn)
  • Cuối cùng: xin lưu ý rằng 2 vòng lặp bạn đã trình bày không tương đương .. lần đầu tiên in thêm một lần nữa * ....

Liên quan: Tại sao n ++ thực thi nhanh hơn n = n + 1?


6
Vì vậy, những gì bạn đang nói là không nhanh hơn để đếm ngược, nó chỉ nhanh hơn so với không so với bất kỳ giá trị nào khác. Có nghĩa là đếm từ 10 đến 100 và đếm ngược từ 100 đến 10 sẽ giống nhau?
Bob

8
Vâng .. đó không phải là vấn đề "đếm ngược hay tăng giá" .. mà là vấn đề "so sánh với cái gì" ..
Betamoo

3
Trong khi điều này là đúng cấp độ lắp ráp. Hai điều kết hợp với tôi không đúng trong thực tế - phần cứng hiện đại sử dụng các ống dẫn dài và các hướng dẫn đầu cơ sẽ lẻn vào "Sub i và N" mà không phát sinh thêm một chu kỳ - và - ngay cả trình biên dịch thô sơ nhất cũng sẽ tối ưu hóa "Sub i và N "tồn tại.
James Anderson

2
@nico Không phải là một hệ thống cổ xưa. Nó chỉ phải là một tập lệnh trong đó có một phép so sánh với phép toán bằng 0, theo cách nào đó nhanh hơn / tốt hơn so với giá trị tương đương so với giá trị đăng ký. x86 có nó trong jcxz. x64 vẫn có nó. Không cổ xưa. Ngoài ra, kiến ​​trúc RISC thường không có trường hợp đặc biệt. Ví dụ, chip DEC AXP Alpha (trong họ MIPS), có "thanh ghi 0" - đọc là 0, viết không làm gì cả. So sánh với thanh ghi số 0 thay vì so với thanh ghi chung có chứa giá trị 0 sẽ làm giảm sự phụ thuộc của lệnh và giúp thực hiện lệnh.
dthorpe

5
@Betamoo: Tôi thường tự hỏi tại sao câu trả lời không tốt hơn / đúng hơn (là của bạn) không được đánh giá cao hơn bởi nhiều phiếu hơn và đi đến kết luận rằng quá thường xuyên về phiếu bầu stackoverflow bị ảnh hưởng bởi danh tiếng (điểm) của một người trả lời ( đó là rất rất xấu) và không phải bởi câu trả lời đúng
Artur

12

Trong C đến psudo-lắp ráp:

for (i = 0; i < 10; i++) {
    foo(i);
}

trở thành

    clear i
top_of_loop:
    call foo
    increment i
    compare 10, i
    jump_less top_of_loop

trong khi:

for (i = 10; i >= 0; i--) {
    foo(i);
}

trở thành

    load i, 10
top_of_loop:
    call foo
    decrement i
    jump_not_neg top_of_loop

Lưu ý sự thiếu so sánh trong lắp ráp psudo thứ hai. Trên nhiều kiến ​​trúc, có các cờ được thiết lập bởi các phép toán số học (cộng, trừ, nhân, chia, tăng, giảm) mà bạn có thể sử dụng để nhảy. Chúng thường cung cấp cho bạn những gì về cơ bản là so sánh kết quả của hoạt động với 0 miễn phí. Trong thực tế trên nhiều kiến ​​trúc

x = x - 0

về mặt ngữ nghĩa giống như

compare x, 0

Ngoài ra, so sánh với 10 trong ví dụ của tôi có thể dẫn đến mã kém hơn. 10 có thể phải sống trong một sổ đăng ký, vì vậy nếu chúng thiếu nguồn cung cấp chi phí và có thể dẫn đến mã bổ sung để di chuyển mọi thứ xung quanh hoặc tải lại 10 mỗi lần qua vòng lặp.

Trình biên dịch đôi khi có thể sắp xếp lại mã để tận dụng lợi thế này, nhưng điều này thường khó khăn vì chúng thường không thể chắc chắn rằng việc đảo ngược hướng qua vòng lặp là tương đương về mặt ngữ nghĩa.


Có thể có một khác biệt của 2 hướng dẫn thay vì chỉ 1?
Pacerier

Ngoài ra, tại sao khó có thể chắc chắn về điều đó? Miễn là var ikhông được sử dụng trong vòng lặp, rõ ràng bạn có thể lật nó không?
Pacerier

6

Đếm ngược nhanh hơn trong trường hợp như thế này:

for (i = someObject.getAllObjects.size(); i >= 0; i--) {…}

bởi vì someObject.getAllObjects.size()thực thi một lần lúc đầu


Chắc chắn, hành vi tương tự có thể đạt được bằng cách gọi size()ra khỏi vòng lặp, như Peter đã đề cập:

size = someObject.getAllObjects.size();
for (i = 0; i < size; i++) {…}

5
Nó không "chắc chắn nhanh hơn". Trong nhiều trường hợp, cuộc gọi kích thước () có thể được kéo ra khỏi vòng lặp khi đếm lên, do đó, nó sẽ vẫn chỉ được gọi một lần. Rõ ràng đây là phụ thuộc vào ngôn ngữ và trình biên dịch (và phụ thuộc mã; ví dụ: trong C ++, nó sẽ không bị treo nếu kích thước () là ảo), nhưng nó không được xác định rõ ràng.
Peter

3
@Peter: Chỉ khi trình biên dịch biết chắc chắn rằng size () là idempotent trên toàn vòng lặp. Điều đó có lẽ gần như không phải luôn luôn như vậy, trừ khi vòng lặp rất đơn giản.
Lawrence Dol

@LawrenceDol, Trình biên dịch chắc chắn sẽ biết điều đó trừ khi bạn có compilatino mã động sử dụng exec.
Pacerier

4

Là nó nhanh hơn để đếm ngược hơn lên?

Có lẽ. Nhưng hơn 99% thời gian nó không thành vấn đề, vì vậy bạn nên sử dụng thử nghiệm 'hợp lý' nhất để chấm dứt vòng lặp, và theo cảm tính, tôi muốn nói rằng người đọc cần ít suy nghĩ nhất để tìm ra những gì vòng lặp đang làm (bao gồm cả những gì làm cho nó dừng lại). Làm cho mã của bạn khớp với mô hình tinh thần (hoặc tài liệu) về những gì mã đang làm.

Nếu vòng lặp đang hoạt động, nó đi lên qua một mảng (hoặc danh sách, hoặc bất cứ thứ gì), bộ đếm tăng thường sẽ phù hợp hơn với cách người đọc có thể nghĩ về vòng lặp đang làm gì - mã hóa vòng lặp của bạn theo cách này.

Nhưng nếu bạn đang làm việc thông qua một container có Ncác mặt hàng và đang loại bỏ các mặt hàng khi bạn đi, nó có thể có ý nghĩa nhận thức hơn để làm việc với bộ đếm.

Chi tiết hơn một chút về câu 'có thể' trong câu trả lời:

Đúng là trên hầu hết các kiến ​​trúc, việc kiểm tra tính toán dẫn đến 0 (hoặc đi từ 0 đến âm) không yêu cầu hướng dẫn kiểm tra rõ ràng - kết quả có thể được kiểm tra trực tiếp. Nếu bạn muốn kiểm tra xem một phép tính có dẫn đến một số khác hay không, dòng lệnh thường sẽ phải có một lệnh rõ ràng để kiểm tra giá trị đó. Tuy nhiên, đặc biệt với các CPU hiện đại, thử nghiệm này thường sẽ thêm ít hơn thời gian bổ sung độ ồn vào cấu trúc vòng lặp. Đặc biệt nếu vòng lặp đó đang thực hiện I / O.

Mặt khác, nếu bạn đếm ngược từ 0 và sử dụng bộ đếm làm chỉ số mảng, chẳng hạn, bạn có thể thấy mã hoạt động theo kiến ​​trúc bộ nhớ của hệ thống - việc đọc bộ nhớ thường sẽ khiến bộ đệm 'nhìn về phía trước' một số vị trí bộ nhớ vượt qua vị trí hiện tại để dự đoán về việc đọc tuần tự. Nếu bạn đang làm việc ngược thông qua bộ nhớ, hệ thống bộ đệm có thể không lường trước được việc đọc vị trí bộ nhớ ở địa chỉ bộ nhớ thấp hơn. Trong trường hợp này, có thể việc lặp 'ngược' có thể ảnh hưởng đến hiệu suất. Tuy nhiên, tôi vẫn có thể mã hóa vòng lặp theo cách này (miễn là hiệu suất không trở thành vấn đề) bởi vì tính chính xác là tối quan trọng và làm cho mã khớp với mô hình là một cách tuyệt vời để giúp đảm bảo tính chính xác. Mã không chính xác là không tối ưu như bạn có thể nhận được.

Vì vậy, tôi sẽ có xu hướng quên lời khuyên của giáo sư (tất nhiên, không phải trong bài kiểm tra của anh ấy - bạn vẫn nên thực dụng cho đến khi lớp học đi), trừ khi và cho đến khi hiệu suất của mã thực sự quan trọng.


3

Trên một số CPU cũ hơn có / được hướng dẫn như DJNZ== "giảm và nhảy nếu không bằng không". Điều này cho phép các vòng lặp hiệu quả trong đó bạn đã tải một giá trị đếm ban đầu vào một thanh ghi và sau đó bạn có thể quản lý hiệu quả một vòng lặp giảm dần với một lệnh. Mặc dù vậy, chúng ta đang nói về ISAs thập niên 1980 - giáo viên của bạn hoàn toàn mất liên lạc nếu anh ta nghĩ rằng "quy tắc ngón tay cái" này vẫn áp dụng với các CPU hiện đại.


3

Bob,

Không phải cho đến khi bạn thực hiện vi mô hóa, tại thời điểm đó, bạn sẽ có hướng dẫn sử dụng cho CPU của mình. Hơn nữa, nếu bạn đang làm điều đó, có lẽ bạn sẽ không cần phải hỏi câu hỏi này. :-) Nhưng, giáo viên của bạn rõ ràng không đăng ký ý tưởng đó ....

Có 4 điều cần xem xét trong ví dụ về vòng lặp của bạn:

for (i=N; 
 i>=0;             //thing 1
 i--)             //thing 2
{
  putchar('*');   //thing 3
}
  • So sánh

So sánh là (như những người khác đã chỉ ra) có liên quan đến kiến trúc bộ xử lý cụ thể . Có nhiều loại bộ xử lý hơn các loại chạy Windows. Cụ thể, có thể có một hướng dẫn đơn giản hóa và tăng tốc độ so sánh với 0.

  • Điều chỉnh

Trong một số trường hợp, nó nhanh hơn để điều chỉnh lên hoặc xuống. Thông thường, một trình biên dịch tốt sẽ tìm ra nó và làm lại vòng lặp nếu có thể. Không phải tất cả các trình biên dịch là tốt mặc dù.

  • Cơ thể vòng

Bạn đang truy cập vào một tòa nhà cao tầng với putar. Đó là ồ ạt chậm. Thêm vào đó, bạn đang hiển thị lên màn hình (một cách gián tiếp). Điều đó thậm chí còn chậm hơn. Hãy nghĩ tỷ lệ 1000: 1 trở lên. Trong tình huống này, thân vòng lặp hoàn toàn và hoàn toàn vượt xa chi phí điều chỉnh / so sánh vòng lặp.

  • Bộ nhớ cache

Bố cục bộ nhớ cache và bộ nhớ có thể có ảnh hưởng lớn đến hiệu suất. Trong tình huống này, nó không thành vấn đề. Tuy nhiên, nếu bạn đang truy cập vào một mảng và cần hiệu năng tối ưu, nó sẽ cho phép bạn điều tra cách trình biên dịch và bộ xử lý của bạn đưa ra các bộ nhớ truy cập và điều chỉnh phần mềm của bạn để tận dụng tối đa điều đó. Ví dụ chứng khoán là một ví dụ được đưa ra liên quan đến phép nhân ma trận.


3

Điều quan trọng hơn nhiều so với việc bạn tăng hay giảm bộ đếm là việc bạn tăng bộ nhớ hay giảm bộ nhớ. Hầu hết các bộ nhớ cache được tối ưu hóa để tăng bộ nhớ, không giảm bộ nhớ. Vì thời gian truy cập bộ nhớ là nút cổ chai mà hầu hết các chương trình hiện nay phải đối mặt, điều này có nghĩa là việc thay đổi chương trình của bạn để tăng bộ nhớ có thể dẫn đến tăng hiệu suất ngay cả khi điều này yêu cầu so sánh bộ đếm của bạn với giá trị khác không. Trong một số chương trình của tôi, tôi đã thấy một sự cải thiện đáng kể về hiệu suất bằng cách thay đổi mã của tôi để tăng bộ nhớ thay vì giảm nó.

Nghi ngờ? Chỉ cần viết một chương trình để vòng lặp thời gian đi lên / xuống bộ nhớ. Đây là đầu ra mà tôi có:

Average Up Memory   = 4839 mus
Average Down Memory = 5552 mus

Average Up Memory   = 18638 mus
Average Down Memory = 19053 mus

(trong đó "mus" là viết tắt của micro giây) khi chạy chương trình này:

#include <chrono>
#include <iostream>
#include <random>
#include <vector>

//Sum all numbers going up memory.
template<class Iterator, class T>
inline void sum_abs_up(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = first;
  do {
    sum += *it;
    it++;
  } while (it != one_past_last);
  total += sum;
}

//Sum all numbers going down memory.
template<class Iterator, class T>
inline void sum_abs_down(Iterator first, Iterator one_past_last, T &total) {
  T sum = 0;
  auto it = one_past_last;
  do {
    it--;
    sum += *it;
  } while (it != first);
  total += sum;
}

//Time how long it takes to make num_repititions identical calls to sum_abs_down().
//We will divide this time by num_repitions to get the average time.
template<class T>
std::chrono::nanoseconds TimeDown(std::vector<T> &vec, const std::vector<T> &vec_original,
                                  std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_down(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class T>
std::chrono::nanoseconds TimeUp(std::vector<T> &vec, const std::vector<T> &vec_original,
                                std::size_t num_repititions, T &running_sum) {
  std::chrono::nanoseconds total{0};
  for (std::size_t i = 0; i < num_repititions; i++) {
    auto start_time = std::chrono::high_resolution_clock::now();
    sum_abs_up(vec.begin(), vec.end(), running_sum);
    total += std::chrono::high_resolution_clock::now() - start_time;
    vec = vec_original;
  }
  return total;
}

template<class Iterator, typename T>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, T a, T b) {
  std::random_device rnd_device;
  std::mt19937 generator(rnd_device());
  std::uniform_int_distribution<T> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class Iterator>
void FillWithRandomNumbers(Iterator start, Iterator one_past_end, double a, double b) {
  std::random_device rnd_device;
  std::mt19937_64 generator(rnd_device());
  std::uniform_real_distribution<double> dist(a, b);
  for (auto it = start; it != one_past_end; it++)
    *it = dist(generator);
  return ;
}

template<class ValueType>
void TimeFunctions(std::size_t num_repititions, std::size_t vec_size = (1u << 24)) {
  auto lower = std::numeric_limits<ValueType>::min();
  auto upper = std::numeric_limits<ValueType>::max();
  std::vector<ValueType> vec(vec_size);

  FillWithRandomNumbers(vec.begin(), vec.end(), lower, upper);
  const auto vec_original = vec;
  ValueType sum_up = 0, sum_down = 0;

  auto time_up   = TimeUp(vec, vec_original, num_repititions, sum_up).count();
  auto time_down = TimeDown(vec, vec_original, num_repititions, sum_down).count();
  std::cout << "Average Up Memory   = " << time_up/(num_repititions * 1000) << " mus\n";
  std::cout << "Average Down Memory = " << time_down/(num_repititions * 1000) << " mus"
            << std::endl;
  return ;
}

int main() {
  std::size_t num_repititions = 1 << 10;
  TimeFunctions<int>(num_repititions);
  std::cout << '\n';
  TimeFunctions<double>(num_repititions);
  return 0;
}

Cả hai sum_abs_upsum_abs_downlàm cùng một điều (tổng hợp vectơ của các số) và được tính thời gian theo cùng một cách với sự khác biệt duy nhất là sum_abs_uptăng bộ nhớ trong khi sum_abs_downđi xuống bộ nhớ. Tôi thậm chí còn chuyển qua vectham chiếu để cả hai hàm truy cập vào cùng một vị trí bộ nhớ. Tuy nhiên, sum_abs_upluôn luôn nhanh hơn sum_abs_down. Hãy tự chạy (Tôi đã biên dịch nó với g ++ -O3).

Điều quan trọng cần lưu ý là vòng lặp mà tôi định thời gian chặt chẽ như thế nào. Nếu cơ thể của một vòng lặp lớn thì có khả năng sẽ không có vấn đề gì nếu trình vòng lặp của nó tăng hay giảm bộ nhớ vì thời gian thực hiện cơ thể của vòng lặp sẽ có khả năng thống trị hoàn toàn. Ngoài ra, điều quan trọng cần đề cập là với một số vòng lặp hiếm, việc giảm bộ nhớ đôi khi nhanh hơn so với việc đi lên. Nhưng ngay cả với các vòng lặp như vậy, không bao giờ xảy ra tình trạng tăng bộ nhớ luôn chậm hơn so với đi xuống (không giống như các vòng lặp nhỏ đi lên bộ nhớ, điều ngược lại là thường xuyên, thực tế, đối với một số vòng lặp nhỏ tôi ' đã tính thời gian, hiệu suất tăng bằng cách tăng bộ nhớ là 40 +%).

Vấn đề là, theo nguyên tắc thông thường, nếu bạn có tùy chọn, nếu cơ thể của vòng lặp nhỏ và nếu có một chút khác biệt giữa việc vòng lặp của bạn tăng bộ nhớ thay vì xuống bộ nhớ, thì bạn nên tăng bộ nhớ.

FYI vec_originalcó mặt để thử nghiệm, để dễ dàng thay đổi sum_abs_upsum_abs_downtheo cách khiến chúng thay đổi vectrong khi không cho phép những thay đổi này ảnh hưởng đến thời gian trong tương lai. Tôi khuyên bạn nên chơi đùa với sum_abs_upsum_abs_downvà thời gian kết quả.


2

bất kể hướng nào luôn sử dụng dạng tiền tố (++ i thay vì i ++)!

for (i=N; i>=0; --i)  

hoặc là

for (i=0; i<N; ++i) 

Giải thích: http://www.eskimo.com/~scs/c class / notes / sx7b.html

Hơn nữa bạn có thể viết

for (i=N; i; --i)  

Nhưng tôi mong đợi các trình biên dịch hiện đại có thể thực hiện chính xác những tối ưu hóa này.


Chưa bao giờ thấy mọi người phàn nàn về điều đó trước đây. Nhưng sau khi đọc các liên kết, nó thực sự có ý nghĩa :) Cảm ơn bạn.
Tommy Jakobsen

3
Ừm, tại sao anh ta luôn phải sử dụng mẫu tiền tố? Nếu không có bài tập nào diễn ra, chúng giống hệt nhau và bài viết bạn liên kết đến thậm chí nói rằng dạng hậu tố phổ biến hơn.
bobDevil

3
Tại sao người ta phải luôn luôn sử dụng mẫu tiền tố? Trong trường hợp này, nó giống hệt nhau về mặt ngữ nghĩa.
Ben Zotto

2
Biểu mẫu postfix có khả năng có thể tạo một bản sao không cần thiết của đối tượng, mặc dù nếu giá trị không bao giờ được sử dụng, trình biên dịch có thể sẽ tối ưu hóa nó thành dạng tiền tố.
Nick Lewis

Không có thói quen, tôi luôn luôn làm - i và i ++ vì khi tôi học máy tính C thường có đăng ký trước và sau đăng ký, nhưng không phải ngược lại. Do đó, * p ++ và * - p nhanh hơn * ++ p và * p-- bởi vì hai cái trước có thể được thực hiện trong một hướng dẫn mã máy 68000.
JeremyP

2

Đó là một câu hỏi thú vị, nhưng như một vấn đề thực tế, tôi không nghĩ nó quan trọng và không làm cho một vòng lặp nào tốt hơn vòng lặp kia.

Theo trang wikipedia này: Bước nhảy vọt thứ hai , "... ngày mặt trời trở nên dài hơn 1,7 ms mỗi thế kỷ do chủ yếu là do ma sát thủy triều." Nhưng nếu bạn đang đếm ngày cho đến ngày sinh nhật của bạn, bạn có thực sự quan tâm đến sự khác biệt nhỏ bé này về thời gian không?

Điều quan trọng hơn là mã nguồn dễ đọc và dễ hiểu. Hai vòng lặp đó là một ví dụ tốt về lý do tại sao khả năng đọc là quan trọng - chúng không lặp cùng một số lần.

Tôi cá là hầu hết các lập trình viên đều đọc (i = 0; i <N; i ++) và hiểu ngay rằng vòng lặp này N lần. Đối với tôi, một vòng lặp (i = 1; i <= N; i ++), ít rõ ràng hơn và với (i = N; i> 0; i--) tôi phải suy nghĩ về nó một lát . Sẽ tốt nhất nếu mục đích của mã đi thẳng vào não mà không cần suy nghĩ.


Cả hai cấu trúc chính xác là dễ hiểu. Có một số người cho rằng nếu bạn có 3 hoặc 4 lần lặp lại, thì tốt hơn là sao chép hướng dẫn hơn là tạo một vòng lặp vì nó dễ hiểu hơn.
Thủy thủ Danubian

2

Kỳ lạ thay, dường như có một sự khác biệt. Ít nhất, trong PHP. Xem xét điểm chuẩn sau:

<?php

print "<br>".PHP_VERSION;
$iter = 100000000;
$i=$t1=$t2=0;

$t1 = microtime(true);
for($i=0;$i<$iter;$i++){}
$t2 = microtime(true);
print '<br>$i++ : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;$i--){}
$t2 = microtime(true);
print '<br>$i-- : '.($t2-$t1);

$t1 = microtime(true);
for($i=0;$i<$iter;++$i){}
$t2 = microtime(true);
print '<br>++$i : '.($t2-$t1);

$t1 = microtime(true);
for($i=$iter;$i>0;--$i){}
$t2 = microtime(true);
print '<br>--$i : '.($t2-$t1);

Kết quả thật thú vị:

PHP 5.2.13
$i++ : 8.8842368125916
$i-- : 8.1797409057617
++$i : 8.0271911621094
--$i : 7.1027431488037


PHP 5.3.1
$i++ : 8.9625310897827
$i-- : 8.5790238380432
++$i : 5.9647901058197
--$i : 5.4021768569946

Nếu ai đó biết tại sao, thật tốt khi biết :)

EDIT : Kết quả là như nhau ngay cả khi bạn bắt đầu đếm không từ 0, nhưng giá trị tùy ý khác. Vì vậy, có lẽ không chỉ so sánh với số không tạo ra sự khác biệt?


Lý do chậm hơn là toán tử tiền tố không cần lưu trữ tạm thời. Hãy xem xét $ foo = $ i ++; Có ba điều xảy ra: $ i được lưu trữ tạm thời, $ i được tăng lên và sau đó $ foo được gán giá trị tạm thời đó. Trong trường hợp $ i ++; một trình biên dịch thông minh có thể nhận ra tạm thời là không cần thiết. PHP không. Trình biên dịch C ++ và Java đủ thông minh để thực hiện tối ưu hóa đơn giản này.
Trình biên dịch dễ thấy

và tại sao $ i-- nhanh hơn $ i ++?
ts.

Có bao nhiêu lần lặp lại điểm chuẩn của bạn mà bạn đã chạy? Bạn đã clip outriders và lấy trung bình cho mỗi kết quả? Máy tính của bạn có làm gì khác trong thời gian chuẩn không? Sự khác biệt ~ 0,5 đó có thể chỉ là kết quả của hoạt động CPU khác, hoặc việc sử dụng đường ống, hoặc ... hoặc ... tốt, bạn hiểu ý.
tám bit

Vâng, ở đây tôi đang đưa ra trung bình. Điểm chuẩn được chạy trên các máy khác nhau và sự khác biệt là vô tình.
ts.

@Conspicuity Compiler => bạn biết hay bạn cho rằng?
ts.

2

thể nhanh hơn.

Trên bộ xử lý NIOS II tôi hiện đang làm việc, vòng lặp truyền thống

for(i=0;i<100;i++)

sản xuất lắp ráp:

ldw r2,-3340(fp) %load i to r2
addi r2,r2,1     %increase i by 1
stw r2,-3340(fp) %save value of i
ldw r2,-3340(fp) %load value again (???)
cmplti r2,r2,100 %compare if less than equal 100
bne r2,zero,0xa018 %jump

Nếu chúng ta đếm ngược

for(i=100;i--;)

chúng tôi nhận được một hội đồng cần ít hơn 2 hướng dẫn.

ldw r2,-3340(fp)
addi r3,r2,-1
stw r3,-3340(fp)
bne r2,zero,0xa01c

Nếu chúng ta có các vòng lặp lồng nhau, trong đó vòng lặp bên trong được thực thi rất nhiều, chúng ta có thể có một sự khác biệt có thể đo lường được:

int i,j,a=0;
for(i=100;i--;){
    for(j=10000;j--;){
        a = j+1;
    }
}

Nếu vòng lặp bên trong được viết như trên, thời gian thực hiện là: 0.12199999999999999734 giây. Nếu vòng lặp bên trong được viết theo cách truyền thống, thời gian thực hiện là: 0.17199999999999998623 giây. Vì vậy, vòng lặp đếm ngược nhanh hơn khoảng 30% .

Nhưng: thử nghiệm này đã được thực hiện với tất cả các tối ưu hóa GCC đã tắt. Nếu chúng ta bật chúng, trình biên dịch thực sự thông minh hơn tối ưu hóa này và thậm chí giữ giá trị trong một thanh ghi trong toàn bộ vòng lặp và chúng ta sẽ có được một hội đồng như

addi r2,r2,-1
bne r2,zero,0xa01c

Trong ví dụ cụ này trình biên dịch thậm chí thông báo, rằng biến một sẽ allways được 1 sau khi thực hiện vòng lặp và bỏ qua vòng alltogether.

Tuy nhiên tôi đã trải nghiệm rằng đôi khi nếu thân vòng lặp đủ phức tạp, trình biên dịch không thể thực hiện tối ưu hóa này, vì vậy cách an toàn nhất để luôn thực hiện vòng lặp nhanh là viết:

register int i;
for(i=10000;i--;)
{ ... }

Tất nhiên điều này chỉ hoạt động, nếu việc vòng lặp được thực hiện ngược lại và như Betamoo đã nói không thành vấn đề, chỉ khi bạn đang đếm ngược về không.


2

Những gì giáo viên của bạn đã nói là một số tuyên bố xiên mà không làm rõ nhiều. KHÔNG phải là giảm dần nhanh hơn tăng nhưng bạn có thể tạo vòng lặp nhanh hơn nhiều với giảm dần so với tăng.

Không cần tiếp tục về nó, không cần sử dụng bộ đếm vòng lặp, v.v. - điều quan trọng dưới đây chỉ là tốc độ và số vòng lặp (khác không).

Đây là cách hầu hết mọi người thực hiện vòng lặp với 10 lần lặp:

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

Đối với 99% trường hợp, tất cả mọi người đều có thể cần, nhưng cùng với PHP, PYTHON, JavaScript, có cả thế giới phần mềm quan trọng về thời gian (thường được nhúng, HĐH, trò chơi, v.v.

int i;
for (i = 0; i < 10; i++)
{
    //something here
}

sau khi biên dịch (không tối ưu hóa) phiên bản đã biên dịch có thể trông như thế này (VS2015):

-------- C7 45 B0 00 00 00 00  mov         dword ptr [i],0  
-------- EB 09                 jmp         labelB 
labelA   8B 45 B0              mov         eax,dword ptr [i]  
-------- 83 C0 01              add         eax,1  
-------- 89 45 B0              mov         dword ptr [i],eax  
labelB   83 7D B0 0A           cmp         dword ptr [i],0Ah  
-------- 7D 02                 jge         out1 
-------- EB EF                 jmp         labelA  
out1:

Toàn bộ vòng lặp là 8 hướng dẫn (26 byte). Trong đó - thực sự có 6 lệnh (17 byte) với 2 nhánh. Có, tôi biết nó có thể được thực hiện tốt hơn (nó chỉ là một ví dụ).

Bây giờ hãy xem xét cấu trúc thường xuyên này mà bạn sẽ thường thấy được viết bởi nhà phát triển nhúng:

i = 10;
do
{
    //something here
} while (--i);

Nó cũng lặp lại 10 lần (vâng tôi biết giá trị của tôi khác so với hiển thị cho vòng lặp nhưng chúng tôi quan tâm đến số lần lặp ở đây). Điều này có thể được tổng hợp vào đây:

00074EBC C7 45 B0 01 00 00 00 mov         dword ptr [i],1  
00074EC3 8B 45 B0             mov         eax,dword ptr [i]  
00074EC6 83 E8 01             sub         eax,1  
00074EC9 89 45 B0             mov         dword ptr [i],eax  
00074ECC 75 F5                jne         main+0C3h (074EC3h)  

5 hướng dẫn (18 byte) và chỉ một nhánh. Thực tế có 4 lệnh trong vòng lặp (11 byte).

Điều tốt nhất là một số CPU (tương thích x86 / x64) có hướng dẫn có thể làm giảm thanh ghi, sau đó so sánh kết quả với 0 và thực hiện nhánh nếu kết quả khác 0. Hầu như TẤT CẢ PC cpus thực hiện hướng dẫn này. Sử dụng nó, vòng lặp thực sự chỉ là một (có một) lệnh 2 byte:

00144ECE B9 0A 00 00 00       mov         ecx,0Ah  
label:
                          // something here
00144ED3 E2 FE                loop        label (0144ED3h)  // decrement ecx and jump to label if not zero

Tôi có phải giải thích cái nào nhanh hơn không?

Bây giờ ngay cả khi CPU cụ thể không thực hiện hướng dẫn trên, tất cả những gì nó yêu cầu để mô phỏng nó là một sự sụt giảm theo sau bước nhảy có điều kiện nếu kết quả của lệnh trước đó bằng không.

Vì vậy, bất kể một số trường hợp mà bạn có thể chỉ ra là một nhận xét tại sao tôi sai, v.v ... TÔI XÁC NHẬN - CÓ NÓ LÀ LỢI ÍCH ĐỂ LOOP TẢI XUỐNG nếu bạn biết cách, tại sao và khi nào.

Tái bút Có, tôi biết rằng trình biên dịch khôn ngoan (với mức tối ưu hóa phù hợp) sẽ viết lại cho vòng lặp (với bộ đếm vòng tăng dần) thành do..trong khi tương đương với các vòng lặp không đổi ... (hoặc hủy đăng ký nó) ...


1

Không, điều đó không thực sự đúng. Một tình huống có thể nhanh hơn là khi bạn gọi một hàm để kiểm tra giới hạn trong mỗi lần lặp của vòng lặp.

for(int i=myCollection.size(); i >= 0; i--)
{
   ...
}

Nhưng nếu nó không rõ ràng để làm theo cách đó, nó không đáng giá. Trong các ngôn ngữ hiện đại, dù sao đi nữa, bạn nên sử dụng vòng lặp foreach. Bạn đặc biệt đề cập đến trường hợp bạn nên sử dụng vòng lặp foreach - khi bạn không cần chỉ mục.


1
Để rõ ràng hiệu quả, bạn nên có thói quen ít nhất for(int i=0, siz=myCollection.size(); i<siz; i++).
Lawrence Dol

1

Vấn đề là khi đếm ngược bạn không cần kiểm tra i >= 0riêng để giảm dần i. Quan sát:

for (i = 5; i--;) {
  alert(i);  // alert boxes showing 4, 3, 2, 1, 0
}

Cả so sánh và giảm dần icó thể được thực hiện trong một biểu thức.

Xem các câu trả lời khác để biết lý do tại sao điều này rút lại ít hướng dẫn x86 hơn.

Về việc nó có tạo ra sự khác biệt có ý nghĩa trong ứng dụng của bạn hay không, tôi đoán điều đó phụ thuộc vào số lượng vòng lặp của bạn và mức độ lồng nhau của chúng. Nhưng với tôi, thật dễ đọc khi làm theo cách này, vì vậy tôi vẫn làm điều đó.


Tôi nghĩ rằng đây là phong cách kém, bởi vì nó phụ thuộc vào người đọc biết rằng giá trị trả về của i-- là giá trị cũ của i, cho giá trị có thể của việc lưu một chu kỳ. Điều đó chỉ có ý nghĩa nếu có nhiều vòng lặp lặp và chu trình là một phần đáng kể của độ dài của vòng lặp và thực sự xuất hiện trong thời gian chạy. Tiếp theo, ai đó sẽ thử (i = 5; --i;) vì họ đã nghe nói rằng trong C ++, bạn có thể muốn tránh tạo một số tạm thời khi tôi thuộc loại không tầm thường, và bây giờ bạn đang ở trong vùng đất lỗi vô tình vứt bỏ cơ hội của bạn để làm cho mã sai nhìn sai.
mabraham

0

Bây giờ, tôi nghĩ rằng bạn đã có đủ các bài giảng lắp ráp :) Tôi muốn trình bày cho bạn một lý do khác cho cách tiếp cận từ trên xuống.

Lý do để đi từ đầu rất đơn giản. Trong phần thân của vòng lặp, bạn có thể vô tình thay đổi ranh giới, có thể kết thúc bằng hành vi không chính xác hoặc thậm chí là vòng lặp không kết thúc.

Nhìn vào phần nhỏ này của mã Java (ngôn ngữ không quan trọng tôi đoán vì lý do này):

    System.out.println("top->down");
    int n = 999;
    for (int i = n; i >= 0; i--) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }
    System.out.println("bottom->up");
    n = 1;
    for (int i = 0; i < n; i++) {
        n++;
        System.out.println("i = " + i + "\t n = " + n);
    }

Vì vậy, quan điểm của tôi là bạn nên xem xét việc thích đi từ trên xuống hoặc có một hằng số là một ranh giới.


Huh?!! Ví dụ thất bại của bạn thực sự phản trực giác, nghĩa là, một cuộc tranh cãi của người rơm - không ai sẽ viết điều này. Một người sẽ viết for (int i=0; i < 999; i++) {.
Lawrence Dol

@Software Monkey tưởng tượng n là kết quả của một số tính toán ... ví dụ: bạn có thể muốn lặp lại một số bộ sưu tập và kích thước của nó là ranh giới, nhưng vì một số hiệu ứng phụ, bạn thêm các yếu tố mới vào bộ sưu tập trong thân vòng lặp.
Gabriel čerbák

Nếu đó là những gì bạn dự định giao tiếp, thì đó là những gì ví dụ của bạn nên minh họa:for(int xa=0; xa<collection.size(); xa++) { collection.add(SomeObject); ... }
Lawrence Dol

@Software Monkey Tôi muốn nói chung chung hơn là chỉ nói riêng về các bộ sưu tập, bởi vì những gì tôi suy luận không liên quan gì đến các bộ sưu tập
Gabriel čerbák

2
Có, nhưng nếu bạn sẽ lý luận bằng ví dụ, ví dụ của bạn cần đáng tin cậy và minh họa cho điểm.
Lawrence Dol

-1

Ở cấp độ trình biên dịch, một vòng lặp đếm ngược đến 0 thường nhanh hơn một chút so với vòng lặp đếm đến một giá trị nhất định. Nếu kết quả tính toán bằng 0, hầu hết các bộ xử lý sẽ đặt cờ không. Nếu trừ đi một phép tính bao quanh quá 0, thông thường sẽ thay đổi cờ mang (trên một số bộ xử lý, nó sẽ đặt nó trên các bộ xử lý khác, nó sẽ xóa nó), do đó, so sánh với số 0 về cơ bản là miễn phí.

Điều này thậm chí còn đúng hơn khi số lần lặp không phải là hằng số mà là một biến.

Trong các trường hợp tầm thường, trình biên dịch có thể tự động tối ưu hóa hướng đếm của vòng lặp nhưng trong các trường hợp phức tạp hơn, có thể lập trình viên biết rằng hướng của vòng lặp không liên quan đến hành vi tổng thể nhưng trình biên dịch không thể chứng minh điều đó.

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.