Các câu lệnh điều kiện không tầm thường có nên được chuyển đến phần khởi tạo của các vòng lặp không?


21

Tôi có ý tưởng này từ câu hỏi này trên stackoverflow.com

Các mẫu sau là phổ biến:

final x = 10;//whatever constant value
for(int i = 0; i < Math.floor(Math.sqrt(x)) + 1; i++) {
  //...do something
}

Điểm tôi đang cố gắng đưa ra là tuyên bố có điều kiện là một cái gì đó phức tạp và không thay đổi.

Là tốt hơn để khai báo nó trong phần khởi tạo của vòng lặp, như vậy?

final x = 10;//whatever constant value
for(int i = 0, j = Math.floor(Math.sqrt(x)) + 1; i < j; i++) {
  //...do something
}

Điều này rõ ràng hơn?

Nếu biểu thức điều kiện đơn giản như

final x = 10;//whatever constant value
for(int i = 0, j = n*n; i > j; j++) {
  //...do something
}

47
Tại sao không chỉ di chuyển nó đến dòng trước vòng lặp, sau đó bạn cũng có thể đặt cho nó một cái tên hợp lý.
jonrsharpe

2
@Mehrdad: Chúng không tương đương. Nếu xlớn về độ lớn, Math.floor(Math.sqrt(x))+1bằng Math.floor(Math.sqrt(x)). :-)
R ..

5
@jonrsharpe Bởi vì điều đó sẽ mở rộng phạm vi của biến. Tôi không nhất thiết phải nói rằng đó là một lý do tốt để không làm điều đó, nhưng đó là lý do một số người không làm.
Kevin Krumwiede

3
@KevinKrumwiede Nếu phạm vi là một mối quan tâm giới hạn nó bằng cách đặt mã vào khối riêng của nó, ví dụ, { x=whatever; for (...) {...} }hoặc tốt hơn, hãy xem xét liệu có đủ xảy ra rằng nó cần phải là một chức năng riêng biệt hay không.
Blrfl

2
@jonrsharpe Bạn cũng có thể đặt cho nó một cái tên hợp lý khi khai báo nó trong phần init. Không nói tôi sẽ đặt nó ở đó; Nó vẫn dễ đọc hơn nếu nó tách rời.
JollyJoker

Câu trả lời:


62

Những gì tôi sẽ làm là một cái gì đó như thế này:

void doSomeThings() {
    final x = 10;//whatever constant value
    final limit = Math.floor(Math.sqrt(x)) + 1;
    for(int i = 0; i < limit; i++) {
         //...do something
    }
}

Thành thật mà nói, lý do chính đáng duy nhất để nhồi nhét việc khởi tạo j(bây giờ limit) vào tiêu đề vòng lặp là để giữ cho phạm vi chính xác. Tất cả những gì cần thiết để làm cho một vấn đề không phải là một phạm vi kèm theo chặt chẽ tốt đẹp.

Tôi có thể đánh giá cao mong muốn được nhanh chóng nhưng đừng hy sinh khả năng đọc mà không có lý do chính đáng.

Chắc chắn, trình biên dịch có thể tối ưu hóa, khởi tạo nhiều vars có thể là hợp pháp, nhưng các vòng lặp đủ khó để gỡ lỗi như hiện tại. Hãy tử tế với con người. Nếu điều này thực sự hóa ra làm chúng ta chậm lại thì thật tốt để hiểu nó đủ để sửa nó.


Điểm tốt. Tôi sẽ giả sử không bận tâm đến một biến khác nếu biểu thức là một cái gì đó đơn giản, ví dụ for(int i = 0; i < n*n; i++){...}bạn sẽ không gán n*ncho một biến?
Celeritas

1
Tôi sẽ và tôi có. Nhưng không phải vì tốc độ. Để dễ đọc. Mã có thể đọc được có xu hướng nhanh.
candied_orange

1
Ngay cả vấn đề phạm vi nó cũng biến mất nếu bạn đánh dấu là một hằng số ( final). Ai quan tâm nếu một hằng số có thực thi trình biên dịch ngăn nó thay đổi có thể truy cập được sau này trong hàm không?
jpmc26

Tôi nghĩ rằng một điều lớn là những gì bạn mong đợi sẽ xảy ra. Tôi biết ví dụ sử dụng sqrt nhưng nếu đó là một chức năng khác thì sao? Là chức năng thuần túy? Bạn luôn mong đợi những giá trị tương tự? Có tác dụng phụ? Là các tác dụng phụ một cái gì đó bạn dự định sẽ xảy ra trên mỗi lần lặp?
Pieter B

38

Một trình biên dịch tốt sẽ tạo ra cùng một mã, vì vậy nếu bạn đang thực hiện, chỉ thực hiện thay đổi nếu nó nằm trong một vòng lặp quan trọng bạn đã thực sự định hình nó và thấy nó tạo ra sự khác biệt. Ngay cả khi trình biên dịch không thể tối ưu hóa nó, như mọi người đã chỉ ra trong các nhận xét về trường hợp có các lệnh gọi hàm, trong phần lớn các tình huống, sự khác biệt về hiệu năng sẽ quá nhỏ để có thể xem xét cho lập trình viên.

Tuy nhiên...

Chúng ta không được quên rằng mã chủ yếu là phương tiện giao tiếp giữa con người và cả hai lựa chọn của bạn đều không giao tiếp với người khác rất tốt. Cái đầu tiên tạo ấn tượng rằng biểu thức cần được tính theo mỗi lần lặp và lần thứ hai trong phần khởi tạo ngụ ý nó sẽ được cập nhật ở đâu đó bên trong vòng lặp, nơi nó thực sự không đổi trong suốt.

Tôi thực sự thích nó được kéo ra phía trên vòng lặp và được thực hiện finalđể làm cho nó rõ ràng ngay lập tức và dồi dào cho bất cứ ai đọc mã. Điều đó cũng không lý tưởng vì nó làm tăng phạm vi của biến, nhưng hàm kèm theo của bạn không nên chứa nhiều hơn vòng lặp đó.


5
Một khi bạn bắt đầu bao gồm các hàm gọi, trình biên dịch sẽ khó khăn hơn nhiều để tối ưu hóa. Trình biên dịch sẽ phải có kiến ​​thức đặc biệt rằng Math.sqrt không có tác dụng phụ.
Peter Green

@PeterGreen Nếu đây là Java, JVM có thể tìm ra nó, nhưng có thể mất một lúc.
chrylis -on đình công-

Câu hỏi được gắn thẻ cả C ++ và Java. Tôi không biết JVM của Java tiên tiến đến mức nào và khi nào nó có thể và không thể tìm ra nó, nhưng tôi biết rằng các trình biên dịch C ++ nói chung không thể tìm ra nó là một hàm thuần túy trừ khi nó có chú thích không chuẩn cho trình biên dịch vì vậy, hoặc phần thân của hàm có thể nhìn thấy và tất cả các hàm được gọi gián tiếp có thể được phát hiện là thuần theo cùng một tiêu chí. Lưu ý: không có hiệu ứng phụ là không đủ để di chuyển nó ra khỏi điều kiện vòng lặp. Các hàm phụ thuộc vào trạng thái toàn cục không thể được chuyển ra khỏi vòng lặp nếu thân vòng lặp có thể sửa đổi trạng thái.
hvd

Sẽ rất thú vị khi Math.sqrt (x) được thay thế bằng Mymodule.SomeNonPureMethodWithSideEffects (x).
Pieter B

9

Như @Karl Bielefeldt đã nói trong câu trả lời của mình, đây thường không phải là vấn đề.

Tuy nhiên, đã có lúc nó là một vấn đề phổ biến trong C và C ++, và một mánh khóe đã xuất hiện để giải quyết vấn đề mà không làm giảm khả năng đọc mã, lặp đi lặp lại0 .

final x = 10;//whatever constant value
for(int i = Math.floor(Math.sqrt(x)); i >= 0; i--) {
  //...do something
}

Bây giờ điều kiện trong mỗi lần lặp chỉ là >= 0mỗi trình biên dịch sẽ biên dịch thành 1 hoặc 2 hướng dẫn lắp ráp. Mỗi CPU được tạo ra trong vài thập kỷ qua nên có các kiểm tra cơ bản như thế này; thực hiện kiểm tra nhanh trên máy x64 của tôi, tôi thấy điều này có thể dự đoán được sẽ biến thành cmpl $0x0, -0x14(%rbp)(giá trị so sánh dài 0 so với thanh ghi rbp đã bù đắp -14) và jl 0x100000f59(chuyển sang hướng dẫn sau vòng lặp nếu so sánh trước đó là đúng đối với thứ 2-arg <1st-arg LINE) .

Lưu ý rằng tôi đã xóa + 1từ Math.floor(Math.sqrt(x)) + 1; để toán học hoạt động, giá trị bắt đầu phải là int i = «iterationCount» - 1. Cũng đáng chú ý là iterator của bạn phải được ký; unsigned intsẽ không hoạt động và có khả năng trình biên dịch cảnh báo.

Sau khi lập trình bằng các ngôn ngữ dựa trên C trong khoảng 20 năm, bây giờ tôi chỉ viết các vòng lặp chỉ mục ngược trừ khi có một lý do cụ thể để lặp lại chỉ mục chuyển tiếp. Ngoài việc kiểm tra đơn giản hơn trong các điều kiện, lặp lại thường cũng là các bước phụ, điều gì sẽ gây rắc rối cho các đột biến mảng-trong khi lặp lại.


1
Tất cả mọi thứ bạn viết là đúng kỹ thuật. Nhận xét về các hướng dẫn có thể gây hiểu nhầm, vì tất cả các CPU hiện đại cũng đã được thiết kế để hoạt động tốt với các vòng lặp chuyển tiếp, bất kể chúng có hướng dẫn đặc biệt cho chúng hay không. Trong mọi trường hợp, hầu hết thời gian thường dành bên trong vòng lặp, không thực hiện lặp đi lặp lại.
Jørgen Fogh

4
Cần lưu ý rằng tối ưu hóa trình biên dịch hiện đại được thiết kế với mã "bình thường". Trong hầu hết các trường hợp, trình biên dịch sẽ tạo mã nhanh như nhau bất kể bạn có sử dụng "thủ thuật" tối ưu hóa hay không. Nhưng một số thủ thuật thực sự có thể cản trở tối ưu hóa tùy thuộc vào mức độ phức tạp của chúng. Nếu sử dụng một số thủ thuật nhất định làm cho mã của bạn dễ đọc hơn hoặc giúp bắt các lỗi phổ biến, điều đó thật tuyệt, nhưng đừng tự lừa mình "nếu tôi viết mã theo cách này thì nó sẽ nhanh hơn". Viết mã như thế nào bạn thường làm, sau đó hồ sơ để tìm nơi bạn cần tối ưu hóa.
0x5453

Cũng lưu ý rằng unsignedcác bộ đếm sẽ hoạt động ở đây nếu bạn sửa đổi kiểm tra (cách dễ nhất là thêm cùng một giá trị cho cả hai bên); ví dụ, đối với bất kỳ sự sụt giảm nào Dec, kiểm tra (i + Dec) >= Decphải luôn có cùng kết quả như signedkiểm tra i >= 0, với cả hai signedunsignedbộ đếm, miễn là ngôn ngữ có các quy tắc bao quanh được xác định rõ cho unsignedcác biến (cụ thể, -n + n == 0phải đúng cho cả hai signedunsigned). Tuy nhiên, lưu ý rằng điều này có thể kém hiệu quả hơn so với >=0kiểm tra đã ký , nếu trình biên dịch không có tối ưu hóa cho nó.
Justin Time 2 phục hồi lại

1
@JustinTime Vâng, yêu cầu đã ký là phần khó nhất; việc thêm một Dechằng số cho cả giá trị bắt đầu và giá trị kết thúc hoạt động nhưng làm cho nó ít trực quan hơn và nếu sử dụng inhư một chỉ mục mảng thì bạn cũng cần phải thực hiện unsigned int arrayI = i - Dec;trong thân vòng lặp. Tôi chỉ sử dụng phép lặp tiến khi bị mắc kẹt với một trình vòng lặp không dấu; thường có i <= count - 1điều kiện để giữ logic song song với các vòng lặp ngược.
Slipp D. Thompson

1
@ SlippD.Thndry Tôi không có nghĩa là thêm Decvào các giá trị bắt đầu và kết thúc cụ thể, nhưng thay đổi kiểm tra điều kiện Decở cả hai bên. for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ Điều này cho phép bạn sử dụng ibình thường trong vòng lặp, đồng thời đảm bảo rằng giá trị thấp nhất có thể ở phía bên trái của điều kiện là 0(để ngăn chặn sự can thiệp từ việc can thiệp). Mặc dù vậy, nó chắc chắn đơn giản hơn nhiều khi sử dụng phép lặp tiến khi làm việc với các bộ đếm không dấu.
Justin Time 2 phục hồi lại

3

Sẽ rất thú vị khi Math.sqrt (x) được thay thế bằng Mymodule.SomeNonPureMethodWithSideEffects (x).

Về cơ bản modus operandi của tôi là: nếu một cái gì đó được mong đợi sẽ luôn cho cùng một giá trị, thì chỉ đánh giá nó một lần. Ví dụ List.Count, nếu danh sách được cho là không thay đổi trong quá trình hoạt động của vòng lặp, thì hãy lấy số đếm bên ngoài vòng lặp thành một biến khác.

Một số "số lượng" này có thể đắt một cách đáng ngạc nhiên, đặc biệt là khi bạn đang xử lý cơ sở dữ liệu. Ngay cả khi bạn đang làm việc trên một tập dữ liệu không được phép thay đổi trong quá trình lặp lại danh sách.


Khi số lượng đắt, bạn không nên sử dụng nó. Thay vào đó, bạn nên làm tương đương vớifor( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt

@BenVoigt Sử dụng một trình vòng lặp chắc chắn là cách tốt nhất cho các hoạt động đó. Tôi chỉ đề cập đến nó để minh họa quan điểm của tôi về việc sử dụng các phương pháp không thuần túy với các tác dụng phụ.
Pieter B

0

Theo tôi đây là ngôn ngữ cụ thể cao. Ví dụ, nếu sử dụng C ++ 11, tôi sẽ nghi ngờ rằng nếu kiểm tra điều kiện là một constexprhàm thì trình biên dịch sẽ rất có khả năng tối ưu hóa nhiều lần thực thi vì nó biết rằng nó sẽ mang lại cùng một giá trị mỗi lần.

Tuy nhiên, nếu lệnh gọi là hàm thư viện thì không constexpr là trình biên dịch thì gần như chắc chắn sẽ thực thi nó trên mỗi lần lặp vì nó không thể suy ra điều này (trừ khi nó là nội tuyến và do đó có thể được suy ra là thuần túy).

Tôi biết ít hơn về Java nhưng được cung cấp bởi JIT, tôi đoán trình biên dịch có đủ thông tin trong thời gian chạy để có khả năng nội tuyến và tối ưu hóa điều kiện. Nhưng điều này sẽ dựa vào thiết kế trình biên dịch tốt và trình biên dịch quyết định vòng lặp này là một bản mẫu tối ưu hóa mà chúng ta chỉ có thể đoán được.

Cá nhân tôi cảm thấy nó thanh lịch hơn một chút khi đặt điều kiện bên trong vòng lặp for nếu bạn có thể, nhưng nếu nó phức tạp tôi sẽ viết nó thành một hàm constexprhoặc inlinehàm, hoặc ngôn ngữ của bạn tương đương để gợi ý rằng hàm này là thuần túy và tối ưu. Điều này làm cho ý định rõ ràng và duy trì kiểu vòng lặp thành ngữ mà không tạo ra một dòng lớn không thể đọc được. Nó cũng cung cấp cho điều kiện kiểm tra một tên nếu đó là chức năng của chính nó để người đọc có thể thấy ngay lập tức một cách logic những gì kiểm tra mà không cần đọc nếu phức tạp.

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.