ดูเหมือนว่าฉันจะชอบผู้คนที่ไม่ชอบgoto
คำแถลงมากดังนั้นฉันจึงรู้สึกว่าจำเป็นที่จะต้องทำให้เรื่องนี้ตรงออกไปเล็กน้อย
ฉันเชื่อว่าคนที่ 'อารมณ์' มีเกี่ยวกับgoto
ในที่สุดก็ถึงความเข้าใจในโค้ดและ (ความเข้าใจผิด) เกี่ยวกับความเป็นไปได้ในการใช้งาน ก่อนที่จะตอบคำถามฉันจะเข้าไปดูรายละเอียดเกี่ยวกับวิธีการรวบรวม
อย่างที่เราทราบกันดีว่า C # นั้นถูกคอมไพล์ไปที่ IL ซึ่งจะถูกคอมไพล์ให้กับแอสเซมเบลอร์ด้วย SSA ฉันจะให้ข้อมูลเชิงลึกเกี่ยวกับวิธีการทำงานทั้งหมดนี้แล้วลองตอบคำถามเอง
จาก C # ถึง IL
ก่อนอื่นเราต้องใช้รหัส C # เริ่มง่าย ๆ :
foreach (var item in array)
{
// ...
break;
// ...
}
ฉันจะทำตามขั้นตอนนี้เพื่อให้คุณมีความคิดที่ดีว่าเกิดอะไรขึ้นภายใต้ประทุน
การแปลครั้งแรก: จากforeach
เป็นfor
ลูปที่เทียบเท่า(หมายเหตุ: ฉันกำลังใช้อาร์เรย์ที่นี่เพราะฉันไม่ต้องการทราบรายละเอียดของ IDisposable - ในกรณีนี้ฉันต้องใช้ IEnumerable ด้วย):
for (int i=0; i<array.Length; ++i)
{
var item = array[i];
// ...
break;
// ...
}
การแปลครั้งที่สอง: for
และbreak
ถูกแปลเป็นภาษาที่เทียบเท่าได้ง่ายขึ้น:
int i=0;
while (i < array.Length)
{
var item = array[i];
// ...
break;
// ...
++i;
}
และการแปลที่สาม (นี้เป็นเทียบเท่าของรหัส IL): เราเปลี่ยนbreak
และwhile
เข้าไปในสาขา:
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;
ในขณะที่คอมไพเลอร์ทำสิ่งเหล่านี้ในขั้นตอนเดียวมันช่วยให้คุณเข้าใจกระบวนการ รหัส IL ที่วิวัฒนาการมาจากโปรแกรม C # เป็นการแปลตามตัวอักษรของรหัส C # ครั้งสุดท้าย คุณสามารถเห็นตัวคุณเองได้ที่นี่: https://dotnetfiddle.net/QaiLRz (คลิก 'ดู IL')
ตอนนี้สิ่งหนึ่งที่คุณสังเกตได้ที่นี่คือในระหว่างกระบวนการรหัสจะซับซ้อนมากขึ้น วิธีที่ง่ายที่สุดในการสังเกตสิ่งนี้คือความจริงที่ว่าเราต้องการรหัสมากขึ้นเพื่อทำสิ่งเดียวกัน นอกจากนี้คุณยังอาจโต้แย้งว่าforeach
, for
, while
และbreak
เป็นจริงสั้นมือสำหรับgoto
ซึ่งเป็นจริงบางส่วน
จาก IL ถึง Assembler
คอมไพเลอร์. NET JIT เป็นคอมไพเลอร์ SSA ฉันจะไม่เข้าไปดูรายละเอียดทั้งหมดของฟอร์ม SSA ที่นี่และวิธีสร้างคอมไพเลอร์ที่ปรับให้เหมาะสมมันมากเกินไป แต่สามารถให้ความเข้าใจพื้นฐานเกี่ยวกับสิ่งที่จะเกิดขึ้น เพื่อความเข้าใจที่ลึกซึ้งยิ่งขึ้นควรเริ่มต้นอ่านการเพิ่มประสิทธิภาพคอมไพเลอร์ (ฉันชอบหนังสือเล่มนี้เพื่อแนะนำสั้น ๆ : http://ssabook.gforge.inria.fr/latest/book.pdf ) และ LLVM (llvm.org) .
คอมไพเลอร์ปรับให้เหมาะสมทุกครั้งอาศัยความจริงที่ว่ารหัสนั้นง่ายและเป็นไปตามรูปแบบที่สามารถคาดเดาได้ ในกรณีของการวนรอบ FOR เราใช้ทฤษฎีกราฟเพื่อวิเคราะห์สาขาและเพิ่มประสิทธิภาพของ cycli ในสาขาของเรา (เช่นสาขาย้อนหลัง)
อย่างไรก็ตามตอนนี้เรามีสาขาไปข้างหน้าเพื่อใช้ลูปของเรา ตามที่คุณอาจเดาได้จริง ๆ แล้วนี่เป็นหนึ่งในขั้นตอนแรกที่ JIT จะแก้ไขเช่นนี้:
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:
// ...
อย่างที่คุณเห็นตอนนี้เรามีสาขาย้อนกลับซึ่งก็คือวงเล็ก ๆ ของเรา สิ่งเดียวที่ยังน่ารังเกียจอยู่ที่นี่คือสาขาที่เราลงเอยด้วยbreak
คำแถลงของเรา ในบางกรณีเราสามารถย้ายสิ่งนี้ในลักษณะเดียวกัน แต่ในบางกรณีเราสามารถอยู่ได้
ดังนั้นทำไมคอมไพเลอร์ถึงทำเช่นนี้? ถ้าเราสามารถคลี่วงเราอาจจะสามารถทำให้เป็นเวกเตอร์ได้ เราอาจสามารถพิสูจน์ได้ว่ามีเพียงค่าคงที่ที่เพิ่มเข้ามาซึ่งหมายความว่าลูปทั้งหมดของเราอาจหายไปในอากาศ เพื่อสรุป: โดยการทำให้รูปแบบที่สามารถคาดการณ์ได้ (โดยทำให้สาขาสามารถคาดเดาได้) เราสามารถพิสูจน์ได้ว่าเงื่อนไขบางอย่างอยู่ในลูปของเราซึ่งหมายความว่าเราสามารถทำเวทมนต์ในระหว่างการเพิ่มประสิทธิภาพ JIT
อย่างไรก็ตามกิ่งก้านมีแนวโน้มที่จะทำลายรูปแบบที่คาดการณ์ได้ดีซึ่งเป็นสิ่งที่เครื่องมือเพิ่มประสิทธิภาพจึงไม่ชอบ แตกหักดำเนินการต่อไปถึง - พวกเขาทั้งหมดตั้งใจที่จะทำลายรูปแบบที่คาดการณ์ได้เหล่านี้ - ดังนั้นจึงไม่ดีจริงๆ
คุณควรตระหนักว่า ณ จุดนี้ความเรียบง่ายforeach
สามารถคาดเดาได้มากกว่าจากนั้นเป็นgoto
ประโยคที่ไปทั่วสถานที่ ในแง่ของ (1) ความสามารถในการอ่านและ (2) จากมุมมองของเครื่องมือเพิ่มประสิทธิภาพมันเป็นทางออกที่ดีกว่า
อีกสิ่งที่มูลค่าการกล่าวขวัญก็คือว่ามันมีความเกี่ยวข้องมากสำหรับการเพิ่มประสิทธิภาพคอมไพเลอร์ที่จะลงทะเบียนกำหนดตัวแปร (กระบวนการที่เรียกว่าการจัดสรรลงทะเบียน ) ดังที่คุณอาจทราบว่ามีจำนวน จำกัด ในการลงทะเบียน CPU ของคุณและพวกเขาเป็นหน่วยความจำที่เร็วที่สุดในฮาร์ดแวร์ของคุณ ตัวแปรที่ใช้ในรหัสที่อยู่ในลูปส่วนใหญ่มีแนวโน้มที่จะได้รับการลงทะเบียนในขณะที่ตัวแปรที่อยู่นอกลูปของคุณมีความสำคัญน้อยกว่า (เพราะรหัสนี้อาจจะตีน้อยกว่า)
ช่วยซับซ้อนเกินไป ... ฉันควรทำอย่างไรดี?
บรรทัดล่างคือคุณควรใช้ภาษาสร้างที่คุณมีในการกำจัดของคุณซึ่งมักจะ (โดยนัย) สร้างรูปแบบที่คาดการณ์ได้สำหรับคอมไพเลอร์ของคุณ พยายามหลีกเลี่ยงการสาขาแปลกถ้าเป็นไปได้ (เฉพาะ: break
, continue
, goto
หรือreturn
ในช่วงกลางของอะไร)
ข่าวดีก็คือรูปแบบที่คาดการณ์ได้เหล่านี้มีทั้งที่อ่านง่าย (สำหรับมนุษย์) และง่ายต่อการตรวจจับ (สำหรับคอมไพเลอร์)
หนึ่งในรูปแบบเหล่านั้นเรียกว่า SESE ซึ่งย่อมาจาก Single Entry Single Exit
และตอนนี้เรามาถึงคำถามที่แท้จริง
ลองนึกภาพว่าคุณมีสิ่งนี้:
// a is a variable.
for (int i=0; i<100; ++i)
{
for (int j=0; j<100; ++j)
{
// ...
if (i*j > a)
{
// break everything
}
}
}
วิธีที่ง่ายที่สุดในการทำให้รูปแบบนี้สามารถคาดเดาได้คือเพียงกำจัดสิ่งที่ไม่if
สมบูรณ์ออกไป:
int i, j;
for (i=0; i<100 && i*j <= a; ++i)
{
for (j=0; j<100 && i*j <= a; ++j)
{
// ...
}
}
ในกรณีอื่นคุณสามารถแยกวิธีเป็น 2 วิธี
// 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;
}
ตัวแปรชั่วคราว? ดีเลวหรือน่าเกลียด?
คุณอาจตัดสินใจคืนบูลีนจากภายในลูป (แต่โดยส่วนตัวแล้วฉันชอบแบบฟอร์ม SESE เพราะนั่นเป็นวิธีที่คอมไพเลอร์จะเห็นมันและฉันคิดว่ามันอ่านง่ายกว่า)
บางคนคิดว่าการใช้ตัวแปรชั่วคราวนั้นสะอาดกว่าและเสนอวิธีแก้ปัญหาเช่นนี้:
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.
// ...
}
// ...
ส่วนตัวผมไม่เห็นด้วยกับแนวทางนี้ ดูอีกครั้งเกี่ยวกับวิธีการรวบรวมรหัส ตอนนี้คิดเกี่ยวกับสิ่งนี้จะทำอย่างไรกับรูปแบบที่ดีและคาดเดาได้เหล่านี้ รับภาพหรือไม่
ใช่ให้ฉันสะกดมันออกมา จะเกิดอะไรขึ้นคือ:
- คอมไพเลอร์จะเขียนทุกอย่างเป็นสาขา
- ในขั้นตอนการปรับให้เหมาะสมคอมไพเลอร์จะทำการวิเคราะห์การไหลของข้อมูลในความพยายามที่จะลบ
more
ตัวแปรแปลก ๆที่เกิดขึ้นเพื่อใช้ในโฟลว์การควบคุมเท่านั้น
- หากประสบความสำเร็จตัวแปร
more
จะถูกลบออกจากโปรแกรมและจะเหลือเพียงกิ่งก้านเท่านั้น สาขาเหล่านี้จะได้รับการปรับปรุงเพื่อให้คุณได้รับเพียงสาขาเดียวจากวงใน
- หากไม่สำเร็จตัวแปร
more
จะถูกใช้ในลูปภายในสุดอย่างแน่นอนดังนั้นหากคอมไพเลอร์ไม่ปรับให้เหมาะสมมันมีโอกาสสูงที่จะถูกจัดสรรให้กับรีจิสเตอร์ (ซึ่งกินหน่วยความจำรีจิสเตอร์ที่มีค่า)
ดังนั้นเพื่อสรุป: เครื่องมือเพิ่มประสิทธิภาพในคอมไพเลอร์ของคุณจะเข้าสู่นรกของปัญหามากมายที่จะคิดออกว่าmore
จะใช้สำหรับโฟลว์การควบคุมเท่านั้นและในกรณีสถานการณ์ที่ดีที่สุดจะแปลเป็นสาขาเดียวนอกด้านนอกสำหรับ ห่วง
กล่าวอีกนัยหนึ่งสถานการณ์กรณีที่ดีที่สุดคือมันจะจบลงด้วยสิ่งนี้:
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:
// ...
ความเห็นส่วนตัวของฉันเกี่ยวกับเรื่องนี้ค่อนข้างง่าย: ถ้านี่คือสิ่งที่เราตั้งใจมาโดยตลอดลองทำให้โลกนี้ง่ายขึ้นสำหรับคอมไพเลอร์และการอ่านได้และเขียนมันทันที
TL; DR:
บรรทัดล่างสุด:
- ใช้เงื่อนไขง่ายๆในการวนรอบถ้าเป็นไปได้ ยึดติดกับโครงสร้างภาษาระดับสูงที่คุณมีให้มากที่สุด
- หากทุกอย่างล้มเหลวและคุณจะเหลืออย่างใดอย่างหนึ่ง
goto
หรือbool more
ชอบอดีต