เหตุใด <= ช้ากว่า <โดยใช้ข้อมูลโค้ดนี้ใน V8


166

ฉันกำลังอ่านสไลด์การ จำกัด Javascript Speed ​​กับ V8และมีตัวอย่างเช่นโค้ดด้านล่าง ฉันไม่สามารถเข้าใจได้ว่าทำไม<=ช้ากว่า<ในกรณีนี้ใครสามารถอธิบายได้บ้าง ความคิดเห็นใด ๆ ที่ชื่นชม

ช้า:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

(เคล็ดลับ: จำนวนเฉพาะเป็นอาร์เรย์ของความยาวจำนวนมาก)

ได้เร็วขึ้น:

this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i < this.prime_count; ++i) {
        if (candidate % this.primes[i] == 0) return true;
    }
    return false;
} 

[ข้อมูลเพิ่มเติม]การปรับปรุงความเร็วมีความสำคัญในการทดสอบสภาพแวดล้อมในท้องถิ่นผลลัพธ์มีดังนี้:

V8 version 7.3.0 (candidate) 

ช้า:

 time d8 prime.js
 287107
 12.71 user 
 0.05 system 
 0:12.84 elapsed 

ได้เร็วขึ้น:

time d8 prime.js
287107
1.82 user 
0.01 system 
0:01.84 elapsed

10
@DacreDenny ความยากในการคำนวณของ<=และ<เหมือนกันทั้งในทางทฤษฎีและในการใช้งานจริงในโปรเซสเซอร์ที่ทันสมัยทั้งหมด (และล่าม)
TypeIA

1
ฉันได้อ่านเอกสารแล้วมีmainรหัสที่เรียกใช้ฟังก์ชันนั้นในลูปที่ใช้25000เวลาดังนั้นคุณจึงทำการวนซ้ำน้อยลงโดยรวมเมื่อทำการเปลี่ยนแปลง นอกจากนี้ถ้าอาร์เรย์มีความยาว 5 พยายามที่จะได้รับarray[5]จะออกไปข้างนอกขีด จำกัด ของเขาให้คุ้มค่าตั้งแต่อาร์เรย์เริ่มต้นการจัดทำดัชนีในundefined 0
Shidersz

1
มันจะมีประโยชน์ถ้าคำถามนี้อธิบายว่าได้รับการปรับปรุงความเร็วเท่าไหร่ (เช่นเร็วขึ้น 5 เท่า) ดังนั้นผู้คนจะไม่ถูกทิ้งโดยการทำซ้ำซ้ำ ฉันพยายามค้นหาความเร็วในสไลด์ แต่มีจำนวนมากและฉันมีปัญหาในการค้นหามิฉะนั้นฉันจะแก้ไขเอง
Captain Man

@CaptainMan คุณพูดถูกการปรับปรุงความเร็วที่แน่นอนนั้นยากที่จะรวบรวมจากสไลด์เพราะมันครอบคลุมปัญหาต่าง ๆ มากมายในคราวเดียว แต่ในการสนทนาของฉันกับผู้พูดหลังจากการพูดคุยนี้เขายืนยันว่ามันไม่ได้เป็นเพียงเศษเสี้ยวของเปอร์เซ็นต์ตามที่คุณอาจคาดหวังจากการทำซ้ำอีกหนึ่งครั้งในกรณีทดสอบนี้ แต่แตกต่างกันมาก: เร็วกว่าหลายครั้ง ของขนาดหรือมากกว่า และเหตุผลก็คือว่า V8 จะถอยกลับ (หรือถอยกลับในสมัยนั้น) กับรูปแบบอาร์เรย์ที่ปรับให้เหมาะสมเมื่อคุณพยายามอ่านนอกขอบเขตอาร์เรย์
Michael Geary

3
มันอาจจะเป็นประโยชน์ในการเปรียบเทียบรุ่นที่ใช้<=แต่อย่างอื่นทำหน้าที่เหมือนกันกับรุ่นด้วยการทำ< i <= this.prime_count - 1วิธีนี้จะช่วยแก้ปัญหา "การวนซ้ำพิเศษ" และปัญหา "หนึ่งผ่านจุดสิ้นสุดของอาร์เรย์"
TheHansinator

คำตอบ:


132

ฉันทำงานที่ V8 ที่ Google และต้องการให้ข้อมูลเชิงลึกเพิ่มเติมเกี่ยวกับคำตอบและความคิดเห็นที่มีอยู่

สำหรับการอ้างอิงต่อไปนี้เป็นตัวอย่างโค้ดแบบเต็มจากสไลด์ :

var iterations = 25000;

function Primes() {
  this.prime_count = 0;
  this.primes = new Array(iterations);
  this.getPrimeCount = function() { return this.prime_count; }
  this.getPrime = function(i) { return this.primes[i]; }
  this.addPrime = function(i) {
    this.primes[this.prime_count++] = i;
  }
  this.isPrimeDivisible = function(candidate) {
    for (var i = 1; i <= this.prime_count; ++i) {
      if ((candidate % this.primes[i]) == 0) return true;
    }
    return false;
  }
};

function main() {
  var p = new Primes();
  var c = 1;
  while (p.getPrimeCount() < iterations) {
    if (!p.isPrimeDivisible(c)) {
      p.addPrime(c);
    }
    c++;
  }
  console.log(p.getPrime(p.getPrimeCount() - 1));
}

main();

ก่อนอื่นความแตกต่างด้านประสิทธิภาพนั้นไม่เกี่ยวข้องกับตัวดำเนินการ<และ<=ตัวดำเนินการโดยตรง ดังนั้นโปรดอย่ากระโดดผ่านห่วงเพียงเพื่อหลีกเลี่ยง<=ในรหัสของคุณเพราะคุณอ่านใน Stack Overflow ว่ามันช้า --- มันไม่ได้!


ประการที่สองผู้คนชี้ให้เห็นว่าอาร์เรย์นั้นเป็น "โพรง" สิ่งนี้ไม่ชัดเจนจากข้อมูลโค้ดในโพสต์ของ OP แต่ชัดเจนเมื่อคุณดูรหัสที่เริ่มต้นthis.primes:

this.primes = new Array(iterations);

ผลนี้ในอาร์เรย์ที่มีองค์ประกอบชนิดใน V8 แม้ว่าอาร์เรย์จะจบลงอย่างสมบูรณ์เต็มไป / บรรจุ / ติดกัน โดยทั่วไปแล้วการดำเนินการบนอาร์เรย์ที่มีโพรงจะช้ากว่าการดำเนินการบนอาร์เรย์บรรจุ แต่ในกรณีนี้คือความแตกต่างเล็กน้อย: มันจะมีจำนวน 1 เพิ่มเติม Smi ( จำนวนเต็มขนาดเล็ก ) ตรวจสอบ (เพื่อป้องกันหลุม) ในแต่ละครั้งเราตีในวงภายใน ไม่ใช่เรื่องใหญ่!HOLEYthis.primes[i]isPrimeDivisible

TL; DR อาเรย์กำลังHOLEYไม่ใช่ปัญหาที่นี่


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

out-of-ขอบเขตอ่านผลในการthis.primes[i]เป็นundefinedบนบรรทัดนี้:

if ((candidate % this.primes[i]) == 0) return true;

และนั่นนำเราไปสู่ปัญหาจริง : ตัว%ดำเนินการกำลังถูกใช้กับตัวถูกดำเนินการที่ไม่ใช่จำนวนเต็ม!

  • integer % someOtherIntegerสามารถคำนวณได้อย่างมีประสิทธิภาพมาก เอ็นจิ้น JavaScript สามารถสร้างรหัสเครื่องที่มีประสิทธิภาพสูงสุดสำหรับกรณีนี้

  • integer % undefinedในทางกลับกันจำนวนวิธีที่มีประสิทธิภาพน้อยลงFloat64Modเนื่องจากundefinedมีการแสดงเป็นคู่

ข้อมูลโค้ดสามารถปรับปรุงได้โดยการเปลี่ยน<=เป็น<บรรทัดนี้:

for (var i = 1; i <= this.prime_count; ++i) {

... ไม่ใช่เพราะ<=ตัวดำเนินการที่ยอดเยี่ยมกว่าอย่างใดอย่างหนึ่ง<แต่เพียงเพราะสิ่งนี้หลีกเลี่ยงขอบเขตที่ไม่ได้อ่านในกรณีนี้โดยเฉพาะ


1
ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
Samuel Liew

1
เพื่อให้เสร็จสมบูรณ์ 100% คีย์โหลด IC สำหรับสิ่งนี้เวลา [i] ใน isPrimeDivisible จะไป megamorphic ใน V8 โดยไม่คาดคิด ดูเหมือนว่าจะเป็นข้อผิดพลาด: bugs.chromium.org/p/v8/issues/detail?id=8561
Mathias Bynens

226

คำตอบและความคิดเห็นอื่น ๆ กล่าวถึงความแตกต่างระหว่างลูปทั้งสองคือว่าอันแรกเรียกใช้การวนซ้ำมากกว่าอีกอันหนึ่ง นี่เป็นเรื่องจริง แต่ในอาร์เรย์ที่เพิ่มขึ้นถึง 25,000 องค์ประกอบการวนซ้ำหนึ่งครั้งจะมากหรือน้อยจะทำให้เกิดความแตกต่างเล็กน้อย ถ้าเราสมมติความยาวเฉลี่ยเมื่อมันโตขึ้นคือ 12,500 ดังนั้นความแตกต่างที่เราอาจคาดหวังควรจะประมาณ 1 / 12,500 หรือเพียง 0.008%

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

this.primes คืออาร์เรย์ที่ต่อเนื่องกัน (ทุกองค์ประกอบมีค่า) และองค์ประกอบคือตัวเลขทั้งหมด

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

เงื่อนไขหนึ่งคือถ้าองค์ประกอบอาร์เรย์บางส่วนหายไป ตัวอย่างเช่น:

let array = [];
a[0] = 10;
a[2] = 20;

ตอนนี้ค่าของa[1]คืออะไร? มันไม่มีค่า (มันไม่ถูกต้องแม้แต่จะบอกว่ามันมีค่าundefined- องค์ประกอบอาร์เรย์ที่มีundefinedค่าแตกต่างจากองค์ประกอบอาร์เรย์ที่หายไปทั้งหมด)

ไม่มีวิธีในการแสดงสิ่งนี้ด้วยตัวเลขเท่านั้นดังนั้นเอ็นจิน JavaScript จึงถูกบังคับให้ใช้รูปแบบที่ปรับให้เหมาะสมน้อยลง หากa[1]มีค่าตัวเลขเช่นองค์ประกอบอีกสององค์ประกอบอาเรย์อาจถูกปรับให้เหมาะสมกับอาเรย์ของตัวเลขเท่านั้น

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

การวนซ้ำครั้งแรกด้วย<=ความพยายามในการอ่านองค์ประกอบผ่านจุดสิ้นสุดของอาร์เรย์ อัลกอริทึมยังคงทำงานได้อย่างถูกต้องเพราะในการทำซ้ำพิเศษล่าสุด:

  • this.primes[i]หาค่าundefinedเพราะiผ่านจุดสิ้นสุดของอาร์เรย์
  • candidate % undefined(สำหรับค่าใด ๆcandidate) NaNประเมิน
  • NaN == 0falseประเมิน
  • ดังนั้นจึงreturn trueไม่ได้ดำเนินการ

ดังนั้นราวกับว่าการย้ำซ้ำที่ไม่เคยเกิดขึ้น - มันไม่มีผลกับตรรกะที่เหลือ รหัสจะสร้างผลลัพธ์แบบเดียวกับที่ไม่มีรหัสซ้ำ

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

ลูปที่สองที่มี<อ่านองค์ประกอบที่มีอยู่ภายในอาเรย์เท่านั้นดังนั้นจึงอนุญาตให้อาเรย์และรหัสที่ได้รับการปรับให้เหมาะสม

ปัญหาอธิบายไว้ในหน้า 90-91ของการสนทนาพร้อมการสนทนาที่เกี่ยวข้องในหน้าก่อนและหลัง

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

หากสิ่งที่ผู้เขียน V8 กล่าวว่ายังคงเป็นจริงการอ่านที่ผ่านมาในตอนท้ายของอาเรย์จะป้องกันไม่ให้มันถูกปรับให้เหมาะสมและมันจะต้องถอยกลับไปเป็นรูปแบบที่ช้ากว่า

ตอนนี้เป็นไปได้ที่ V8 ได้รับการปรับปรุงในระหว่างนี้เพื่อจัดการกับกรณีนี้ได้อย่างมีประสิทธิภาพหรือเอนจิ้น JavaScript อื่น ๆ จัดการกับมันแตกต่างกัน ฉันไม่รู้ทางเดียวหรืออย่างอื่น แต่การทำให้หมดหวังนี้เป็นสิ่งที่งานนำเสนอพูดถึง


1
ฉันค่อนข้างแน่ใจว่าอาร์เรย์ยังคงต่อเนื่องกัน - ไม่มีเหตุผลที่จะเปลี่ยนเค้าโครงหน่วยความจำ สิ่งที่สำคัญคือการที่ดัชนีไม่อยู่ในขอบเขตการตรวจสอบในการเข้าถึงคุณสมบัติไม่สามารถปรับได้และบางครั้งรหัสจะถูกป้อนundefinedแทนตัวเลขที่นำไปสู่การคำนวณที่แตกต่างกัน
Bergi

1
@Bergi ฉันไม่ใช่ผู้เชี่ยวชาญ JS / V8 แต่วัตถุในภาษา GC มักจะอ้างอิงถึงวัตถุจริง วัตถุจริงเหล่านั้นมีการจัดสรรที่เป็นอิสระแม้ว่าการอ้างอิงจะต่อเนื่องกันเนื่องจากอายุการใช้งานของวัตถุ GC ไม่ได้เชื่อมโยงกัน เครื่องมือเพิ่มประสิทธิภาพสามารถแพ็คการจัดสรรอิสระเหล่านั้นให้อยู่ติดกัน แต่ (a) หน่วยความจำใช้ skyrockets และ (b) คุณมีบล็อกที่ต่อเนื่องกันสองบล็อกที่ถูกวนซ้ำ (การอ้างอิงและข้อมูลที่อ้างอิง) แทนที่จะเป็นหนึ่ง ฉันคิดว่าเครื่องมือเพิ่มประสิทธิภาพที่บ้าสามารถแทรกการอ้างอิงและข้อมูลที่อ้างถึงและมีอาร์เรย์ที่เป็นเจ้าของแถบความจำ ...
Yakk - Adam Nevraumont

1
@Bergi อาร์เรย์อาจยังคงอยู่ติดกันในกรณีที่ไม่ได้รับการปรับให้เหมาะสม แต่องค์ประกอบของอาร์เรย์ไม่ได้เป็นชนิดเดียวกันกับในกรณีที่ปรับให้เหมาะสม รุ่นที่ได้รับการปรับปรุงให้ดีที่สุดคืออาร์เรย์ของตัวเลขอย่างง่ายโดยไม่ต้องมีขนเพิ่มเติม รุ่นที่ไม่ได้รับการปรับปรุงให้ดีที่สุดคืออาร์เรย์ของวัตถุ (รูปแบบวัตถุภายในไม่ใช่ JavaScript Object) เนื่องจากจะต้องสนับสนุนการผสมชนิดข้อมูลใด ๆ ในอาร์เรย์ ดังที่ฉันได้กล่าวไปแล้วโค้ดในลูปที่ถูกป้อนundefinedไม่มีผลต่อความถูกต้องของอัลกอริทึม แต่จะไม่เปลี่ยนการคำนวณเลย (เหมือนกับว่าการทำซ้ำซ้ำไม่เกิดขึ้น)
Michael Geary

3
@Bergi ผู้เขียน V8 ที่กล่าวคำปราศรัยนี้กล่าวว่าความพยายามในการอ่านนอกขอบเขตอาเรย์ทำให้อาร์เรย์นั้นได้รับการปฏิบัติราวกับว่ามันมีหลายประเภท: แทนที่จะเป็นรูปแบบตัวเลขเท่านั้นที่ได้รับการปรับปรุง รูปแบบทั่วไป ในกรณีที่ได้รับการปรับปรุงให้ดีที่สุดมันคือจำนวนของแถวอย่างที่คุณอาจใช้ในโปรแกรม C ในกรณีที่ปรับให้เหมาะสมมันเป็นอาร์เรย์ของValueวัตถุที่สามารถเก็บการอ้างอิงถึงค่าประเภทใด ๆ (ฉันสร้างชื่อขึ้นมาValueแต่ประเด็นก็คือองค์ประกอบอาร์เรย์ไม่ได้เป็นเพียงตัวเลขง่าย ๆ แต่เป็นวัตถุที่ล้อมรอบตัวเลขหรือประเภทอื่น ๆ )
Michael Geary

3
ฉันทำงานที่ V8 อาร์เรย์ที่เป็นปัญหาจะถูกทำเครื่องหมายว่าเป็นHOLEYเพราะมันถูกสร้างขึ้นโดยใช้new Array(n)(แม้ว่าส่วนนี้ของรหัสจะไม่ปรากฏใน OP) HOLEYอาร์เรย์ยังคงอยู่HOLEYตลอดไปใน V8แม้ว่าจะถูกเติมในภายหลัง ที่กล่าวว่าอาร์เรย์ที่มีโพรงไม่ได้เป็นเหตุผลสำหรับปัญหาที่สมบูรณ์แบบในกรณีนี้; มันหมายความว่าเราต้องทำการตรวจสอบ Smi พิเศษในแต่ละรอบ (เพื่อป้องกันหลุม) ซึ่งไม่ใช่เรื่องใหญ่
งัด Bynens

19

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 ทั้งสองเป็นรูปแบบของการเพิ่มประสิทธิภาพ):

  1. Data-Representation: วิธีการแสดง / เก็บ Array ภายในหน่วยความจำ (object, hashmap, array ตัวเลข 'real' เป็นต้น)
  2. ฟังก์ชั่นรหัสเครื่อง: วิธีการรวบรวมรหัสที่เข้าถึง / จัดการ (อ่าน / แก้ไข) 'อาร์เรย์' เหล่านี้

รายการที่ 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 เหมือนprimesArray on-the-fly เป็น some_else ในขณะที่คอมไพเลอร์รู้ว่าฟังก์ชั่นนี้จะไม่ปรับเปลี่ยนเลย!

ภายใต้สมมติฐานเหล่านี้ที่เหลือ 2 ตัวเลือก:

  1. คอมไพล์เป็นตัวเลข - cruncher สมมติว่าไม่มีขอบเขตนอกพบปัญหาหมดขอบเขตเมื่อสิ้นสุดการคอมไพล์และทำงานซ้ำ (ตามที่อธิบายไว้ในตัวอย่างทางทฤษฎีในการแก้ไข 1 ข้างต้น)
  2. คอมไพเลอร์ตรวจพบแล้ว (หรือสงสัยว่า) ออกจากขอบเขตก่อนหน้าและฟังก์ชั่นนั้นถูกรวบรวม JIT ราวกับว่าอาร์กิวเมนต์ที่ผ่านเป็นวัตถุที่กระจัดกระจายส่งผลให้รหัสเครื่องทำงานช้าลง (เพราะจะมีการตรวจสอบ / แปลง ฯลฯ ) กล่าวอีกนัยหนึ่ง: ฟังก์ชั่นไม่เคยมีคุณสมบัติเหมาะสมสำหรับการเพิ่มประสิทธิภาพบางอย่างมันถูกรวบรวมราวกับว่ามันได้รับอาร์กิวเมนต์ 'sparse array' (- like)

ตอนนี้ฉันสงสัยจริงๆว่าใน 2 สิ่งนี้คืออะไร!


2
การอภิปรายที่ดีในประเด็นพื้นฐานบางประการ - อย่างไรก็ตามคุณแทบจะไม่ต้องอธิบายคำตอบเลย (ในประโยคสุดท้าย) อาจเพิ่ม tl; dr ไปด้านบนสุดหรือไม่ เช่น "ลูปที่ช้ากว่านั้นเกิดจากการเกินขอบเขตที่กำหนดไว้ซึ่งบังคับให้เครื่องยนต์ทำการประเมินลูปอีกครั้งโดยไม่มีการปรับให้เหมาะสมอ่านเพื่อเรียนรู้ว่าทำไม"
brichins

@brichins: ขอบคุณและขอบคุณสำหรับข้อเสนอแนะที่ฉันได้ reworded เล็กน้อยในการแก้ไขเพิ่มเติมครั้งที่สองของฉันเพราะยิ่งฉันคิดว่ามันงบที่ด้านบนก็ดูเหมือนว่าถูกต้องเช่นกัน
GitaarLAB

6

หากต้องการเพิ่มความเป็นวิทยาศาสตร์ลงไปนี่คือ jsperf

https://jsperf.com/ints-values-in-out-of-array-bounds

มันทดสอบกรณีการควบคุมของอาร์เรย์ที่เต็มไปด้วย ints และวนลูปทำเลขคณิตแบบแยกส่วนในขณะที่อยู่ภายในขอบเขต มี 5 กรณีทดสอบ:

  • 1. วนออกจากขอบเขต
  • 2. อาร์เรย์ของ Holey
  • 3. โมดูลเลขคณิตเทียบกับ NaNs
  • 4. ค่าที่ไม่ได้กำหนดอย่างสมบูรณ์
  • 5. การใช้ new Array()

มันแสดงให้เห็นว่า 4 รายแรกนั้นแย่มากต่อประสิทธิภาพ การวนรอบนอกขอบเขตนั้นดีกว่าอีก 3 เล็กน้อย แต่ทั้งหมด 4 นั้นช้ากว่ากรณีที่ดีที่สุดประมาณ 98% กรณีที่เกือบจะดีเท่าอาร์เรย์ดิบเพียงไม่กี่เปอร์เซ็นต์ช้าลง
new Array()

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