TL; DRการวนซ้ำที่ช้ากว่านั้นเกิดจากการเข้าถึง Array 'นอกขอบเขต' ซึ่งจะบังคับให้เครื่องยนต์คอมไพล์ฟังก์ชันใหม่โดยใช้การปรับให้เหมาะสมน้อยลงหรือไม่มีเลยหรือจะไม่รวบรวมฟังก์ชั่นใด ๆ หากคอมไพเลอร์ (JIT-) ตรวจพบ / สงสัยว่าเงื่อนไขนี้ก่อนการคอมไพล์ครั้งแรก 'เวอร์ชั่น') ให้อ่านสาเหตุด้านล่าง
คนที่เพิ่ง
มีการพูดแบบนี้ (ไม่มีใครประหลาดใจอย่างเต็มที่ได้แล้ว):
มีการใช้เป็นเวลาที่ข้อมูลโค้ดของ OP จะเป็นตัวอย่างพฤตินัยในการเริ่มต้นหนังสือการเขียนโปรแกรมตั้งใจที่จะเค้าร่าง / เน้นว่า 'อาร์เรย์ในจาวาสคริปต์ที่จะเริ่มต้นการจัดทำดัชนี ที่ 0, 1 ไม่ได้และเป็นเช่นถูกนำมาใช้เป็นตัวอย่างของทั่วไป 'เริ่มต้นผิดพลาด' (ให้คุณไม่รักวิธีที่ผมหลีกเลี่ยงวลี 'การวางข้อผิดพลาด'
;)
): ออกจากขอบเขตการเข้าถึงอาร์เรย์
ตัวอย่างที่ 1:
a Dense Array
(อยู่ติดกัน (หมายถึงไม่มีช่องว่างระหว่างดัชนี) และจริง ๆ องค์ประกอบในแต่ละดัชนี) ของ 5 องค์ประกอบโดยใช้การทำดัชนี 0 ตาม (เสมอใน ES262)
var arr_five_char=['a', 'b', 'c', 'd', 'e']; // arr_five_char.length === 5
// indexes are: 0 , 1 , 2 , 3 , 4 // there is NO index number 5
ดังนั้นเราจึงไม่ได้พูดถึงความแตกต่างของประสิทธิภาพระหว่าง<
vs <=
(หรือ 'การทำซ้ำหนึ่งครั้งพิเศษ') แต่เรากำลังพูดถึง:
'เหตุใดข้อมูลโค้ดที่ถูกต้อง (b) จึงทำงานได้เร็วกว่าข้อมูลโค้ดที่ผิดพลาด (a)'
คำตอบคือ 2 เท่า (แม้ว่าจากมุมมองของผู้ใช้ภาษา ES262 ทั้งสองเป็นรูปแบบของการเพิ่มประสิทธิภาพ):
- Data-Representation: วิธีการแสดง / เก็บ Array ภายในหน่วยความจำ (object, hashmap, array ตัวเลข 'real' เป็นต้น)
- ฟังก์ชั่นรหัสเครื่อง: วิธีการรวบรวมรหัสที่เข้าถึง / จัดการ (อ่าน / แก้ไข) 'อาร์เรย์' เหล่านี้
รายการที่ 1 พอ (และถูกต้อง IMHO) อธิบายได้ด้วยคำตอบที่ได้รับการยอมรับแต่เพียงว่าใช้เวลา 2 คำ ( 'รหัส') ในรายการที่ 2: การรวบรวม
แม่นยำมากขึ้น: JIT รวบรวมและแม้กระทั่งที่สำคัญกว่า JIT- RE -Compilation!
ข้อกำหนดทางภาษานั้นเป็นเพียงคำอธิบายของชุดของอัลกอริทึม ('ขั้นตอนในการดำเนินการเพื่อให้ได้ผลลัพธ์ที่กำหนดไว้') ซึ่งตามที่ปรากฎเป็นวิธีที่สวยงามมากในการอธิบายภาษา และปล่อยให้วิธีการจริงที่เครื่องยนต์ใช้เพื่อให้ได้ผลลัพธ์ตามที่กำหนดเปิดให้ผู้ใช้งานมีโอกาสมากมายที่จะคิดหาวิธีที่มีประสิทธิภาพมากขึ้นในการสร้างผลลัพธ์ที่กำหนดไว้ เอ็นจิ้นที่สอดคล้องกับสเป็คควรให้ผลสเป็คที่สอดคล้องกันสำหรับอินพุตที่กำหนดไว้
ตอนนี้ด้วยรหัสจาวาสคริปต์ / ห้องสมุด / การใช้งานที่เพิ่มขึ้นและการจดจำจำนวนทรัพยากร (เวลา / หน่วยความจำ / ฯลฯ ) คอมไพเลอร์ 'ของจริง' ใช้มันชัดเจนว่าเราไม่สามารถทำให้ผู้ใช้เยี่ยมชมหน้าเว็บรอนาน (และต้องการพวกเขา เพื่อให้มีทรัพยากรมากมายพร้อมใช้งาน)
ลองนึกภาพฟังก์ชั่นง่าย ๆ ดังต่อไปนี้:
function sum(arr){
var r=0, i=0;
for(;i<arr.length;) r+=arr[i++];
return r;
}
ชัดเจนสมบูรณ์แบบใช่มั้ย ไม่ต้องการคำชี้แจงเพิ่มเติมใด ๆ ใช่มั้ย ผลตอบแทนประเภทคือNumber
ใช่มั้ย
ดี .. ไม่ไม่ & ไม่ ... มันขึ้นอยู่กับอาร์กิวเมนต์ที่คุณส่งไปยังพารามิเตอร์ฟังก์ชันที่มีชื่อarr
...
sum('abcde'); // String('0abcde')
sum([1,2,3]); // Number(6)
sum([1,,3]); // Number(NaN)
sum(['1',,3]); // String('01undefined3')
sum([1,,'3']); // String('NaN3')
sum([1,2,{valueOf:function(){return this.val}, val:6}]); // Number(9)
var val=5; sum([1,2,{valueOf:function(){return val}}]); // Number(8)
เห็นปัญหาไหม จากนั้นให้พิจารณาว่านี่เป็นเพียงการคัดลอกการเรียงสับเปลี่ยนขนาดใหญ่ที่เป็นไปได้ ... เราไม่รู้ด้วยซ้ำว่าฟังก์ชันประเภท TYPE ส่งกลับจนกว่าเราจะทำ ...
ตอนนี้ลองนึกภาพฟังก์ชั่นรหัสเดียวกันนี้จริง ๆ แล้วถูกใช้กับประเภทที่แตกต่างกันหรือแม้กระทั่งการเปลี่ยนแปลงของอินพุตทั้งที่อธิบายไว้อย่างสมบูรณ์ (ในซอร์สโค้ด) และแบบไดนามิกในโปรแกรม 'อาร์เรย์' ที่สร้างขึ้น ..
ดังนั้นถ้าคุณมีการรวบรวมฟังก์ชั่นsum
เพียงครั้งเดียวแล้ววิธีเดียวที่มักจะส่งกลับผลสเปคที่กำหนดไว้สำหรับการใด ๆ และทุกประเภทของการป้อนข้อมูลแล้วอย่างเห็นได้ชัดโดยเฉพาะการดำเนินการทั้งหมดข้อมูลจำเพาะที่กำหนดหลักและขั้นตอนย่อยสามารถรับประกันได้ผลสอดคล้องข้อมูลจำเพาะ (เช่นเบราว์เซอร์ pre-y2k ที่ไม่มีชื่อ) ไม่มีการปรับให้เหมาะสม (เพราะไม่มีข้อสันนิษฐาน) และภาษาสคริปต์ที่แปลช้าแปลช้า
รวบรวม JIT (JIT ในเวลาเพียง) เป็นทางออกที่นิยมในปัจจุบัน
ดังนั้นคุณจึงเริ่มรวบรวมฟังก์ชั่นโดยใช้สมมติฐานเกี่ยวกับสิ่งที่มันทำส่งคืนและยอมรับ
คุณคิดการตรวจสอบที่ง่ายที่สุดเท่าที่จะทำได้เพื่อตรวจสอบว่าฟังก์ชั่นอาจเริ่มส่งคืนผลลัพธ์ที่ไม่สอดคล้องตามข้อกำหนด (เช่นเนื่องจากได้รับอินพุตที่ไม่คาดคิด) จากนั้นให้ทิ้งผลลัพธ์ที่รวบรวมไว้ก่อนหน้านี้และคอมไพล์กับสิ่งที่ซับซ้อนกว่าเดิมตัดสินใจว่าจะทำอย่างไรกับผลลัพธ์บางส่วนที่คุณมีอยู่แล้ว (เป็นที่ถูกต้องน่าเชื่อถือหรือคำนวณอีกครั้งเพื่อให้แน่ใจ) ผูกไว้ในโปรแกรม ลองอีกครั้ง. ในที่สุดก็กลับไปที่การตีความสคริปต์แบบขั้นตอนตามที่ระบุ
ทั้งหมดนี้ต้องใช้เวลา!
เบราว์เซอร์ทั้งหมดทำงานบนเอ็นจิ้นของพวกเขาสำหรับแต่ละเวอร์ชั่นย่อยคุณจะเห็นการปรับปรุงและถอยหลัง สตริงมีบางจุดในประวัติศาสตร์สตริงที่ไม่เปลี่ยนรูปแบบจริงๆ (ดังนั้น array.join เร็วกว่าการต่อสตริง) ตอนนี้เราใช้ ropes (หรือคล้ายกัน) ซึ่งช่วยบรรเทาปัญหา ทั้งคืนผลลัพธ์ที่สอดคล้องตามข้อกำหนดและนั่นคือสิ่งที่สำคัญ!
เรื่องสั้นสั้น: เพียงเพราะความหมายของภาษาจาวาสคริปต์มักจะได้รับกลับของเรา (เช่นเดียวกับข้อบกพร่องเงียบในตัวอย่างของ OP) ไม่ได้หมายความว่าข้อผิดพลาด 'โง่' เพิ่มโอกาสของเราในการคอมไพเลอร์คายรหัสเครื่องจักรที่รวดเร็ว มันถือว่าเราเขียน 'ถูกต้อง' คำแนะนำที่ถูกต้อง: มนต์ปัจจุบันเรา 'ผู้ใช้' (ของภาษาการเขียนโปรแกรม) ต้องมี: ช่วยคอมไพเลอร์อธิบายสิ่งที่เราต้องการโปรดปรานสำนวนทั่วไป (ใช้คำแนะนำจาก asm.js เพื่อความเข้าใจพื้นฐาน เบราว์เซอร์ใดสามารถลองปรับให้เหมาะสมและทำไม)
ด้วยเหตุนี้การพูดคุยเกี่ยวกับประสิทธิภาพจึงเป็นสิ่งที่สำคัญ แต่ก็เป็นเรื่องที่ต้องอธิบายด้วย (และเพราะเหตุใดฉันจึงต้องการที่จะจบลงด้วยการชี้ไปที่ (และข้อความ) เนื้อหาที่เกี่ยวข้อง:
การเข้าถึงคุณสมบัติออบเจกต์ที่ไม่มีอยู่และองค์ประกอบอาเรย์นอกขอบเขตส่งคืนundefined
ค่าแทนที่จะเพิ่มข้อยกเว้น คุณสมบัติแบบไดนามิกเหล่านี้ทำให้การเขียนโปรแกรมใน JavaScript สะดวก แต่ก็ทำให้ยากที่จะรวบรวม JavaScript เป็นรหัสเครื่องที่มีประสิทธิภาพ
...
หลักฐานสำคัญสำหรับการเพิ่มประสิทธิภาพ JIT ที่มีประสิทธิภาพคือโปรแกรมเมอร์ใช้คุณลักษณะแบบไดนามิกของ JavaScript อย่างเป็นระบบ ตัวอย่างเช่นคอมไพเลอร์ JIT ใช้ประโยชน์จากความจริงที่ว่าคุณสมบัติของวัตถุมักจะถูกเพิ่มลงในวัตถุประเภทที่กำหนดในลำดับที่เฉพาะเจาะจงหรือการเข้าถึงอาร์เรย์ที่ไม่อยู่นอกขอบเขตเกิดขึ้นน้อยครั้ง คอมไพเลอร์ของ JIT ใช้ประโยชน์จากสมมติฐานปกติเหล่านี้เพื่อสร้างรหัสเครื่องที่มีประสิทธิภาพในขณะทำงาน หากการบล็อกรหัสเป็นไปตามสมมติฐานเครื่องยนต์ JavaScript จะเรียกใช้รหัสเครื่องที่มีประสิทธิภาพและสร้างขึ้น มิฉะนั้นเครื่องยนต์จะต้องถอยกลับไปที่รหัสช้าลงหรือเพื่อตีความโปรแกรม
ที่มา:
"JITProf: การระบุจาวาสคริปต์ไม่เป็นมิตร JIT" การ
เผยแพร่ Berkeley, 2014, โดย Liang Gong, Michael Pradel, Koushik Sen.
http://software-lab.org/publications/jitprof_tr_aug3_2014.pdf
ASM.JS (ยังไม่ชอบออกจากการเข้าถึงอาร์เรย์ที่ถูกผูกไว้):
การรวบรวมล่วงหน้าเวลา
เนื่องจาก asm.js เป็นชุดย่อยที่เข้มงวดของจาวาสเปคนี้จะกำหนดเฉพาะตรรกะการตรวจสอบ - ความหมายการดำเนินการเป็นเพียงแค่ของจาวาสคริปต์ อย่างไรก็ตามการตรวจสอบ asm.js นั้นคล้อยตามการคอมไพล์ล่วงหน้า (AOT) ยิ่งไปกว่านั้นโค้ดที่สร้างโดยคอมไพเลอร์ AOT นั้นค่อนข้างมีประสิทธิภาพซึ่งมี:
- การเป็นตัวแทนที่ไม่ได้ระบุจำนวนเต็มและตัวเลขทศนิยม
- ไม่มีการตรวจสอบประเภทรันไทม์;
- ไม่มีการรวบรวมขยะ และ
- โหลดฮีปและร้านค้าที่มีประสิทธิภาพ (ด้วยกลยุทธ์การใช้งานที่แตกต่างกันไปตามแพลตฟอร์ม)
รหัสที่ล้มเหลวในการตรวจสอบจะต้องถอยกลับไปสู่การดำเนินการด้วยวิธีดั้งเดิมเช่นการตีความและ / หรือการรวบรวม Just-in-time (JIT)
http://asmjs.org/spec/latest/
และในที่สุดก็https://blogs.windows.com/msedgedev/2015/05/07/bringing-asm-js-to-chakra-microsoft-edge/ หาก
มีส่วนย่อยเล็ก ๆ เกี่ยวกับการปรับปรุงประสิทธิภาพภายในของเครื่องยนต์เมื่อลบขอบเขตออก - ตรวจสอบ (ในขณะที่เพิ่งยกขอบเขต - ตรวจสอบนอกวงนั้นมีการปรับปรุง 40%)
แก้ไข:
โปรดทราบว่าหลายแหล่งพูดคุยเกี่ยวกับระดับที่แตกต่างกันของ JIT- รวบรวมซ้ำเพื่อตีความ
ตัวอย่างเชิงทฤษฎีตามข้อมูลข้างต้นเกี่ยวกับตัวอย่างของ OP:
- โทรไปที่ isPrimeDivisible
- รวบรวม isPrimeDivisible โดยใช้สมมติฐานทั่วไป (เช่นไม่มีการเข้าถึงที่ จำกัด )
- ทำงาน
- BAM ทันใดนั้นอาเรย์ก็สามารถเข้าถึงได้อย่างไร้ขอบเขต (ขวาที่ส่วนท้าย)
- Crap พูดว่า engine มาคอมไพล์ใหม่ที่ isPrimeDivisible โดยใช้สมมติฐานที่ต่างกัน (น้อยกว่า) และเอ็นจิ้นตัวอย่างนี้ไม่ได้พยายามหาว่ามันสามารถนำผลบางส่วนกลับมาใช้ใหม่ได้หรือไม่
- คำนวณงานทั้งหมดโดยใช้ฟังก์ชั่นที่ช้ากว่า (หวังว่ามันจะเสร็จสิ้นไม่เช่นนั้นจะทำซ้ำและคราวนี้ก็แปลรหัส)
- ส่งคืนผลลัพธ์
ดังนั้นเวลานั้นคือ: การ
เรียกใช้ครั้งแรก (ล้มเหลวในตอนท้าย) + การทำงานทั้งหมดซ้ำอีกครั้งโดยใช้รหัสเครื่องที่ช้าลงสำหรับการวนซ้ำแต่ละครั้งการคอมไพล์ซ้ำ ฯลฯ ใช้เวลานานกว่า 2 เท่า ในตัวอย่างเชิงทฤษฎีนี้ !
แก้ไข 2: (ข้อจำกัดความรับผิดชอบ: การคาดเดาตามข้อเท็จจริงด้านล่าง)
ยิ่งฉันคิดถึงมันมากเท่าไหร่ฉันยิ่งคิดว่าคำตอบนี้อาจอธิบายเหตุผลที่เด่นชัดกว่าสำหรับ 'การลงโทษ' ในตัวอย่างที่ผิดพลาด a (หรือโบนัสประสิทธิภาพบน snippet b) ขึ้นอยู่กับว่าคุณคิดอย่างไร) ทำไมฉันถึงชื่นชอบการโทรหาข้อผิดพลาดในการเขียนโปรแกรม (ตัวอย่าง)
มันค่อนข้างน่าดึงดูดหากคิดว่าthis.primes
เป็น 'อาร์เรย์หนาแน่น' ซึ่งเป็นตัวเลขบริสุทธิ์
- ตัวอักษรรหัสตายตัวในซอร์สโค้ด (ผู้สมัคร excel ที่รู้จักกันดีจะกลายเป็นอาร์เรย์ 'ของจริง' เพราะทุกอย่างเป็นที่รู้จักกันแล้วโดยคอมไพเลอร์ก่อนรวบรวมเวลา) หรือ
- ส่วนใหญ่สร้างขึ้นโดยใช้ฟังก์ชั่นตัวเลขกรอกขนาด (
new Array(/*size value*/)
) ในลำดับต่อเนื่อง (ผู้สมัครที่รู้จักกันมานานที่จะกลายเป็นอาร์เรย์ 'จริง')
นอกจากนี้เรายังทราบว่าความprimes
ยาวของอาร์เรย์นั้นถูกแคชไว้เช่นกันprime_count
! (ระบุถึงความตั้งใจและขนาดที่แน่นอน)
นอกจากนี้เรายังทราบว่าเอ็นจิ้นส่วนใหญ่ผ่าน Arays ในขั้นต้นเป็น copy-on-modified (เมื่อจำเป็น) ซึ่งจะทำให้การจัดการพวกมันรวดเร็วยิ่งขึ้น (ถ้าคุณไม่เปลี่ยน)
ดังนั้นจึงมีเหตุผลที่จะสมมติว่า Array primes
น่าจะเป็นอาเรย์ที่ได้รับการปรับปรุงภายในแล้วซึ่งไม่ได้รับการเปลี่ยนแปลงหลังจากการสร้าง (ง่ายต่อการรู้สำหรับคอมไพเลอร์หากไม่มีรหัสที่ปรับเปลี่ยนอาเรย์หลังการสร้าง) และดังนั้นจึงเป็น เครื่องยนต์) เก็บไว้ในวิธีที่ดีที่สุด, สวยมากราวกับว่าTyped Array
มันเป็น
เมื่อฉันพยายามทำให้ชัดเจนกับsum
ตัวอย่างฟังก์ชั่นของฉันอาร์กิวเมนต์ที่ได้รับการส่งผ่านอิทธิพลอย่างมากในสิ่งที่จำเป็นต้องเกิดขึ้นจริงและเช่นนั้นวิธีการรวบรวมรหัสเฉพาะลงในเครื่อง - รหัส การส่งผ่านString
ไปยังsum
ฟังก์ชันไม่ควรเปลี่ยนสตริง แต่เปลี่ยนวิธีการทำงานของ JIT-Compiled! การส่ง Array ไปยังsum
ควรรวบรวมรุ่นอื่น (หรืออาจจะเพิ่มเติมสำหรับประเภทนี้หรือ 'รูปร่าง' ตามที่พวกเขาเรียกมันว่าวัตถุที่ผ่านไปแล้ว) รุ่นของรหัสเครื่อง
ดูเหมือนว่า bonkus เล็กน้อยในการแปลง Typed_Array เหมือนprimes
Array on-the-fly เป็น some_else ในขณะที่คอมไพเลอร์รู้ว่าฟังก์ชั่นนี้จะไม่ปรับเปลี่ยนเลย!
ภายใต้สมมติฐานเหล่านี้ที่เหลือ 2 ตัวเลือก:
- คอมไพล์เป็นตัวเลข - cruncher สมมติว่าไม่มีขอบเขตนอกพบปัญหาหมดขอบเขตเมื่อสิ้นสุดการคอมไพล์และทำงานซ้ำ (ตามที่อธิบายไว้ในตัวอย่างทางทฤษฎีในการแก้ไข 1 ข้างต้น)
- คอมไพเลอร์ตรวจพบแล้ว (หรือสงสัยว่า) ออกจากขอบเขตก่อนหน้าและฟังก์ชั่นนั้นถูกรวบรวม JIT ราวกับว่าอาร์กิวเมนต์ที่ผ่านเป็นวัตถุที่กระจัดกระจายส่งผลให้รหัสเครื่องทำงานช้าลง (เพราะจะมีการตรวจสอบ / แปลง ฯลฯ ) กล่าวอีกนัยหนึ่ง: ฟังก์ชั่นไม่เคยมีคุณสมบัติเหมาะสมสำหรับการเพิ่มประสิทธิภาพบางอย่างมันถูกรวบรวมราวกับว่ามันได้รับอาร์กิวเมนต์ 'sparse array' (- like)
ตอนนี้ฉันสงสัยจริงๆว่าใน 2 สิ่งนี้คืออะไร!
<=
และ<
เหมือนกันทั้งในทางทฤษฎีและในการใช้งานจริงในโปรเซสเซอร์ที่ทันสมัยทั้งหมด (และล่าม)