แนวทางปฏิบัติที่ดีที่สุดในการลดกิจกรรม Garbage Collector ใน Javascript


96

ฉันมีแอป Javascript ที่ค่อนข้างซับซ้อนซึ่งมีลูปหลักที่เรียกว่า 60 ครั้งต่อวินาที ดูเหมือนว่าจะมีการรวบรวมขยะจำนวนมากเกิดขึ้น (ตามเอาต์พุต 'ฟันเลื่อย' จากไทม์ไลน์หน่วยความจำในเครื่องมือ Chrome dev) - และสิ่งนี้มักส่งผลกระทบต่อประสิทธิภาพของแอปพลิเคชัน

ดังนั้นฉันจึงพยายามค้นคว้าแนวทางปฏิบัติที่ดีที่สุดในการลดปริมาณงานที่พนักงานเก็บขยะต้องทำ (ข้อมูลส่วนใหญ่ที่ฉันสามารถหาได้บนเว็บเกี่ยวกับการหลีกเลี่ยงการรั่วไหลของหน่วยความจำซึ่งเป็นคำถามที่แตกต่างกันเล็กน้อย - หน่วยความจำของฉันได้รับการปลดปล่อยเพียงแค่มีการรวบรวมขยะมากเกินไป) ฉันสมมติว่า สิ่งนี้ส่วนใหญ่มาจากการนำวัตถุกลับมาใช้ใหม่ให้มากที่สุด แต่แน่นอนว่าปีศาจอยู่ในรายละเอียด

แอปพลิเคมีโครงสร้างใน 'เรียน' ตามสายของจอห์นเรซิกง่าย JavaScript มรดก

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

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

ฉันสามารถใช้เทคนิคอะไรเพื่อลดปริมาณงานที่พนักงานเก็บขยะต้องทำ?

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


2
คุณมีตัวอย่างรหัสของคุณที่จะแสดงให้เราเห็นหรือไม่? คำถามจะง่ายกว่าที่จะตอบ (แต่อาจจะกว้างน้อยกว่าด้วยดังนั้นฉันไม่แน่ใจที่นี่)
John Dvorak

2
แล้วหยุดการทำงานหลายพันครั้งต่อวินาทีล่ะ? นั่นเป็นวิธีเดียวที่จะเข้าถึงสิ่งนี้ได้จริงหรือ? คำถามนี้ดูเหมือนปัญหา XY คุณกำลังอธิบาย X แต่สิ่งที่คุณกำลังมองหาจริงๆคือวิธีแก้ปัญหาของ Y.
Travis J

2
@TravisJ: เขาทำงานเพียง 60 ครั้งต่อวินาทีซึ่งเป็นอัตราการเคลื่อนไหวที่ค่อนข้างธรรมดา เขาไม่ขอทำงานน้อยลง แต่จะทำอย่างไรให้มีประสิทธิภาพในการเก็บขยะมากขึ้น
Bergi

1
@Bergi - "ฟังก์ชันบางอย่างสามารถเรียกได้หลายพันครั้งต่อวินาที" นั่นคือหนึ่งครั้งต่อมิลลิวินาที (อาจแย่กว่านั้น!) ที่ไม่ธรรมดาเลย 60 ครั้งต่อวินาทีไม่ควรเป็นปัญหา คำถามนี้คลุมเครือมากเกินไปและเป็นเพียงการแสดงความคิดเห็นหรือการคาดเดา
Travis J

4
@TravisJ - มันไม่ใช่เรื่องแปลกเลยในเฟรมเวิร์กของเกม
UpTheCreek

คำตอบ:


131

หลายสิ่งที่คุณต้องทำเพื่อลด GC churn ให้น้อยที่สุดให้เทียบกับสิ่งที่ถือว่าเป็นสำนวน JS ในสถานการณ์อื่น ๆ ส่วนใหญ่ดังนั้นโปรดคำนึงถึงบริบทเมื่อตัดสินคำแนะนำที่ฉันให้

การจัดสรรเกิดขึ้นในล่ามสมัยใหม่ในหลาย ๆ ที่:

  1. เมื่อคุณสร้างออบเจ็กต์ผ่านnewหรือผ่านทางไวยากรณ์ตามตัวอักษร[...]หรือ{}.
  2. เมื่อคุณเชื่อมสตริงเข้าด้วยกัน
  3. เมื่อคุณป้อนขอบเขตที่มีการประกาศฟังก์ชัน
  4. เมื่อคุณดำเนินการที่ทำให้เกิดข้อยกเว้น
  5. เมื่อคุณประเมินนิพจน์ฟังก์ชัน: (function (...) { ... }).
  6. เมื่อคุณดำเนินการที่บังคับให้ Object เหมือนObject(myNumber)หรือNumber.prototype.toString.call(42)
  7. เมื่อคุณเรียกบิวอินที่ทำสิ่งเหล่านี้ภายใต้ประทุนเช่นArray.prototype.slice.
  8. เมื่อคุณใช้argumentsเพื่อสะท้อนรายการพารามิเตอร์
  9. เมื่อคุณแยกสตริงหรือจับคู่กับนิพจน์ทั่วไป

หลีกเลี่ยงการทำสิ่งเหล่านั้นและรวมและนำวัตถุกลับมาใช้ใหม่หากเป็นไปได้

โดยเฉพาะอย่างยิ่งมองหาโอกาสที่จะ:

  1. ดึงฟังก์ชั่นภายในที่ไม่มีการพึ่งพาหรือไม่กี่อย่างในสถานะปิดออกไปสู่ขอบเขตที่สูงขึ้นและมีอายุการใช้งานยาวนานขึ้น (ตัวย่อโค้ดบางตัวเช่นคอมไพเลอร์ปิดสามารถอินไลน์ฟังก์ชันภายในและอาจปรับปรุงประสิทธิภาพ GC ของคุณ)
  2. หลีกเลี่ยงการใช้สตริงเพื่อแสดงข้อมูลที่มีโครงสร้างหรือสำหรับการกำหนดแอดเดรสแบบไดนามิก โดยเฉพาะอย่างยิ่งหลีกเลี่ยงการแยกวิเคราะห์ซ้ำ ๆ โดยใช้splitหรือการจับคู่นิพจน์ทั่วไปเนื่องจากแต่ละรายการต้องมีการจัดสรรวัตถุหลายรายการ สิ่งนี้มักเกิดขึ้นกับคีย์ในตารางการค้นหาและ ID โหนด DOM แบบไดนามิก ตัวอย่างเช่นlookupTable['foo-' + x]และdocument.getElementById('foo-' + x)ทั้งสองเกี่ยวข้องกับการจัดสรรเนื่องจากมีการต่อสายอักขระ บ่อยครั้งที่คุณสามารถแนบกุญแจกับวัตถุที่มีอายุยืนยาวแทนที่จะเชื่อมต่อกันใหม่ คุณอาจสามารถMapใช้ออบเจ็กต์เป็นคีย์ได้โดยตรงทั้งนี้ขึ้นอยู่กับเบราว์เซอร์ที่คุณต้องการรองรับ
  3. หลีกเลี่ยงการจับข้อยกเว้นบนเส้นทางรหัสปกติ แทนการทำtry { op(x) } catch (e) { ... }if (!opCouldFailOn(x)) { op(x); } else { ... }
  4. เมื่อคุณไม่สามารถหลีกเลี่ยงการสร้างสตริงเช่นเพื่อส่งข้อความไปยังเซิร์ฟเวอร์ให้ใช้บิวอินJSON.stringifyที่ใช้บัฟเฟอร์เนทีฟภายในเพื่อสะสมเนื้อหาแทนที่จะจัดสรรอ็อบเจ็กต์หลายตัว
  5. หลีกเลี่ยงการใช้การโทรกลับสำหรับเหตุการณ์ที่มีความถี่สูงและในกรณีที่คุณสามารถส่งผ่านเป็นฟังก์ชันที่มีอายุการใช้งานยาวนาน (ดู 1) ที่สร้างสถานะขึ้นมาใหม่จากเนื้อหาข้อความ
  6. หลีกเลี่ยงการใช้argumentsตั้งแต่ฟังก์ชันที่ใช้ที่ต้องสร้างวัตถุคล้ายอาร์เรย์เมื่อถูกเรียก

ฉันแนะนำให้ใช้JSON.stringifyเพื่อสร้างข้อความเครือข่ายขาออก การแยกวิเคราะห์ข้อความที่ป้อนโดยใช้JSON.parseชัดเจนเกี่ยวข้องกับการจัดสรรและจำนวนมากสำหรับข้อความขนาดใหญ่ หากคุณสามารถแสดงข้อความขาเข้าของคุณเป็นอาร์เรย์ของแบบดั้งเดิมคุณจะสามารถบันทึกการจัดสรรได้มาก builtin เฉพาะอื่น ๆ String.prototype.charCodeAtที่คุณสามารถสร้างแยกวิเคราะห์ที่ไม่ได้จัดสรรเป็น ตัวแยกวิเคราะห์สำหรับรูปแบบที่ซับซ้อนซึ่งใช้เฉพาะที่จะอ่านได้อย่างชั่วร้าย


คุณไม่คิดว่าJSON.parseวัตถุ d จัดสรรพื้นที่น้อยกว่า (หรือเท่ากัน) กว่าสตริงข้อความหรือไม่?
Bergi

@Bergi ขึ้นอยู่กับว่าชื่อคุณสมบัติต้องการการจัดสรรแยกต่างหาก แต่ตัวแยกวิเคราะห์ที่สร้างเหตุการณ์แทนที่จะเป็นแผนผังการแยกวิเคราะห์จะไม่มีการจัดสรรที่ไม่เกี่ยวข้อง
Mike Samuel

คำตอบที่ยอดเยี่ยมขอบคุณ! ขอโทษหลายครั้งสำหรับเงินรางวัลที่หมดอายุ - ฉันกำลังเดินทางในเวลานั้นและด้วยเหตุผลบางประการฉันไม่สามารถเข้าสู่ระบบ SO ด้วยบัญชี Gmail บนโทรศัพท์ของฉันได้ .... : /
UpTheCreek

เพื่อชดเชยช่วงเวลาที่ไม่ดีของฉันด้วยเงินรางวัลฉันได้เพิ่มอีกหนึ่งรายการเพื่อเติมเงิน (200 คือขั้นต่ำที่ฉันสามารถให้ได้;) - ด้วยเหตุผลบางอย่างแม้ว่าฉันจะต้องรอ 24 ชั่วโมงก่อนที่จะได้รับรางวัลก็ตาม (แม้ว่า ฉันเลือก 'ให้รางวัลแก่คำตอบที่มีอยู่') พรุ่งนี้จะเป็นของคุณ ...
UpTheCreek

@UpTheCreek ไม่ต้องกังวล ฉันดีใจที่คุณพบว่ามีประโยชน์
Mike Samuel

12

Chrome เครื่องมือสำหรับนักพัฒนามีคุณลักษณะที่ดีมากสำหรับการติดตามการจัดสรรหน่วยความจำ เรียกว่า Memory Timeline บทความนี้อธิบายรายละเอียดบางประการ ฉันคิดว่านี่คือสิ่งที่คุณกำลังพูดถึง "ฟันเลื่อย"? นี่เป็นพฤติกรรมปกติสำหรับรันไทม์ส่วนใหญ่ของ GC การจัดสรรจะดำเนินไปจนกว่าจะถึงเกณฑ์การใช้งานที่เรียกใช้คอลเลกชัน โดยปกติจะมีคอลเล็กชันหลายประเภทในเกณฑ์ที่ต่างกัน

ไทม์ไลน์หน่วยความจำใน Chrome

คอลเลกชันขยะจะรวมอยู่ในรายการเหตุการณ์ที่เกี่ยวข้องกับการติดตามพร้อมกับระยะเวลา ในสมุดบันทึกที่ค่อนข้างเก่าของฉันคอลเลกชันชั่วคราวจะเกิดขึ้นที่ประมาณ 4Mb และใช้เวลา 30ms นี่คือ 2 ของการวนซ้ำ 60Hz ของคุณ หากเป็นภาพเคลื่อนไหวคอลเลกชั่น 30ms อาจทำให้พูดติดอ่าง คุณควรเริ่มต้นที่นี่เพื่อดูว่าเกิดอะไรขึ้นในสภาพแวดล้อมของคุณ: เกณฑ์การเก็บรวบรวมอยู่ที่ใดและการเก็บรวบรวมของคุณใช้เวลานานเท่าใด สิ่งนี้ทำให้คุณมีจุดอ้างอิงในการประเมินการเพิ่มประสิทธิภาพ แต่คุณอาจทำได้ไม่ดีไปกว่าการลดความถี่ของการพูดติดอ่างโดยการทำให้อัตราการจัดสรรช้าลงทำให้ช่วงเวลาระหว่างคอลเลกชันยาวขึ้น

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

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

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


ถูกต้องนั่นคือสิ่งที่ฉันหมายถึงฟันเลื่อย ฉันรู้ว่ามันจะมีรูปแบบฟันเลื่อยอยู่เสมอ แต่สิ่งที่ฉันกังวลคือแอพของฉันความถี่ฟันเลื่อยและ 'หน้าผา' นั้นค่อนข้างสูง ที่น่าสนใจเหตุการณ์ GC จะไม่แสดงขึ้นบนไทม์ไลน์ของฉัน - เหตุการณ์เดียวที่ปรากฏในบานหน้าต่าง 'บันทึก' (หนึ่งกลาง) คือ: request animation frame, และanimation frame fired composite layersฉันไม่รู้ว่าทำไมฉันถึงไม่เห็นGC Eventแบบคุณ (นี่คือ Chrome เวอร์ชันล่าสุดและ Canary ด้วย)
UpTheCreek

4
ฉันได้ลองใช้ profiler กับ 'บันทึกการจัดสรรฮีป' แต่จนถึงขณะนี้ยังไม่พบว่ามีประโยชน์มากนัก บางทีอาจเป็นเพราะฉันไม่รู้วิธีใช้อย่างถูกต้อง มันน่าจะเป็นแบบเต็มรูปแบบของการอ้างอิงที่ไม่มีความหมายอะไรกับผมเช่นและ@342342 code relocation info
UpTheCreek

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

9

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

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

var options = {var1: value1, var2: value2, ChangingVariable: value3};
function loopfunc()
{
    //do something
}

while(true)
{
    $.each(listofthings, loopfunc);

    options.ChangingVariable = newvalue;
    someOtherFunction(options);
}

จะทำงานได้เร็วกว่านี้มาก:

while(true)
{
    $.each(listofthings, function(){
        //do something on the list
    });

    someOtherFunction({
        var1: value1,
        var2: value2,
        ChangingVariable: newvalue
    });
}

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

ขออภัยหากทั้งหมดนี้เป็นเรื่องเล็กน้อยเมื่อเทียบกับสิ่งที่คุณได้ลองและคิดไว้แล้ว


นี้. นอกจากนี้ฟังก์ชันที่กล่าวถึงในฟังก์ชันอื่น ๆ (ที่ไม่ใช่ IIFE) ยังเป็นการละเมิดทั่วไปที่เผาผลาญหน่วยความจำจำนวนมากและพลาดง่าย
Esailija

ขอบคุณคริส! ฉันไม่มีเวลาหยุดทำงานใด ๆ น่าเสียดาย: /
UpTheCreek

4

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

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

ปล. มันอาจทำให้โค้ดบางส่วนนั้นดูแลรักษาได้น้อยลงเล็กน้อย


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