เหตุใดจึงต้องใช้วิธีการ OO แทนคำสั่ง“ สวิตช์” ขนาดใหญ่


59

ฉันทำงานใน. Net, C # shop และฉันมีเพื่อนร่วมงานที่คอยยืนยันว่าเราควรใช้คำสั่ง Switch ขนาดใหญ่ในรหัสของเราด้วย "คดี" จำนวนมากแทนที่จะเป็นวิธีเชิงวัตถุมากกว่า การโต้เถียงของเขากลับไปอย่างต่อเนื่องตามข้อเท็จจริงที่ว่าคำสั่ง Switch รวบรวมเป็น "ตารางกระโดด cpu" และเป็นตัวเลือกที่เร็วที่สุด (แม้ว่าในสิ่งอื่น ๆ ทีมของเราบอกว่าเราไม่สนใจความเร็ว)

ฉันไม่ได้โต้แย้งเรื่องนี้ ... เพราะฉันไม่รู้ว่าเขากำลังพูดถึงเรื่องอะไร
เขาพูดถูกมั้ย
เขาเพิ่งพูดถึงลาของเขาหรือไม่?
แค่พยายามเรียนรู้ที่นี่


7
คุณสามารถตรวจสอบว่าเขาถูกต้องโดยใช้สิ่ง. NET สะท้อนเพื่อดูรหัสประกอบและมองหา "cpu ตารางกระโดด"
FrustratedWithFormsDesigner

5
"Switch statement รวบรวมเป็น" cpu jump table "ดังนั้นวิธีที่แย่ที่สุดในกรณีที่ส่งไปกับฟังก์ชั่น pure-virtual ทั้งหมดไม่มีฟังก์ชั่น virtual เพียงแค่เชื่อมโยงโดยตรงคุณทำการทิ้ง code เพื่อเปรียบเทียบหรือไม่
S.Lott

64
ควรเขียนรหัสสำหรับคนไม่ใช่สำหรับเครื่องจักรมิฉะนั้นเราจะทำทุกอย่างในการประกอบ
maple_shaft

8
ถ้าเขาเป็นคนที่มีค่ามากให้อ้าง Knuth กับเขาว่า: "เราควรลืมประสิทธิภาพเล็กน้อยพูดถึง 97% ของเวลา: การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากฐานของความชั่วทั้งหมด"
DaveE

12
การบำรุงรักษา คำถามอื่นใดที่มีคำตอบเดียวที่ฉันสามารถช่วยคุณได้?
Matt Ellen

คำตอบ:


48

เขาอาจเป็นแฮ็กเกอร์ C คนเก่าและใช่เขาพูดออกมาจากตูดของเขา .Net ไม่ใช่ C ++; คอมไพเลอร์. Net ช่วยให้การแฮ็กดีขึ้นและแฮ็คที่ฉลาดที่สุดเป็นแบบต่อต้านหากไม่ได้วันนี้ในรุ่น. Net ถัดไป ฟังก์ชั่นขนาดเล็กเป็นที่นิยมมากกว่าเพราะ. Net JIT-s ทุกฟังก์ชั่นหนึ่งครั้งก่อนที่มันจะถูกใช้ ดังนั้นหากบางกรณีไม่เคยถูกโจมตีระหว่างวงจรชีวิตของโปรแกรมดังนั้นจึงไม่มีค่าใช้จ่ายในการรวบรวม JIT อย่างไรก็ตามถ้าความเร็วไม่ใช่ปัญหาก็ไม่ควรมีการปรับให้เหมาะสม เขียนสำหรับโปรแกรมเมอร์ก่อนสำหรับคอมไพเลอร์ที่สอง เพื่อนร่วมงานของคุณจะไม่ถูกโน้มน้าวใจได้ง่ายดังนั้นฉันจะพิสูจน์ด้วยประจักษ์ว่ารหัสจัดระเบียบที่ดีกว่านั้นเร็วกว่าจริง ฉันจะเลือกหนึ่งในตัวอย่างที่เลวร้ายที่สุดของเขาเขียนใหม่ในวิธีที่ดีกว่าจากนั้นตรวจสอบให้แน่ใจว่าโค้ดของคุณเร็วขึ้น เชอร์รี่เลือกถ้าคุณต้อง จากนั้นรันสองสามล้านครั้งโปรไฟล์และแสดงให้เขาเห็น

แก้ไข

Bill Wagner เขียนว่า:

รายการที่ 11: ทำความเข้าใจกับฟังก์ชั่นที่น่าสนใจของฟังก์ชั่นขนาดเล็ก (มีผลบังคับใช้ C # Second Edition) โปรดจำไว้ว่าการแปลรหัส C # ของคุณเป็นรหัสที่ปฏิบัติการด้วยเครื่องเป็นกระบวนการสองขั้นตอน คอมไพเลอร์ C # สร้าง IL ที่ได้รับการส่งมอบในการชุมนุม คอมไพเลอร์ JIT สร้างรหัสเครื่องสำหรับแต่ละวิธี (หรือกลุ่มของวิธีเมื่อเกี่ยวข้องกับการทำอินไลน์) ตามต้องการ ฟังก์ชั่นขนาดเล็กทำให้การคอมไพเลอร์ JIT ง่ายขึ้นเพื่อตัดจำหน่ายค่าใช้จ่ายนั้น ฟังก์ชั่นขนาดเล็กยังมีแนวโน้มที่จะเป็นผู้สมัครสำหรับอินไลน์ ไม่ใช่แค่เรื่องเล็ก: การควบคุมการไหลที่เรียบง่ายนั้นสำคัญมาก สาขาการควบคุมน้อยลงภายในฟังก์ชั่นทำให้ง่ายขึ้นสำหรับคอมไพเลอร์ JIT เพื่อลงทะเบียนตัวแปร ไม่ใช่แค่การเขียนโค้ดที่ชัดเจน มันเป็นวิธีที่คุณสร้างโค้ดที่มีประสิทธิภาพยิ่งขึ้นขณะใช้งานจริง

EDIT2:

ดังนั้น ... เห็นได้ชัดว่าคำสั่ง switch นั้นเร็วกว่าและดีกว่ากลุ่มคำสั่ง if / else เพราะการเปรียบเทียบอย่างหนึ่งคือลอการิทึมและอีกอันเป็นเส้นตรง http://sequence-points.blogspot.com/2007/10/why-is-switch-statement-faster-than-if.html

วิธีที่ฉันโปรดปรานในการเปลี่ยนคำสั่งสวิตช์ขนาดใหญ่คือด้วยพจนานุกรม (หรือบางครั้งอาจเป็นอาร์เรย์ถ้าฉันเปลี่ยน enums หรือ int ขนาดเล็ก) ซึ่งเป็นค่าการแม็พกับฟังก์ชันที่เรียกใช้เพื่อตอบสนองต่อพวกเขา การทำเช่นนั้นบังคับให้คนหนึ่งกำจัดสปาเก็ตตี้ที่น่ารังเกียจออกไปมากมาย แต่นั่นเป็นสิ่งที่ดี คำสั่ง switch ขนาดใหญ่มักเป็นฝันร้ายของการบำรุงรักษา ดังนั้น ... ด้วยอาร์เรย์และพจนานุกรมการค้นหาจะใช้เวลาคงที่และจะมีหน่วยความจำเหลือน้อยมาก

ฉันยังไม่มั่นใจว่าคำสั่ง switch นั้นดีกว่า


47
ไม่ต้องกังวลกับการพิสูจน์ได้เร็วขึ้น นี่คือการเพิ่มประสิทธิภาพก่อนวัยอันควร มิลลิวินาทีที่คุณอาจบันทึกคือไม่มีอะไรเทียบกับดัชนีที่คุณลืมเพิ่มลงในฐานข้อมูลที่มีค่าใช้จ่าย 200ms คุณกำลังต่อสู้กับการต่อสู้ที่ผิด
Rein Henrichs

27
@ งานจะทำอย่างไรถ้าเขาจริงใช่ไหม ประเด็นก็คือว่าเขาไม่ได้ผิดประเด็นก็คือว่าเขามีสิทธิและมันไม่สำคัญ
Rein Henrichs

2
แม้ว่าเขาจะถูกต้องประมาณ 100% ของคดีเขายังคงเสียเวลาของเรา
Jeremy

6
ฉันต้องการขุดตาออกพยายามอ่านหน้าเว็บที่คุณเชื่อมโยง
AttackHobo

3
ความเกลียดชัง C ++ คืออะไร คอมไพเลอร์ C ++ ก็เริ่มดีขึ้นเช่นกันและสวิตช์ขนาดใหญ่ก็แย่ใน C ++ เหมือนใน C # และด้วยเหตุผลเดียวกันทั้งหมด หากคุณถูกล้อมรอบด้วยอดีตโปรแกรมเมอร์ C ++ ที่ให้ความเศร้าโศกไม่ใช่เพราะพวกเขาเป็นโปรแกรมเมอร์ C ++ นั่นเป็นเพราะพวกเขาเป็นโปรแกรมเมอร์ที่ไม่ดี
Sebastian Redl

39

หากเพื่อนร่วมงานของคุณไม่สามารถพิสูจน์ได้ว่าการเปลี่ยนแปลงนี้ให้ผลประโยชน์ที่วัดได้จริงในระดับของการสมัครทั้งหมดมันก็ด้อยกว่าวิธีการของคุณ (เช่น polymorphism) ซึ่งจริง ๆ แล้วให้ประโยชน์ดังกล่าว: การบำรุงรักษา

ควรทำการ Microoptimisation หลังจากคอขวดถูกตรึง เพิ่มประสิทธิภาพก่อนกำหนดเป็นรากของความชั่วร้ายทั้งหมด

ความเร็วเป็นเชิงปริมาณ มีข้อมูลที่เป็นประโยชน์เล็กน้อยใน "วิธีการ A เร็วกว่าวิธี B" คำถามคือ " เร็วแค่ไหน? "


2
จริงที่สุด. อย่าอ้างว่ามีอะไรที่เร็วกว่าเสมอวัดได้เสมอ และวัดเฉพาะเมื่อส่วนหนึ่งของแอปพลิเคชันเป็นคอขวดของประสิทธิภาพ
Kilian Foth

6
-1 สำหรับ "การปรับให้เหมาะสมก่อนกำหนดเป็นรากของความชั่วร้ายทั้งหมด" โปรดแสดงใบเสนอราคาทั้งหมดไม่ใช่เพียงส่วนหนึ่งที่แสดงความคิดเห็นของ Knuth
ทางเลือก

2
@ มาธิปิก: ฉันตั้งใจไม่ได้แสดงสิ่งนี้เป็นคำพูด ประโยคนี้ตามที่เป็นอยู่คือความเห็นส่วนตัวของฉันแม้ว่าแน่นอนว่าไม่ใช่การสร้างของฉัน แม้ว่าอาจสังเกตได้ว่าคนจาก c2 ดูเหมือนจะพิจารณาเพียงส่วนหนึ่งของภูมิปัญญาหลัก
back2dos

8
@ ทางเลือกอ้างถึง Knuth แบบเต็ม "ไม่ต้องสงสัยเลยว่า grail ของประสิทธิภาพจะนำไปสู่การละเมิดโปรแกรมเมอร์เสียเวลาจำนวนมากที่คิดหรือกังวลเกี่ยวกับความเร็วของส่วนที่ไม่สำคัญของโปรแกรมและความพยายามเหล่านี้มีประสิทธิภาพจริง ๆ ผลกระทบด้านลบอย่างมากเมื่อพิจารณาการดีบั๊กและการบำรุงรักษาเราควรลืมประสิทธิภาพเล็กน้อยพูดถึง 97% ของเวลา: การปรับให้เหมาะสมก่อนกำหนดเป็นรากฐานของความชั่วร้ายทั้งหมด " อธิบายผู้ร่วมงานของ OP ได้อย่างสมบูรณ์แบบ back2dos IMHO สรุปคำพูดดีกับ "การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากของความชั่วร้ายทั้งหมด"
MarkJ

2
@ MarkJ 97% ของเวลา
ทางเลือก

27

ใครจะสนใจว่าเร็วกว่าใคร

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

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

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

[ในด้านที่น่าสนใจ: JITter ทำงานได้ดีขึ้นในวิธีการที่เล็กกว่าดังนั้นคำสั่งของสวิตช์ขนาดใหญ่ (และวิธีการที่มีขนาดใหญ่โดยเนื้อแท้) จะเป็นอันตรายต่อความเร็วของคุณในชุดประกอบขนาดใหญ่ IIRC]


1
+ เป็นตัวอย่างที่ดีของการเพิ่มประสิทธิภาพก่อนกำหนด
ShaneC

อย่างนี้แน่นอน
DeadMG

+1 สำหรับ 'คำสั่งเปลี่ยนยักษ์ไม่สามารถดูแลรักษาได้แม้แต่น้อย'
Korey Hinton

2
คำแถลงสวิทช์ยักษ์นั้นง่ายกว่ามากสำหรับคนใหม่ที่จะเข้าใจ: พฤติกรรมที่เป็นไปได้ทั้งหมดจะถูกรวบรวมไว้ในรายการเรียบร้อยดี การโทรทางอ้อมนั้นยากมากที่จะติดตามในกรณีที่เลวร้ายที่สุด (ตัวชี้ฟังก์ชั่น) คุณต้องค้นหาฐานรหัสทั้งหมดเพื่อฟังก์ชั่นของลายเซ็นที่ถูกต้องและการโทรเสมือนจะดีขึ้นเล็กน้อย (ค้นหาฟังก์ชันของชื่อและลายเซ็นที่ถูกต้อง เกี่ยวข้องโดยการสืบทอด) แต่การบำรุงรักษาไม่ได้เกี่ยวกับการอ่านอย่างเดียว
Ben Voigt

14

ก้าวออกจากคำสั่งเปลี่ยน ...

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


11
ที่มาพร้อมกับข้อแม้ มีการทำงาน (ฟังก์ชั่น / วิธีการ) และประเภท เมื่อคุณเพิ่มการดำเนินการใหม่คุณจะต้องเปลี่ยนรหัสในที่เดียวสำหรับคำสั่งเปลี่ยน (เพิ่มฟังก์ชั่นใหม่ที่มีคำสั่งเปลี่ยน) แต่คุณจะต้องเพิ่มวิธีการที่จะเรียนทั้งหมดในกรณี OO (ละเมิดเปิด หลักการปิด) หากคุณเพิ่มประเภทใหม่คุณจะต้องแตะคำสั่ง switch ทุกอัน แต่ในกรณี OO คุณจะต้องเพิ่มคลาสอีกหนึ่งชั้น ดังนั้นในการตัดสินใจอย่างมีข้อมูลคุณต้องทราบว่าคุณจะเพิ่มการดำเนินการเพิ่มเติมให้กับประเภทที่มีอยู่หรือเพิ่มประเภทอื่น ๆ
Scott Whitlock

3
หากคุณต้องการเพิ่มการดำเนินการเพิ่มเติมให้กับประเภทที่มีอยู่ในกระบวนทัศน์ OO โดยไม่ละเมิด OCP ฉันเชื่อว่านั่นเป็นรูปแบบของผู้เข้าชม
Scott Whitlock

3
@ มาร์ติน - เรียกชื่อถ้าคุณจะ แต่นี่คือการแลกเปลี่ยนที่รู้จักกันดี ฉันแนะนำให้คุณทำความสะอาด Code โดย RC Martin เขาทบทวนบทความของเขาอีกครั้งใน OCP โดยอธิบายสิ่งที่ฉันอธิบายไว้ข้างต้น คุณไม่สามารถออกแบบพร้อมกันสำหรับความต้องการในอนาคตทั้งหมดได้พร้อมกัน คุณต้องเลือกระหว่างว่ามีแนวโน้มที่จะเพิ่มการดำเนินการมากขึ้นหรือประเภทมากขึ้น OO สนับสนุนการเพิ่มประเภท คุณสามารถใช้ OO เพื่อเพิ่มการดำเนินการเพิ่มเติมหากคุณจำลองการทำงานเป็นคลาส แต่ดูเหมือนว่าจะเข้าสู่รูปแบบผู้เยี่ยมชมซึ่งมีปัญหาของตัวเอง (โดยเฉพาะค่าใช้จ่าย)
Scott Whitlock

8
@ มาร์ติน: คุณเคยเขียน parser หรือไม่? เป็นเรื่องปกติที่จะมีตัวเรือนขนาดใหญ่ที่สลับโทเค็นถัดไปในบัฟเฟอร์ lookahead คุณสามารถแทนที่สวิตช์เหล่านั้นด้วยการเรียกใช้ฟังก์ชันเสมือนไปยังโทเค็นถัดไป แต่นั่นจะเป็นฝันร้ายของการบำรุงรักษา มันเป็นของหายาก แต่บางครั้งสวิตช์ตัวพิมพ์ใหญ่ก็เป็นตัวเลือกที่ดีกว่าเพราะมันจะเก็บโค้ดที่ควรอ่าน / แก้ไขร่วมกันในระยะใกล้
nikie

1
@ มาร์ติน: คุณใช้คำเช่น "ไม่เคย", "เคย" และ "Poppycock" ดังนั้นฉันสมมติว่าคุณกำลังพูดถึงทุกกรณีโดยไม่มีข้อยกเว้นไม่ใช่แค่เรื่องที่พบบ่อยที่สุด (และ BTW: ผู้คนยังคงเขียนโปรแกรมแยกวิเคราะห์ด้วยมือตัวอย่างเช่นตัวแยกวิเคราะห์ CPython ยังคงเขียนด้วยมือ IIRC)
nikie

8

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

ตัวชี้วัดที่คุณต้องใส่ใจคือ:

  • ความเร็วในการเปลี่ยนแปลง
  • ความเร็วในการค้นหาปัญหาเมื่อมันเกิดขึ้น

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

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

มีอะไรผิดปกติกับคำสั่งสวิตช์ขนาดใหญ่สำหรับภาษา OO

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

8

ฉันไม่ซื้ออาร์กิวเมนต์ประสิทธิภาพ มันคือทั้งหมดที่เกี่ยวกับการบำรุงรักษาโค้ด

แต่: บางครั้งคำสั่งสวิตช์ขนาดใหญ่นั้นง่ายต่อการบำรุงรักษา (โค้ดน้อยกว่า) มากกว่ากลุ่มของคลาสย่อยที่แทนที่ฟังก์ชันเสมือนของคลาสพื้นฐานที่เป็นนามธรรม ตัวอย่างเช่นถ้าคุณต้องใช้ซีพียูอีมูเลเตอร์คุณจะไม่ใช้ฟังก์ชั่นของแต่ละคำสั่งในคลาสที่แยกต่างหาก - คุณเพียงแค่ใส่มันลงใน swtich ยักษ์บน opcode ซึ่งอาจเรียกฟังก์ชั่นตัวช่วยสำหรับคำแนะนำที่ซับซ้อนมากขึ้น

Rule of thumb: หากสวิตช์ทำงานบน TYPE คุณควรใช้การสืบทอดและฟังก์ชันเสมือน หากสวิตช์ทำงานบน VALUE แบบคงที่ (เช่นคำแนะนำ opcode ดังกล่าวข้างต้น) ก็โอเคที่จะปล่อยไว้เหมือนเดิม


5

คุณไม่สามารถโน้มน้าวฉันได้ว่า:

void action1()
{}

void action2()
{}

void action3()
{}

void action4()
{}

void doAction(int action)
{
    switch(action)
    {
        case 1: action1();break;
        case 2: action2();break;
        case 3: action3();break;
        case 4: action4();break;
    }
}

เร็วกว่า:

struct IAction
{
    virtual ~IAction() {}
    virtual void action() = 0;
}

struct Action1: public IAction
{
    virtual void action()    { }
}

struct Action2: public IAction
{
    virtual void action()    { }
}

struct Action3: public IAction
{
    virtual void action()    { }
}

struct Action4: public IAction
{
    virtual void action()    { }
}

void doAction(IAction& actionObject)
{
    actionObject.action();
}

นอกจากนี้เวอร์ชั่น OO สามารถบำรุงรักษาได้มากขึ้น


8
สำหรับบางสิ่งและสำหรับการกระทำที่น้อยกว่ารุ่น OO นั้นน่าสนใจกว่ามาก ต้องมีโรงงานบางประเภทในการแปลงค่าบางส่วนเป็นการสร้าง IAction ในหลายกรณีมันสามารถอ่านได้มากขึ้นเพียงแค่เปลี่ยนค่านั้นแทน
Zan Lynx

@Zan Lynx: ข้อโต้แย้งของคุณกว้างเกินไป การสร้างวัตถุ IAction นั้นยากพอ ๆ กับการเรียกจำนวนเต็มของการดำเนินการที่ไม่ยากอีกต่อไป ดังนั้นเราสามารถมีการสนทนาจริงโดยไม่ต้องมีวิธีทั่วไป พิจารณาเครื่องคิดเลข ความแตกต่างของความซับซ้อนที่นี่คืออะไร? คำตอบคือศูนย์ การกระทำทั้งหมดที่สร้างไว้ล่วงหน้า คุณได้รับข้อมูลจากผู้ใช้และการกระทำของมันอยู่แล้ว
Martin York

3
@Martin: คุณกำลังสมมติว่าแอพเครื่องคิดเลข GUI ลองใช้แอปเครื่องคิดเลขแบบแป้นพิมพ์เขียนสำหรับ C ++ บนระบบฝังตัวแทน ตอนนี้คุณมีจำนวนเต็มรหัสสแกนจากการลงทะเบียนฮาร์ดแวร์ ตอนนี้อะไรที่ซับซ้อนน้อยกว่ากัน?
Zan Lynx

2
@ มาร์ติน: คุณไม่เห็นว่าจำนวนเต็ม -> ตารางการค้นหา -> การสร้างวัตถุใหม่ -> โทรฟังก์ชั่นเสมือนมีความซับซ้อนกว่าจำนวนเต็ม -> สลับ -> ฟังก์ชั่น? คุณไม่เห็นว่าอย่างไร
Zan Lynx

2
@ มาร์ติน: บางทีฉันจะ ในระหว่างนี้ให้อธิบายวิธีที่คุณรับออบเจ็กต์ IAction เพื่อเรียกการกระทำ () จากจำนวนเต็มโดยไม่มีตารางการค้นหา
Zan Lynx

4

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

HOWEVER : แน่นอนว่าแอปพลิเคชันของคุณไม่จำเป็นต้องกังวลเกี่ยวกับการเพิ่มประสิทธิภาพแบบไมโครหรือคุณจะไม่ใช้. net ตั้งแต่แรก สำหรับแอปพลิเคชันแบบฝังที่มีข้อ จำกัด มากหรืองานที่ต้องใช้ CPU มากคุณควรปล่อยให้คอมไพเลอร์จัดการกับการปรับให้เหมาะสมเสมอ มีสมาธิในการเขียนรหัสที่สะอาดและบำรุงรักษาได้ นี่เป็นสิ่งที่มีค่ายิ่งใหญ่กว่าเวลาเพียงไม่กี่สิบนาโนวินาทีในเวลาดำเนินการ


3

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


3

คำสั่ง switch ในรหัส OOP เป็นการบ่งชี้อย่างชัดเจนถึงคลาสที่หายไป

ลองทั้งสองวิธีและใช้การทดสอบความเร็วอย่างง่าย โอกาสมีความแตกต่างไม่สำคัญ หากเป็นและรหัสมีความสำคัญต่อเวลาให้เก็บคำสั่ง switch ไว้


3

ปกติฉันเกลียดคำว่า "การปรับให้เหมาะสมก่อนวัยอันควร" แต่นี่เป็นผลของมัน เป็นที่น่าสังเกตว่า Knuth ใช้คำพูดที่โด่งดังนี้ในบริบทของการผลักดันการใช้gotoข้อความเพื่อเร่งรหัสในพื้นที่สำคัญ นั่นคือกุญแจสำคัญ: เส้นทางที่สำคัญ

เขาแนะนำให้ใช้gotoเพื่อเร่งโค้ด แต่เตือนผู้เขียนโปรแกรมที่ต้องการทำสิ่งต่าง ๆ เหล่านี้ตามลางสังหรณ์และความเชื่อโชคลางสำหรับโค้ดที่ไม่สำคัญแม้แต่น้อย

เพื่อสนับสนุนswitchงบมากที่สุดเท่าที่เป็นไปได้อย่างสม่ำเสมอทั่ว codebase (ไม่ว่าจะมีการจัดการภาระหนักหรือไม่ก็ตาม) เป็นตัวอย่างคลาสสิกของสิ่งที่ Knuth เรียกโปรแกรมเมอร์ "penny-wise และ pound-foolish" ที่ใช้เวลาตลอดทั้งวัน รหัสที่กลายเป็นฝันร้ายในการดีบั๊กเนื่องจากพยายามบันทึกเพนนีมากกว่าปอนด์ รหัสนี้ไม่ค่อยบำรุงรักษาให้อยู่คนเดียวแม้จะมีประสิทธิภาพในสถานที่แรก

เขาพูดถูกมั้ย

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

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

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

เขาเพิ่งพูดถึงลาของเขาหรือไม่?

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

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

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


2

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


1
พร้อมกับการทดสอบเวลาดัมพ์โค้ดสามารถช่วยแสดงให้เห็นว่า C ++ (และ C #) วิธีการจัดส่งทำงานอย่างไร
S.Lott

2

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

เช่น. ถ้าคุณจะสลับไปมาระหว่างDog, CatและElephantและบางครั้งDogและCatมีกรณีเดียวกันคุณสามารถทำให้พวกเขาทั้งสองได้รับมรดกจากระดับนามธรรมDomesticAnimalและใส่ฟังก์ชั่นผู้ที่อยู่ในระดับนามธรรม

นอกจากนี้ฉันยังประหลาดใจที่หลาย ๆ คนใช้ parser เป็นตัวอย่างที่คุณจะไม่ใช้ polymorphism สำหรับ parser ที่มีลักษณะคล้ายต้นไม้นี่เป็นแนวทางที่ผิด แต่ถ้าคุณมีบางอย่างเช่นชุดประกอบที่แต่ละบรรทัดค่อนข้างอิสระและเริ่มด้วย opcode ที่ระบุว่าส่วนที่เหลือของบรรทัดควรตีความอย่างไรฉันจะใช้ความหลากหลาย และโรงงาน แต่ละชั้นสามารถใช้ฟังก์ชั่นเหมือนหรือExtractConstants ExtractSymbolsฉันใช้วิธีนี้สำหรับล่ามพื้นฐานของของเล่น


สวิทช์ก็สามารถสืบทอดพฤติกรรมได้เช่นกันโดยใช้ตัวพิมพ์เล็ก "... ขยาย BaseOperationVisitor" กลายเป็น "ค่าเริ่มต้น: BaseOperation (โหนด)"
ซามูเอล Danielson

0

"เราควรลืมเกี่ยวกับประสิทธิภาพเล็กน้อยพูดถึง 97% ของเวลา: การปรับให้เหมาะสมก่อนกำหนดเป็นรากของความชั่วทั้งหมด"

Donald Knuth


0

แม้ว่าสิ่งนี้จะไม่ดีต่อการบำรุงรักษา แต่ฉันก็ไม่เชื่อว่ามันจะดีกว่าสำหรับประสิทธิภาพ การเรียกฟังก์ชั่นเสมือนเป็นเพียงการเสริมทางเดียว (เช่นเดียวกับกรณีที่ดีที่สุดสำหรับคำสั่งสวิตช์) ดังนั้นแม้ใน C ++ ประสิทธิภาพก็ควรจะเท่ากัน ใน C # ที่การเรียกใช้ฟังก์ชันทั้งหมดเป็นเสมือนคำสั่ง switch น่าจะแย่กว่าเนื่องจากคุณมีการเรียกใช้ฟังก์ชันเสมือนที่เหมือนกันในทั้งสองเวอร์ชัน


1
ไม่มี "ไม่ใช่" ใช่หรือไม่ ใน C # การเรียกใช้ฟังก์ชันไม่ได้ทั้งหมดเป็นเสมือน C # ไม่ใช่ Java
Ben Voigt

0

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

คอมไพเลอร์ C # แปลงคำสั่งสลับกับกรณีเพียงไม่กี่กรณีเป็นชุดของ if / else ดังนั้นจึงไม่เร็วกว่าการใช้ if / else คอมไพเลอร์แปลงงบสวิทช์ที่ใหญ่ขึ้นเป็นพจนานุกรม (ตารางกระโดดที่เพื่อนร่วมงานของคุณอ้างถึง) โปรดดูคำตอบนี้จะเป็นคำถามที่กองมากเกินในหัวข้อสำหรับรายละเอียดเพิ่มเติม

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


0

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

ยิ่งไปกว่านั้นการกระจายแบบไดนามิกมักจะทำหน้าที่เป็น "การเพิ่มประสิทธิภาพการกีดขวาง" ซึ่งหมายความว่าคอมไพเลอร์มักจะไม่สามารถอินไลน์โค้ดและจัดสรรการลงทะเบียนอย่างเหมาะสมเพื่อลดการสแต็กสแต็กและสิ่งที่แปลกใหม่ ฟังก์ชั่นเสมือนจริงจะถูกเรียกผ่านตัวชี้ฐานเพื่ออินไลน์และทำเวทมนต์ทั้งหมดของการเพิ่มประสิทธิภาพ ผมไม่แน่ใจว่าคุณยังต้องการเพิ่มประสิทธิภาพที่จะเป็นสมาร์ทและพยายามที่จะเพิ่มประสิทธิภาพออกไปเรียกฟังก์ชันทางอ้อมตั้งแต่ที่อาจนำไปสู่หลายสาขามีรหัสที่จะสร้างแยกลงสแต็คโทรที่กำหนด (ฟังก์ชั่นที่เรียกร้องfoo->f()จะมี เพื่อสร้างรหัสเครื่องที่แตกต่างอย่างสิ้นเชิงจากรหัสที่เรียกbar->f() ผ่านตัวชี้ฐานและฟังก์ชั่นที่เรียกใช้ฟังก์ชั่นนั้นจะต้องสร้างรหัสสองรุ่นขึ้นไปและอื่น ๆ - จำนวนรหัสเครื่องที่สร้างขึ้นจะเกิดการระเบิด - อาจไม่เลวร้ายนักกับ JIT ติดตามซึ่ง สร้างรหัสได้ทันทีขณะที่กำลังติดตามผ่านเส้นทางการเรียกใช้งานที่ร้อนแรง)

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

ที่กล่าวว่าฉันกระโดดเข้ามาที่นี่พร้อมคำตอบที่ผิดปกติ ฉันต้องการที่จะทำให้เป็นกรณีสำหรับการบำรุงรักษาของที่switchงบกว่าวิธีการแก้ปัญหาเมื่อ polymorphic switchและเฉพาะเมื่อคุณทราบว่ามีเพียงจะเป็นสถานที่หนึ่งที่ต้องดำเนินการ

ตัวอย่างที่สำคัญคือตัวจัดการเหตุการณ์กลาง ในกรณีนี้โดยทั่วไปคุณไม่มีสถานที่จัดการเหตุการณ์มากมายเพียงแห่งเดียว (เพราะเหตุใดจึงเป็น "ศูนย์กลาง") สำหรับกรณีเหล่านี้คุณไม่ได้รับประโยชน์จากความสามารถในการขยายที่โซลูชัน polymorphic จัดเตรียม วิธีการแก้ปัญหา polymorphic เป็นประโยชน์เมื่อมีหลายสถานที่ที่จะทำswitchคำสั่งแบบอะนาล็อก หากคุณรู้ว่ามีเพียงหนึ่งswitchคำสั่งที่มี 15 รายอาจง่ายกว่าการออกแบบคลาสพื้นฐานที่สืบทอดโดย 15 ชนิดย่อยที่มีฟังก์ชั่นแทนที่และโรงงานเพื่อยกตัวอย่างพวกเขาเท่านั้นที่จะใช้ในฟังก์ชั่นเดียว ในระบบทั้งหมด ในกรณีเหล่านั้นการเพิ่มประเภทย่อยใหม่นั้นน่าเบื่อกว่าการเพิ่มcaseคำสั่งลงในฟังก์ชั่นเดียว ถ้ามีอะไรฉันก็เถียงกับการบำรุงรักษาไม่ใช่ประสิทธิภาพswitch คำแถลงในกรณีพิเศษนี้ซึ่งคุณไม่ได้รับประโยชน์จากความสามารถในการขยายเพิ่มเติมใด ๆ

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