ควรย้ายเงื่อนไขที่ไม่น่าสนใจไปยังส่วนเริ่มต้นของลูปหรือไม่?


21

ฉันได้ความคิดนี้จากคำถามนี้ใน stackoverflow.com

รูปแบบต่อไปนี้เป็นเรื่องธรรมดา:

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

จุดที่ฉันพยายามทำคือข้อความที่มีเงื่อนไขเป็นเรื่องที่ซับซ้อนและไม่เปลี่ยนแปลง

มันเป็นการดีกว่าที่จะประกาศในส่วนเริ่มต้นของลูปเช่นนี้?

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

ชัดเจนกว่านี้ไหม

เกิดอะไรขึ้นถ้านิพจน์เงื่อนไขนั้นง่ายเช่น

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

47
ทำไมไม่เพียงแค่ย้ายไปที่บรรทัดก่อนลูปจากนั้นคุณสามารถตั้งชื่อที่สมเหตุสมผลได้
jonrsharpe

2
@ Mehrdad: พวกเขาไม่เท่ากัน ถ้าxมีขนาดใหญ่ในขนาดเท่ากับMath.floor(Math.sqrt(x))+1 Math.floor(Math.sqrt(x)):-)
..

5
@jonrsharpe เพราะนั่นจะขยายขอบเขตของตัวแปร ฉันไม่จำเป็นต้องพูดว่านั่นเป็นเหตุผลที่ดีที่จะไม่ทำ แต่นั่นคือเหตุผลที่บางคนไม่
Kevin Krumwiede

3
@KevinKrumwiede หากขอบเขตเป็นข้อกังวล จำกัด โดยใส่รหัสลงในบล็อกของตัวเองเช่น{ x=whatever; for (...) {...} }หรือดีกว่าให้พิจารณาว่ามีเพียงพอที่จะต้องดำเนินการแยกต่างหากหรือไม่
Blrfl

2
@jonrsharpe คุณสามารถตั้งชื่อที่เหมาะสมเมื่อประกาศในส่วน init ด้วย ไม่บอกว่าฉันจะเอามันไป มันยังง่ายต่อการอ่านถ้าแยกจากกัน
JollyJoker

คำตอบ:


62

สิ่งที่ฉันทำคืออะไรเช่นนี้:

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
    }
}

สุจริตเหตุผลเดียวที่ดีในการเริ่มต้นการบีบอัดj(ตอนนี้limit) ลงในส่วนหัวของลูปคือการกำหนดขอบเขตอย่างถูกต้อง ทั้งหมดที่ใช้เพื่อทำให้ปัญหาที่ไม่ได้เป็นขอบเขตการปิดล้อมที่ดี

ฉันสามารถชื่นชมความต้องการที่จะเร็ว แต่อย่าเสียสละความสามารถในการอ่านโดยไม่มีเหตุผลที่ดี

แน่นอนว่าคอมไพเลอร์อาจปรับให้เหมาะสมการกำหนดค่าเริ่มต้นหลาย vars อาจถูกกฎหมาย แต่ลูปนั้นยากพอที่จะทำการดีบักอย่างที่มันเป็น กรุณาใจดีกับมนุษย์ ถ้าสิ่งนี้ทำให้เราช้าลงจริง ๆ ก็เป็นเรื่องดีที่ได้เข้าใจมันเพียงพอที่จะแก้ไข


จุดดี. ฉันคิดว่าจะไม่รบกวนตัวแปรอื่นถ้านิพจน์นั้นเป็นอะไรที่ง่ายตัวอย่างเช่นfor(int i = 0; i < n*n; i++){...}คุณจะไม่กำหนดn*nตัวแปรให้คุณ
Celeritas

1
ฉันจะและฉันมี แต่ไม่ใช่สำหรับความเร็ว สำหรับการอ่าน รหัสที่อ่านได้มีแนวโน้มที่จะเป็นของตัวเองอย่างรวดเร็ว
candied_orange

1
แม้ปัญหาการกำหนดขอบเขตจะหายไปหากคุณทำเครื่องหมายว่าเป็นค่าคงที่ ( final) ใครจะสนใจว่าค่าคงที่ที่มีการบังคับใช้คอมไพเลอร์ซึ่งป้องกันไม่ให้มีการเปลี่ยนแปลงนั้นจะสามารถเข้าถึงได้ในฟังก์ชั่นในภายหลัง
jpmc26

ฉันคิดว่าเรื่องใหญ่คือสิ่งที่คุณคาดหวังว่าจะเกิดขึ้น ฉันรู้ว่าตัวอย่างใช้ sqrt แต่ถ้ามันเป็นฟังก์ชั่นอื่น? ฟังก์ชั่นบริสุทธิ์หรือไม่ คุณคาดหวังคุณค่าที่เหมือนกันหรือไม่? มีผลข้างเคียงหรือไม่? ด้านข้างมีผลกับสิ่งที่คุณตั้งใจจะทำในทุก ๆ รอบหรือไม่?
Pieter B

38

คอมไพเลอร์ที่ดีจะสร้างโค้ดเดียวกันด้วยวิธีใดวิธีหนึ่งดังนั้นหากคุณต้องการประสิทธิภาพให้ทำการเปลี่ยนแปลงหากมันอยู่ในลูปวิกฤติและคุณได้ทำโปรไฟล์จริงและพบว่ามันสร้างความแตกต่าง แม้ว่าคอมไพเลอร์จะไม่สามารถปรับให้เหมาะสมเพราะคนได้ชี้ให้เห็นในความคิดเห็นเกี่ยวกับกรณีที่มีการเรียกใช้ฟังก์ชั่นในสถานการณ์ส่วนใหญ่ความแตกต่างของประสิทธิภาพจะเล็กเกินไปที่จะคุ้มค่ากับการพิจารณาของโปรแกรมเมอร์

อย่างไรก็ตาม ...

เราต้องไม่ลืมว่ารหัสนั้นเป็นสื่อกลางในการสื่อสารระหว่างมนุษย์และตัวเลือกของคุณทั้งสองไม่สามารถสื่อสารกับมนุษย์คนอื่นได้เป็นอย่างดี อันแรกให้ความรู้สึกว่านิพจน์จะต้องคำนวณทุกครั้งและส่วนที่สองในส่วนเริ่มต้นบอกเป็นนัยว่ามันจะได้รับการปรับปรุงที่ไหนสักแห่งในวงที่มันคงที่ตลอด

ฉันอยากจะดึงมันออกมาเหนือลูปและfinalทำให้มันชัดเจนขึ้นอย่างทันทีทันใดสำหรับทุกคนที่อ่านโค้ด ไม่เหมาะอย่างยิ่งเนื่องจากจะเป็นการเพิ่มขอบเขตของตัวแปร แต่ฟังก์ชันการล้อมรอบของคุณไม่ควรมีมากไปกว่าการวนซ้ำนั้น


5
เมื่อคุณเริ่มรวมถึงการเรียกใช้ฟังก์ชันมันจะทำให้คอมไพเลอร์เพิ่มประสิทธิภาพได้ยากขึ้น คอมไพเลอร์จะต้องมีความรู้พิเศษว่า Math.sqrt ไม่มีผลข้างเคียง
ปีเตอร์กรีน

@PeterGreen หากนี่คือ Java JVM สามารถค้นหาได้ แต่อาจใช้เวลาสักครู่
chrylis -on strike-

คำถามถูกติดแท็กทั้ง C ++ และ Java ฉันไม่ทราบว่า JVM ของ Java ขั้นสูงเป็นอย่างไรและเมื่อใดที่สามารถและไม่สามารถหาได้ แต่ฉันรู้ว่าคอมไพเลอร์ C ++ โดยทั่วไปไม่สามารถคิดออกได้ แต่ฟังก์ชั่นนั้นบริสุทธิ์ ดังนั้นหรือมองเห็นร่างกายของฟังก์ชันและสามารถเรียกใช้ฟังก์ชันทางอ้อมที่เรียกว่าบริสุทธิ์โดยใช้เกณฑ์เดียวกัน หมายเหตุ: ผลข้างเคียงไม่เพียงพอที่จะย้ายออกจากสภาพลูป ฟังก์ชั่นที่ขึ้นอยู่กับสถานะทั่วโลกไม่สามารถย้ายออกจากลูปได้เช่นกันหากตัวลูปอาจแก้ไขสถานะได้
hvd

มันน่าสนใจเมื่อ Math.sqrt (x) ถูกแทนที่ด้วย Mymodule.SomeNonPureMethodWithSideEffects (x)
Pieter B

9

ดังที่ @Karl Bielefeldt พูดในคำตอบของเขานี่มักจะไม่ใช่ปัญหา

แต่ก็เป็นช่วงเวลาหนึ่งที่เป็นปัญหาที่พบบ่อยใน C และ C ++ และเคล็ดลับมาเกี่ยวกับไปทางด้านขั้นตอนการแก้ไขปัญหาโดยไม่ต้องลดรหัส readability- ย้ำถอยหลังลงไป0

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

ตอนนี้เงื่อนไขในการวนซ้ำทุกครั้งเป็นเพียง>= 0ที่คอมไพเลอร์ทุกคนจะรวบรวมเป็น 1 หรือ 2 คำแนะนำการชุมนุม ซีพียูทุกตัวในช่วงสองสามทศวรรษที่ผ่านมาควรมีการตรวจสอบขั้นพื้นฐานเช่นนี้ ทำการตรวจสอบอย่างรวดเร็วบนเครื่อง x64 ของฉันฉันเห็นสิ่งนี้กลายเป็นสิ่งที่คาดการณ์ได้cmpl $0x0, -0x14(%rbp)(ค่า long-int- เปรียบเทียบ 0 เทียบกับ register rbp offsetted -14) และjl 0x100000f59(ข้ามไปยังคำสั่งต่อไปนี้ลูปถ้าการเปรียบเทียบก่อนหน้านี้เป็นจริงสำหรับ <1st-หาเรื่อง”)

โปรดทราบว่าฉันลบออก+ 1จากMath.floor(Math.sqrt(x)) + 1; int i = «iterationCount» - 1เพื่อให้ทางคณิตศาสตร์ที่จะออกมาทำงานค่าเริ่มต้นควรจะเป็น สิ่งที่ควรสังเกตคือตัววนซ้ำของคุณต้องลงนาม unsigned intจะไม่ทำงานและน่าจะคอมไพเลอร์เตือน

หลังจากการเขียนโปรแกรมในภาษาที่ใช้ภาษา C เป็นเวลา ~ 20 ปีตอนนี้ฉันเขียนลูปย้อนกลับดัชนีซ้ำแล้วซ้ำอีกเว้นแต่มีเหตุผลเฉพาะในการส่งต่อดัชนีซ้ำ นอกจากการตรวจสอบที่ง่ายกว่าในเงื่อนไขแล้วการทำซ้ำย้อนกลับยังมักจะทำตามขั้นตอนด้านที่จะทำให้เกิดการกลายพันธุ์ของอาร์เรย์ในขณะที่ทำซ้ำ


1
ทุกสิ่งที่คุณเขียนนั้นถูกต้องทางเทคนิค ความคิดเห็นเกี่ยวกับคำแนะนำอาจทำให้เข้าใจผิด แต่เนื่องจากซีพียูสมัยใหม่ทั้งหมดได้รับการออกแบบให้ทำงานได้ดีกับลูปการวนซ้ำไปข้างหน้าโดยไม่คำนึงว่าพวกเขามีคำสั่งพิเศษสำหรับพวกเขาหรือไม่ ไม่ว่าในกรณีใด ๆ เวลาส่วนใหญ่มักจะใช้เวลาในการวนซ้ำไม่ใช่การวนซ้ำ
Jørgen Fogh

4
ควรสังเกตว่าการเพิ่มประสิทธิภาพคอมไพเลอร์ที่ทันสมัยได้รับการออกแบบด้วยรหัส "ปกติ" ในใจ ในกรณีส่วนใหญ่คอมไพเลอร์จะสร้างรหัสที่รวดเร็วโดยไม่คำนึงว่าคุณใช้ "เทคนิค" การปรับให้เหมาะสมหรือไม่ แต่เทคนิคบางอย่างอาจขัดขวางการปรับให้เหมาะสมขึ้นอยู่กับความซับซ้อน หากใช้เทคนิคบางอย่างทำให้โค้ดของคุณอ่านง่ายขึ้นหรือช่วยดักจับข้อผิดพลาดทั่วไปดีมาก แต่อย่าหลอกตัวเองให้คิดว่า "ถ้าฉันเขียนโค้ดด้วยวิธีนี้มันจะเร็วขึ้น" เขียนรหัสตามปกติแล้วโปรไฟล์เพื่อค้นหาสถานที่ที่คุณต้องการเพิ่มประสิทธิภาพ
0x5453

นอกจากนี้โปรดทราบว่าunsignedตัวนับจะทำงานที่นี่หากคุณแก้ไขการตรวจสอบ (วิธีที่ง่ายที่สุดคือการเพิ่มค่าเดียวกันทั้งสองด้าน) ตัวอย่างเช่นสำหรับการลดลงใด ๆDecการตรวจสอบ(i + Dec) >= Decควรมีผลเช่นเดียวกับการsignedตรวจสอบi >= 0กับทั้งสองsignedและunsignedตัวนับตราบใดที่ภาษามีกฎการขึ้นบรรทัดใหม่ที่ชัดเจนสำหรับunsignedตัวแปร (โดยเฉพาะ-n + n == 0จะต้องเป็นจริงสำหรับทั้งสองsignedและunsigned) อย่างไรก็ตามโปรดทราบว่านี่อาจมีประสิทธิภาพน้อยกว่าการ>=0ตรวจสอบที่ลงนามหากคอมไพเลอร์ไม่มีการปรับให้เหมาะสม
Justin เวลา 2 Reinstate Monica

1
@ จัสตินไทม์ใช่ข้อกำหนดที่ลงนามแล้วนั้นเป็นส่วนที่ยากที่สุด การเพิ่มDecค่าคงที่ให้กับทั้งค่าเริ่มต้นและค่าสิ้นสุดนั้นทำงานได้ง่ายกว่าและถ้าใช้iเป็นดัชนีอาร์เรย์คุณจะต้องทำunsigned int arrayI = i - Dec;ในลูปเนื้อหาด้วย ฉันเพิ่งใช้การทำซ้ำไปข้างหน้าเมื่อติดกับตัววนซ้ำที่ไม่ได้ลงชื่อ บ่อยครั้งที่มีi <= count - 1เงื่อนไขเพื่อให้ตรรกะขนานกับลูปย้อนกลับซ้ำ
Slipp D. Thompson

1
@ SlippDThompson ฉันไม่ได้หมายถึงการเพิ่มDecค่าเริ่มต้นและจุดสิ้นสุดโดยเฉพาะ แต่เลื่อนการตรวจสอบสภาพโดยDecทั้งสองด้าน for (unsigned i = N - 1; i + 1 >= 1; i--) /*...*/ สิ่งนี้ช่วยให้คุณสามารถใช้งานได้iตามปกติในลูปในขณะที่รับประกันได้ว่าค่าต่ำสุดที่เป็นไปได้ทางด้านซ้ายของเงื่อนไขคือ0(เพื่อป้องกันการพันด้วยการแทรกซึม) มันแน่นอนมากง่ายต่อการใช้ซ้ำไปข้างหน้าเมื่อทำงานกับเคาน์เตอร์ไม่ได้ลงนาม แต่
Justin เวลา 2 Reinstate Monica

3

มันน่าสนใจเมื่อ Math.sqrt (x) ถูกแทนที่ด้วย Mymodule.SomeNonPureMethodWithSideEffects (x)

โดยทั่วไปตัวถูกดำเนินการ modus ของฉันคือ: หากสิ่งที่คาดว่าจะให้ค่าเดียวกันเสมอแล้วประเมินเพียงครั้งเดียว ตัวอย่างเช่น List.Count หากรายการไม่ควรเปลี่ยนระหว่างการดำเนินการของลูปดังนั้นให้นับจำนวนภายนอกลูปเป็นตัวแปรอื่น

"จำนวน" เหล่านี้บางอย่างอาจมีราคาแพงโดยเฉพาะอย่างยิ่งเมื่อคุณจัดการกับฐานข้อมูล แม้ว่าคุณกำลังทำงานกับชุดข้อมูลที่ไม่ควรเปลี่ยนแปลงในระหว่างการวนซ้ำของรายการ


เมื่อการนับมีราคาแพงคุณไม่ควรใช้เลย แต่คุณควรจะทำสิ่งที่เทียบเท่าfor( auto it = begin(dataset); !at_end(it); ++it )
Ben Voigt

@BenVoigt การใช้ตัววนซ้ำเป็นวิธีที่ดีที่สุดสำหรับการดำเนินการเหล่านั้นฉันแค่พูดถึงมันเพื่อแสดงให้เห็นถึงจุดของฉันเกี่ยวกับการใช้วิธีการที่ไม่บริสุทธิ์พร้อมผลข้างเคียง
Pieter B

0

ในความเห็นของฉันนี่เป็นภาษาเฉพาะสูง ตัวอย่างเช่นหากใช้ C ++ 11 ฉันจะสงสัยว่าถ้าการตรวจสอบเงื่อนไขเป็นconstexprฟังก์ชันคอมไพเลอร์จะมีแนวโน้มที่จะปรับการประมวลผลหลายครั้งให้เหมาะสมเพราะมันรู้ว่ามันจะให้ค่าเดียวกันทุกครั้ง

อย่างไรก็ตามถ้าการเรียกฟังก์ชันเป็นฟังก์ชันไลบรารีที่ไม่ใช่constexprตัวแปลภาษานั้นจะดำเนินการในทุก ๆ การวนซ้ำเนื่องจากมันไม่สามารถอนุมานได้ (ยกเว้นว่าเป็นแบบอินไลน์

ฉันรู้น้อยเกี่ยวกับ Java แต่เนื่องจากเป็น JIT ที่คอมไพล์ฉันคาดเดาว่าคอมไพเลอร์มีข้อมูลเพียงพอที่รันไทม์เพื่ออินไลน์ที่เป็นไปได้และปรับเงื่อนไขให้เหมาะสม แต่สิ่งนี้จะขึ้นอยู่กับการออกแบบคอมไพเลอร์ที่ดีและคอมไพเลอร์ที่ตัดสินใจว่าการวนซ้ำนี้เป็นการเพิ่มประสิทธิภาพเบื้องต้นที่เราคาดเดาได้เท่านั้น

โดยส่วนตัวแล้วฉันรู้สึกว่ามันดีกว่าการใส่เงื่อนไขไว้ในลูปถ้าคุณทำได้ แต่ถ้ามันซับซ้อนฉันจะเขียนมันลงในฟังก์ชั่นconstexprหรือinlineฟังก์ชั่นหรือภาษาของคุณเทียบเท่ากับคำใบ้ว่าฟังก์ชั่นนั้นบริสุทธิ์และเหมาะสมที่สุด สิ่งนี้ทำให้เจตนาชัดเจนและรักษาสไตล์วนซ้ำโดยไม่สร้างบรรทัดที่อ่านไม่ได้ขนาดใหญ่ นอกจากนี้ยังให้เงื่อนไขการตรวจสอบชื่อถ้ามันเป็นฟังก์ชั่นของตัวเองเพื่อให้ผู้อ่านสามารถเห็นเหตุผลในทันทีสิ่งที่ตรวจสอบมีไว้สำหรับโดยไม่ต้องอ่านมันถ้าซับซ้อน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.