Dường như với tôi như mọi người không thích một goto
tuyên bố rất nhiều, vì vậy tôi cảm thấy cần phải nói thẳng ra điều này một chút.
Tôi tin rằng mọi người 'cảm xúc' goto
cuối cùng đã hiểu rõ về mã và (quan niệm sai lầm) về ý nghĩa hiệu suất có thể có. Do đó, trước khi trả lời câu hỏi, trước tiên tôi sẽ đi vào một số chi tiết về cách nó được biên dịch.
Như chúng ta đã biết, C # được biên dịch sang IL, sau đó được biên dịch thành trình biên dịch bằng trình biên dịch SSA. Tôi sẽ cung cấp một chút thông tin chi tiết về cách thức hoạt động của tất cả, và sau đó cố gắng tự trả lời câu hỏi.
Từ C # đến IL
Đầu tiên chúng ta cần một đoạn mã C #. Hãy bắt đầu đơn giản:
foreach (var item in array)
{
// ...
break;
// ...
}
Tôi sẽ làm điều này từng bước để cung cấp cho bạn một ý tưởng tốt về những gì xảy ra dưới mui xe.
Bản dịch đầu tiên: từ vòng lặp foreach
tương đương for
(Lưu ý: Tôi đang sử dụng một mảng ở đây, vì tôi không muốn tìm hiểu chi tiết về IDis Dùng - trong trường hợp đó tôi cũng phải sử dụng IEnumerable):
for (int i=0; i<array.Length; ++i)
{
var item = array[i];
// ...
break;
// ...
}
Bản dịch thứ hai: for
và break
được dịch thành tương đương dễ dàng hơn:
int i=0;
while (i < array.Length)
{
var item = array[i];
// ...
break;
// ...
++i;
}
Và bản dịch thứ ba (đây là tương đương với mã IL): chúng tôi thay đổi break
và while
thành một nhánh:
int i=0; // for initialization
startLoop:
if (i >= array.Length) // for condition
{
goto exitLoop;
}
var item = array[i];
// ...
goto exitLoop; // break
// ...
++i; // for post-expression
goto startLoop;
Trong khi trình biên dịch thực hiện những điều này trong một bước duy nhất, nó cung cấp cho bạn cái nhìn sâu sắc về quy trình. Mã IL phát triển từ chương trình C # là bản dịch theo nghĩa đen của mã C # cuối cùng. Bạn có thể tự mình xem tại đây: https://dotnetfiddle.net/QaiLRz (nhấp vào 'xem IL')
Bây giờ, một điều bạn đã quan sát ở đây là trong quá trình, mã trở nên phức tạp hơn. Cách dễ nhất để quan sát điều này là bởi thực tế là chúng ta cần ngày càng nhiều mã để hoàn thành điều tương tự. Bạn cũng có thể cho rằng foreach
, for
, while
và break
thực sự là ngắn tay cho goto
, mà là một phần sự thật.
Từ IL đến Trình biên dịch
Trình biên dịch .NET JIT là trình biên dịch SSA. Tôi sẽ không đi sâu vào tất cả các chi tiết của mẫu SSA ở đây và cách tạo một trình biên dịch tối ưu hóa, nó chỉ là quá nhiều, nhưng có thể cung cấp một sự hiểu biết cơ bản về những gì sẽ xảy ra. Để hiểu sâu hơn, tốt nhất là bắt đầu đọc về tối ưu hóa trình biên dịch (Tôi thích cuốn sách này để giới thiệu ngắn gọn: http://ssabook.gforge.inria.fr/latest/book.pdf ) và LLVM (llvm.org) .
Mọi trình biên dịch tối ưu hóa đều dựa trên thực tế là mã dễ dàng và tuân theo các mẫu có thể dự đoán được . Trong trường hợp các vòng lặp FOR, chúng tôi sử dụng lý thuyết đồ thị để phân tích các nhánh và sau đó tối ưu hóa những thứ như cycli trong các nhánh của chúng tôi (ví dụ như các nhánh ngược).
Tuy nhiên, bây giờ chúng tôi có các chi nhánh chuyển tiếp để thực hiện các vòng lặp của chúng tôi. Như bạn có thể đoán, đây thực sự là một trong những bước đầu tiên mà JIT sẽ sửa chữa, như thế này:
int i=0; // for initialization
if (i >= array.Length) // for condition
{
goto endOfLoop;
}
startLoop:
var item = array[i];
// ...
goto endOfLoop; // break
// ...
++i; // for post-expression
if (i >= array.Length) // for condition
{
goto startLoop;
}
endOfLoop:
// ...
Như bạn có thể thấy, bây giờ chúng ta có một nhánh lạc hậu, đó là vòng lặp nhỏ của chúng ta. Điều duy nhất vẫn còn khó chịu ở đây là chi nhánh mà chúng tôi đã kết thúc do break
tuyên bố của chúng tôi . Trong một số trường hợp, chúng ta có thể di chuyển cái này theo cùng một cách, nhưng trong những trường hợp khác, nó vẫn ở đó.
Vậy tại sao trình biên dịch làm điều này? Chà, nếu chúng ta có thể hủy đăng ký vòng lặp, chúng ta có thể vector hóa nó. Chúng tôi thậm chí có thể chứng minh rằng chỉ có các hằng số được thêm vào, có nghĩa là toàn bộ vòng lặp của chúng tôi có thể tan biến vào không khí mỏng. Tóm lại: bằng cách làm cho các mẫu có thể dự đoán được (bằng cách làm cho các nhánh có thể dự đoán được), chúng ta có thể chứng minh rằng các điều kiện nhất định giữ trong vòng lặp của chúng ta, điều đó có nghĩa là chúng ta có thể làm phép thuật trong quá trình tối ưu hóa JIT.
Tuy nhiên, các nhánh có xu hướng phá vỡ các mô hình dự đoán tốt đẹp này, đó là điều tối ưu hóa do đó không thích - một sự không thích. Phá vỡ, tiếp tục, goto - tất cả họ đều có ý định phá vỡ các mô hình dự đoán này - và do đó không thực sự 'tốt đẹp'.
Tại thời điểm này, bạn cũng nên nhận ra rằng một đơn giản dễ foreach
dự đoán hơn sau đó là một loạt các goto
tuyên bố đi khắp nơi. Xét về (1) khả năng đọc và (2) từ góc độ tối ưu hóa, cả hai đều là giải pháp tốt hơn.
Một điều đáng nói nữa là nó rất phù hợp để tối ưu hóa trình biên dịch để gán các thanh ghi cho các biến (một quá trình gọi là cấp phát thanh ghi ). Như bạn có thể biết, chỉ có một số lượng thanh ghi hữu hạn trong CPU của bạn và chúng là phần bộ nhớ nhanh nhất trong phần cứng của bạn. Các biến được sử dụng trong mã trong vòng lặp bên trong nhất, có nhiều khả năng được đăng ký được gán, trong khi các biến bên ngoài vòng lặp của bạn ít quan trọng hơn (vì mã này có thể bị ảnh hưởng ít hơn).
Giúp đỡ, quá phức tạp ... tôi nên làm gì?
Điểm mấu chốt là bạn phải luôn luôn sử dụng các cấu trúc ngôn ngữ mà bạn có theo ý của bạn, điều này thường sẽ (ngụ ý) xây dựng các mẫu có thể dự đoán được cho trình biên dịch của bạn. Cố gắng tránh các chi nhánh lạ nếu có thể (cụ thể là: break
, continue
, goto
hoặc return
ở giữa không có gì).
Tin tốt ở đây là những mẫu có thể dự đoán này vừa dễ đọc (đối với con người) vừa dễ phát hiện (đối với trình biên dịch).
Một trong những mẫu đó được gọi là SESE, viết tắt của Single Entry Single Exit.
Và bây giờ chúng ta đến câu hỏi thực sự.
Hãy tưởng tượng rằng bạn có một cái gì đó như thế này:
// a is a variable.
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a)
{
// break everything
}
}
}
Cách dễ nhất để biến điều này thành một mô hình có thể dự đoán được là chỉ cần loại bỏ if
hoàn toàn:
int i, j;
for (i=0; i<100 && i*j <= a; ++i)
{
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
}
Trong các trường hợp khác, bạn cũng có thể chia phương thức thành 2 phương thức:
// Outer loop in method 1:
for (i=0; i<100 && processInner(i); ++i)
{
}
private bool processInner(int i)
{
int j;
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
return i*j<=a;
}
Biến tạm thời? Tốt, xấu hay xấu?
Bạn thậm chí có thể quyết định trả về một boolean từ trong vòng lặp (nhưng cá nhân tôi thích biểu mẫu SESE hơn vì đó là cách trình biên dịch sẽ nhìn thấy nó và tôi nghĩ rằng nó dễ đọc hơn).
Một số người nghĩ rằng việc sử dụng một biến tạm thời sẽ sạch hơn và đề xuất một giải pháp như thế này:
bool more = true;
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a) { more = false; break; } // yuck.
// ...
}
if (!more) { break; } // yuck.
// ...
}
// ...
Cá nhân tôi phản đối cách tiếp cận này. Nhìn lại về cách mã được biên dịch. Bây giờ hãy nghĩ về những gì nó sẽ làm với những mẫu đẹp, có thể dự đoán được. Lấy tấm hình?
Phải, để tôi đánh vần nó ra. Điều gì sẽ xảy ra là:
- Trình biên dịch sẽ viết ra mọi thứ như các nhánh.
- Là một bước tối ưu hóa, trình biên dịch sẽ thực hiện phân tích luồng dữ liệu trong nỗ lực loại bỏ
more
biến lạ chỉ xảy ra được sử dụng trong luồng điều khiển.
- Nếu thành công, biến
more
sẽ bị loại khỏi chương trình và chỉ còn lại các nhánh. Các nhánh này sẽ được tối ưu hóa, vì vậy bạn sẽ chỉ nhận được một nhánh duy nhất ra khỏi vòng lặp bên trong.
- Nếu không hiệu quả, biến
more
chắc chắn được sử dụng trong vòng lặp bên trong nhất, vì vậy nếu trình biên dịch sẽ không tối ưu hóa nó đi, nó có khả năng cao được phân bổ cho một thanh ghi (ăn hết bộ nhớ đăng ký có giá trị).
Vì vậy, để tóm tắt: trình tối ưu hóa trong trình biên dịch của bạn sẽ gặp rất nhiều rắc rối khi chỉ ra rằng more
nó chỉ được sử dụng cho luồng điều khiển và trong trường hợp tốt nhất sẽ chuyển nó sang một nhánh duy nhất bên ngoài vòng.
Nói cách khác, trường hợp tốt nhất là nó sẽ kết thúc với tương đương với điều này:
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a) { goto exitLoop; } // perhaps add a comment
// ...
}
// ...
}
exitLoop:
// ...
Ý kiến cá nhân của tôi về điều này khá đơn giản: nếu đây là những gì chúng tôi dự định, hãy làm cho thế giới dễ dàng hơn cho cả trình biên dịch và khả năng đọc, và viết nó ngay lập tức.
tl; dr:
Dòng dưới cùng:
- Sử dụng một điều kiện đơn giản trong vòng lặp for của bạn nếu có thể. Bám sát các cấu trúc ngôn ngữ cấp cao mà bạn có sẵn theo ý của bạn càng nhiều càng tốt.
- Nếu mọi thứ đều thất bại và bạn đang trái với một trong hai
goto
hoặc bool more
, thích cũ.