ไล่ล่าปัญหาหน่วยความจำใน Ruby: A Definitive Guide
เผยแพร่แล้ว: 2022-03-11ฉันแน่ใจว่ามีนักพัฒนา Ruby ที่โชคดีหลายคนที่ไม่เคยพบปัญหาเกี่ยวกับหน่วยความจำ แต่สำหรับพวกเราที่เหลือ การค้นหาตำแหน่งที่การใช้หน่วยความจำหมดมือและแก้ไขปัญหานั้นเป็นเรื่องยากอย่างเหลือเชื่อ โชคดี ถ้าคุณใช้ Ruby สมัยใหม่ (2.1+) มีเครื่องมือและเทคนิคดีๆ ที่พร้อมสำหรับจัดการกับปัญหาทั่วไป อาจกล่าวได้ว่าการปรับหน่วยความจำให้เหมาะสมนั้นสนุกและคุ้มค่า แม้ว่าฉันจะอยู่ตามลำพังในความรู้สึกนั้น
เช่นเดียวกับการเพิ่มประสิทธิภาพทุกรูปแบบ มีโอกาสเป็นไปได้ที่โค้ดจะมีความซับซ้อนเพิ่มขึ้น ดังนั้นจึงไม่คุ้มที่จะทำเว้นแต่จะมีกำไรที่วัดได้และมีนัยสำคัญ
ทุกอย่างที่อธิบายไว้ในที่นี้ใช้ MRI Ruby แบบบัญญัติเวอร์ชัน 2.2.4 แม้ว่าเวอร์ชัน 2.1+ อื่น ๆ ควรมีพฤติกรรมคล้ายกัน
ไม่ใช่ความทรงจำที่รั่วไหล!
เมื่อพบปัญหาหน่วยความจำ ง่ายที่จะข้ามไปยังข้อสรุปว่ามีหน่วยความจำรั่ว ตัวอย่างเช่น ในเว็บแอปพลิเคชัน คุณอาจเห็นว่าหลังจากที่คุณเปิดเซิร์ฟเวอร์แล้ว การเรียกซ้ำไปยังปลายทางเดียวกันจะทำให้การใช้หน่วยความจำเพิ่มขึ้นในแต่ละคำขอ มีบางกรณีที่หน่วยความจำรั่วโดยชอบด้วยกฎหมายเกิดขึ้น แต่ฉันพนันได้เลยว่าหน่วยความจำเหล่านี้มีจำนวนมากกว่าปัญหาหน่วยความจำในลักษณะเดียวกันซึ่งไม่ได้รั่วไหลจริงๆ
ตัวอย่าง (ที่ประดิษฐ์ขึ้น) ให้ดูที่โค้ด Ruby เล็กน้อยที่สร้างแฮชจำนวนมากซ้ำแล้วซ้ำอีกและทิ้งมันไป อย่างแรก นี่คือโค้ดบางส่วนที่จะแชร์ผ่านตัวอย่างในโพสต์นี้:
# common.rb require "active_record" require "active_support/all" require "get_process_mem" require "sqlite3" ActiveRecord::Base.establish_connection( adapter: "sqlite3", database: "people.sqlite3" ) class Person < ActiveRecord::Base; end def print_usage(description) mb = GetProcessMem.new.mb puts "#{ description } - MEMORY USAGE(MB): #{ mb.round }" end def print_usage_before_and_after print_usage("Before") yield print_usage("After") end def random_name (0...20).map { (97 + rand(26)).chr }.join end
และตัวสร้างอาร์เรย์:
# build_arrays.rb require_relative "./common" ARRAY_SIZE = 1_000_000 times = ARGV.first.to_i print_usage(0) (1..times).each do |n| foo = [] ARRAY_SIZE.times { foo << {some: "stuff"} } print_usage(n) end
get_process_mem gem เป็นเพียงวิธีที่สะดวกในการรับหน่วยความจำที่ใช้โดยกระบวนการ Ruby ปัจจุบัน สิ่งที่เราเห็นเป็นพฤติกรรมเดียวกันกับที่อธิบายข้างต้น คือการใช้หน่วยความจำเพิ่มขึ้นอย่างต่อเนื่อง
$ ruby build_arrays.rb 10 0 - MEMORY USAGE(MB): 17 1 - MEMORY USAGE(MB): 330 2 - MEMORY USAGE(MB): 481 3 - MEMORY USAGE(MB): 492 4 - MEMORY USAGE(MB): 559 5 - MEMORY USAGE(MB): 584 6 - MEMORY USAGE(MB): 588 7 - MEMORY USAGE(MB): 591 8 - MEMORY USAGE(MB): 603 9 - MEMORY USAGE(MB): 613 10 - MEMORY USAGE(MB): 621
อย่างไรก็ตาม หากเราทำซ้ำๆ กันมากขึ้น ในที่สุดเราก็จะมีที่ราบสูง
$ ruby build_arrays.rb 40 0 - MEMORY USAGE(MB): 9 1 - MEMORY USAGE(MB): 323 ... 32 - MEMORY USAGE(MB): 700 33 - MEMORY USAGE(MB): 699 34 - MEMORY USAGE(MB): 698 35 - MEMORY USAGE(MB): 698 36 - MEMORY USAGE(MB): 696 37 - MEMORY USAGE(MB): 696 38 - MEMORY USAGE(MB): 696 39 - MEMORY USAGE(MB): 701 40 - MEMORY USAGE(MB): 697
การกดปุ่มที่ราบสูงนี้เป็นเครื่องหมายของการไม่ใช่หน่วยความจำรั่วจริง หรือหน่วยความจำรั่วไหลมีขนาดเล็กมากจนมองไม่เห็นเมื่อเทียบกับส่วนที่เหลือของการใช้หน่วยความจำ สิ่งที่อาจไม่สัญชาตญาณก็คือสาเหตุที่การใช้หน่วยความจำยังคงเพิ่มขึ้นหลังจากการทำซ้ำครั้งแรก ท้ายที่สุด มันสร้างอาร์เรย์ขนาดใหญ่ แต่จากนั้นก็ทิ้งมันทันทีและเริ่มสร้างอาร์เรย์ใหม่ที่มีขนาดเท่ากัน ใช้พื้นที่ว่างจากอาร์เรย์ก่อนหน้าไม่ได้หรือ คำตอบที่อธิบายปัญหาของเราคือไม่ นอกเหนือจากการปรับแต่งตัวรวบรวมขยะแล้ว คุณไม่สามารถควบคุมได้ว่าจะทำงานเมื่อใด และสิ่งที่เราเห็นในตัวอย่าง build_arrays.rb
คือการจัดสรรหน่วยความจำใหม่ก่อนที่จะมีการรวบรวมขยะของวัตถุเก่าที่ถูกทิ้งของเรา
ฉันควรชี้ให้เห็นว่านี่ไม่ใช่ปัญหาการจัดการหน่วยความจำที่น่ากลัวสำหรับ Ruby โดยเฉพาะ แต่โดยทั่วไปแล้วจะใช้ได้กับภาษาที่เก็บขยะ เพื่อสร้างความมั่นใจในเรื่องนี้ ฉันทำซ้ำตัวอย่างเดียวกันกับ Go และเห็นผลลัพธ์ที่คล้ายคลึงกัน อย่างไรก็ตาม มีไลบรารี่ Ruby ที่ทำให้ง่ายต่อการสร้างปัญหาหน่วยความจำประเภทนี้
แบ่งแยกและพิชิต
ดังนั้น หากเราต้องทำงานกับข้อมูลจำนวนมาก เราถึงวาระที่จะทิ้ง RAM จำนวนมากไปที่ปัญหาของเราหรือไม่ โชคดีที่ไม่เป็นเช่นนั้น หากเราใช้ตัวอย่าง build_arrays.rb
และลดขนาดอาร์เรย์ เราจะเห็นการลดลงในจุดที่การใช้หน่วยความจำลดลงซึ่งเป็นสัดส่วนโดยประมาณกับขนาดอาร์เรย์
ซึ่งหมายความว่าหากเราสามารถแบ่งงานของเราออกเป็นชิ้นเล็ก ๆ เพื่อประมวลผลและหลีกเลี่ยงการมีอ็อบเจ็กต์มากเกินไปในคราวเดียว เราก็สามารถลดรอยเท้าของหน่วยความจำได้อย่างมาก น่าเสียดาย ที่มักจะหมายถึงการนำโค้ดที่ดีและสะอาดมาใช้ แล้วเปลี่ยนเป็นโค้ดอื่นๆ ที่ทำสิ่งเดียวกัน โดยใช้หน่วยความจำอย่างมีประสิทธิภาพมากขึ้น
การแยกฮอตสปอตการใช้หน่วยความจำ
ใน codebase จริง แหล่งที่มาของปัญหาหน่วยความจำมักจะไม่ชัดเจนเหมือนในตัวอย่าง build_arrays.rb
การแยกปัญหาหน่วยความจำก่อนพยายามเจาะลึกและแก้ไขเป็นสิ่งสำคัญ เนื่องจากง่ายต่อการตั้งสมมติฐานที่ไม่ถูกต้องเกี่ยวกับสาเหตุของปัญหา
โดยทั่วไปฉันใช้สองวิธี ซึ่งมักจะใช้ร่วมกันในการติดตามปัญหาหน่วยความจำ: ปล่อยให้โค้ดไม่เสียหายและล้อมรอบตัวสร้างโปรไฟล์ และตรวจสอบการใช้หน่วยความจำของกระบวนการขณะปิดใช้งาน/เปิดใช้งานส่วนต่างๆ ของโค้ดที่ฉันสงสัยว่าอาจเป็นปัญหาได้ ฉันจะใช้ memory_profiler เพื่อทำโปรไฟล์ แต่ ruby-prof เป็นอีกตัวเลือกยอดนิยม และ derailed_benchmarks มีความสามารถเฉพาะของ Rails ที่ยอดเยี่ยม
นี่คือโค้ดบางส่วนที่จะใช้หน่วยความจำจำนวนมาก ซึ่งอาจไม่ชัดเจนในทันทีว่าขั้นตอนใดที่ผลักดันการใช้หน่วยความจำได้มากที่สุด:
# people.rb require_relative "./common" def run(number) Person.delete_all names = number.times.map { random_name } names.each do |name| Person.create(name: name) end records = Person.all.to_a File.open("people.txt", "w") { |out| out << records.to_json } end
ด้วยการใช้ get_process_mem เราสามารถตรวจสอบได้อย่างรวดเร็วว่ามีการใช้หน่วยความจำจำนวนมากเมื่อมีการสร้างเรคคอร์ด Person
จำนวนมาก
# before_and_after.rb require_relative "./people" print_usage_before_and_after do run(ARGV.shift.to_i) end
ผลลัพธ์:
$ ruby before_and_after.rb 10000 Before - MEMORY USAGE(MB): 37 After - MEMORY USAGE(MB): 96
เมื่อดูโค้ดแล้ว มีหลายขั้นตอนที่ดูเหมือนจะเป็นตัวเลือกที่ดีสำหรับการใช้หน่วยความจำจำนวนมาก: การสร้างสตริงขนาดใหญ่ การเรียก #to_a
บนความสัมพันธ์ Active Record เพื่อสร้างออบเจ็กต์ Active Record จำนวนมาก (ไม่ใช่ความคิดที่ดี แต่ทำขึ้นเพื่อการสาธิต) และทำให้อาร์เรย์ของวัตถุ Active Record เป็นอนุกรม
จากนั้นเราสามารถโปรไฟล์รหัสนี้เพื่อดูว่าการจัดสรรหน่วยความจำเกิดขึ้นที่ไหน:
# profile.rb require "memory_profiler" require_relative "./people" report = MemoryProfiler.report do run(1000) end report.pretty_print(to_file: "profile.txt")
โปรดทราบว่าตัวเลขที่ป้อนเพื่อ run
ที่นี่คือ 1/10 ของตัวอย่างก่อนหน้า เนื่องจากตัวสร้างโปรไฟล์ใช้หน่วยความจำจำนวนมาก และอาจนำไปสู่ความอ่อนล้าของหน่วยความจำได้จริงเมื่อสร้างโปรไฟล์โค้ดที่ทำให้มีการใช้หน่วยความจำสูงอยู่แล้ว
ไฟล์ผลลัพธ์ค่อนข้างยาวและรวมถึงหน่วยความจำและการจัดสรรอ็อบเจ็กต์และการเก็บรักษาที่ระดับอัญมณี ไฟล์ และตำแหน่ง มีข้อมูลมากมายให้สำรวจ แต่ต่อไปนี้คือตัวอย่างข้อมูลบางส่วนที่น่าสนใจ:
allocated memory by gem ----------------------------------- 17520444 activerecord-4.2.6 7305511 activesupport-4.2.6 2551797 activemodel-4.2.6 2171660 arel-6.0.3 2002249 sqlite3-1.3.11 ... allocated memory by file ----------------------------------- 2840000 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activesupport-4.2.6/lib/activ e_support/hash_with_indifferent_access.rb 2006169 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activerecord-4.2.6/lib/active _record/type/time_value.rb 2001914 /Users/bruz/code/mem_test/people.rb 1655493 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activerecord-4.2.6/lib/active _record/connection_adapters/sqlite3_adapter.rb 1628392 /Users/bruz/.rvm/gems/ruby-2.2.4/gems/activesupport-4.2.6/lib/activ e_support/json/encoding.rb
เราเห็นการจัดสรรส่วนใหญ่เกิดขึ้นใน Active Record ซึ่งดูเหมือนจะชี้ไปที่การสร้างอินสแตนซ์ของวัตถุทั้งหมดในอาร์เรย์ records
หรือการทำให้เป็นอันดับด้วย #to_json
ต่อไป เราสามารถทดสอบการใช้หน่วยความจำของเราโดยไม่ต้องใช้ตัวสร้างโปรไฟล์ในขณะที่ปิดการใช้งานผู้ต้องสงสัยเหล่านี้ เราไม่สามารถปิดใช้งานการดึงข้อมูลเรก records
และยังสามารถทำขั้นตอนการทำให้เป็นอนุกรมได้ ดังนั้นเรามาลองปิดการใช้งานการทำให้เป็นอันดับก่อน
# File.open("people.txt", "w") { |out| out << records.to_json }
ผลลัพธ์:

$ ruby before_and_after.rb 10000 Before: 36 MB After: 47 MB
ดูเหมือนว่าจะเป็นที่ที่หน่วยความจำส่วนใหญ่จะไปโดยที่เดลต้าหน่วยความจำก่อน / หลังลดลง 81% โดยการข้ามไป นอกจากนี้เรายังสามารถดูว่าจะเกิดอะไรขึ้นหากเราหยุดบังคับให้สร้างเร็กคอร์ดขนาดใหญ่
# records = Person.all.to_a records = Person.all # File.open("people.txt", "w") { |out| out << records.to_json }
ผลลัพธ์:
$ ruby before_and_after.rb 10000 Before: 36 MB After: 40 MB
ซึ่งจะช่วยลดการใช้หน่วยความจำได้เช่นกัน แม้ว่าจะลดขนาดลงน้อยกว่าการปิดใช้งานการทำให้เป็นอนุกรมก็ตาม ณ จุดนี้ เรารู้แล้วว่าผู้กระทำผิดที่ใหญ่ที่สุดของเรา และสามารถตัดสินใจเกี่ยวกับสิ่งที่ควรเพิ่มประสิทธิภาพตามข้อมูลนี้
แม้ว่าตัวอย่างนี้จะถูกสร้างขึ้นมา แต่แนวทางเหล่านี้ก็ใช้ได้โดยทั่วไป ผลลัพธ์ของ Profiler อาจไม่ชี้ให้คุณเห็นจุดที่แน่นอนในโค้ดของคุณที่ปัญหาอยู่ และยังสามารถตีความได้ ดังนั้นจึงเป็นความคิดที่ดีที่จะติดตามผลโดยดูการใช้หน่วยความจำจริงในขณะที่เปิดและปิดส่วนของโค้ด ต่อไป เราจะดูกรณีทั่วไปที่การใช้หน่วยความจำกลายเป็นปัญหาและวิธีเพิ่มประสิทธิภาพ
ดีซีเรียลไลเซชัน
ปัญหาทั่วไปของหน่วยความจำคือการดีซีเรียลไลซ์ข้อมูลจำนวนมากจาก XML, JSON หรือรูปแบบการทำให้เป็นอนุกรมของข้อมูลอื่นๆ การใช้วิธีการต่างๆ เช่น JSON.parse
หรือ Hash.from_xml
ของ Active Support นั้นสะดวกมาก แต่เมื่อข้อมูลที่คุณกำลังโหลดมีขนาดใหญ่ โครงสร้างข้อมูลที่เป็นผลลัพธ์ที่โหลดในหน่วยความจำอาจมีขนาดมหาศาล
หากคุณควบคุมแหล่งที่มาของข้อมูลได้ คุณสามารถทำสิ่งต่างๆ เพื่อจำกัดปริมาณข้อมูลที่คุณได้รับ เช่น เพิ่มการกรองหรือรองรับการแบ่งหน้า แต่ถ้าเป็นแหล่งภายนอกหรือแหล่งที่คุณไม่สามารถควบคุมได้ อีกทางเลือกหนึ่งคือการใช้ตัวดีซีเรียลไลเซอร์สำหรับสตรีม สำหรับ XML Ox เป็นตัวเลือกหนึ่ง และสำหรับ JSON yajl-ruby ดูเหมือนว่าจะทำงานคล้ายกัน แม้ว่าฉันจะไม่มีประสบการณ์กับมันมากนัก
ต่อไปนี้คือตัวอย่างการแยกวิเคราะห์ไฟล์ XML 1.7MB โดยใช้ Hash#from_xml
# parse_with_from_xml.rb require_relative "./common" print_usage_before_and_after do # From http://www.cs.washington.edu/research/xmldatasets/data/mondial/mondial-3.0.xml file = File.open(File.expand_path("../mondial-3.0.xml", __FILE__)) hash = Hash.from_xml(file)["mondial"]["continent"] puts hash.map { |c| c["name"] }.join(", ") end
$ ruby parse_with_from_xml.rb Before - MEMORY USAGE(MB): 37 Europe, Asia, America, Australia/Oceania, Africa After - MEMORY USAGE(MB): 164
111MB สำหรับไฟล์ 1.7MB! เห็นได้ชัดว่าจะไม่ขยายขนาดให้ดี นี่คือเวอร์ชันแยกวิเคราะห์สตรีมมิ่ง
# parse_with_ox.rb require_relative "./common" require "ox" class Handler < ::Ox::Sax def initialize(&block) @yield_to = block end def start_element(name) case name when :continent @in_continent = true end end def end_element(name) case name when :continent @yield_to.call(@name) if @name @in_continent = false @name = nil end end def attr(name, value) case name when :name @name = value if @in_continent end end end print_usage_before_and_after do # From http://www.cs.washington.edu/research/xmldatasets/data/mondial/mondial-3.0.xml file = File.open(File.expand_path("../mondial-3.0.xml", __FILE__)) continents = [] handler = Handler.new do |continent| continents << continent end Ox.sax_parse(handler, file) puts continents.join(", ") end
$ ruby parse_with_ox.rb Before - MEMORY USAGE(MB): 37 Europe, Asia, America, Australia/Oceania, Africa After - MEMORY USAGE(MB): 37
สิ่งนี้ทำให้เรามีหน่วยความจำเพิ่มขึ้นเล็กน้อยและควรจะสามารถจัดการไฟล์ที่มีขนาดใหญ่กว่าอย่างมากมายได้ อย่างไรก็ตาม ข้อเสียคือตอนนี้เรามีรหัสตัวจัดการ 28 บรรทัดที่เราไม่ต้องการก่อนหน้านี้ ซึ่งดูเหมือนว่าจะมีโอกาสเกิดข้อผิดพลาดได้ และสำหรับการใช้งานจริง ควรมีการทดสอบอยู่บ้าง
การทำให้เป็นอนุกรม
ตามที่เราเห็นในหัวข้อเกี่ยวกับการแยกฮอตสปอตการใช้หน่วยความจำ การทำให้เป็นอนุกรมอาจมีต้นทุนหน่วยความจำสูง นี่คือส่วนสำคัญของ people.rb
จากก่อนหน้านี้
# to_json.rb require_relative "./common" print_usage_before_and_after do File.open("people.txt", "w") { |out| out << Person.all.to_json } end
รันสิ่งนี้ด้วย 100,000 เร็กคอร์ดในฐานข้อมูล เราได้รับ:
$ ruby to_json.rb Before: 36 MB After: 505 MB
ปัญหาในการเรียก #to_json
ที่นี่คือการสร้างอินสแตนซ์ของอ็อบเจ็กต์สำหรับทุกเรคคอร์ด จากนั้นจึงเข้ารหัสเป็น JSON การสร้างเรกคอร์ดโดยเรกคอร์ด JSON เพื่อให้มีออบเจ็กต์เรคคอร์ดเพียงตัวเดียวในแต่ละครั้งจะลดการใช้หน่วยความจำลงอย่างมาก ดูเหมือนว่าไลบรารี Ruby JSON ยอดนิยมจะไม่จัดการเรื่องนี้ แต่แนวทางที่แนะนำโดยทั่วไปคือการสร้างสตริง JSON ด้วยตนเอง มี json-write-stream gem ที่ให้ API ที่ดีสำหรับการทำเช่นนี้ และการแปลงตัวอย่างของเราเป็นดังนี้:
# json_stream.rb require_relative "./common" require "json-write-stream" print_usage_before_and_after do file = File.open("people.txt", "w") JsonWriteStream.from_stream(file) do |writer| writer.write_object do |obj_writer| obj_writer.write_array("people") do |arr_writer| Person.find_each do |people| arr_writer.write_element people.as_json end end end end end
เป็นอีกครั้งที่เราเห็นว่าการเพิ่มประสิทธิภาพได้ให้โค้ดแก่เรามากขึ้น แต่ผลลัพธ์ก็ดูคุ้มค่า:
$ ruby json_stream.rb Before: 36 MB After: 56 MB
ขี้เกียจ
คุณสมบัติที่ยอดเยี่ยมที่เพิ่มเข้ามาใน Ruby ที่เริ่มต้นด้วย 2.0 คือความสามารถในการทำให้ตัวแจงนับขี้เกียจ สิ่งนี้ยอดเยี่ยมสำหรับการปรับปรุงการใช้หน่วยความจำเมื่อเชื่อมโยงเมธอดบนตัวแจงนับ เริ่มจากโค้ดที่ไม่ขี้เกียจกันก่อน:
# not_lazy.rb require_relative "./common" number = ARGV.shift.to_i print_usage_before_and_after do names = number.times .map { random_name } .map { |name| name.capitalize } .map { |name| "#{ name } Jr." } .select { |name| name[0] == "X" } .to_a end
ผลลัพธ์:
$ ruby not_lazy.rb 1_000_000 Before: 36 MB After: 546 MB
สิ่งที่เกิดขึ้นที่นี่คือในแต่ละขั้นตอนในเชน มันวนซ้ำทุกองค์ประกอบในตัวแจงนับ สร้างอาร์เรย์ที่มีเมธอดที่ตามมาในเชนที่เรียกใช้ และอื่นๆ มาดูกันว่าจะเกิดอะไรขึ้นเมื่อเราทำให้ขี้เกียจซึ่งเพียงแค่ต้องเพิ่มการเรียก lazy
ในการแจงนับที่เราได้รับจาก times
:
# lazy.rb require_relative "./common" number = ARGV.shift.to_i print_usage_before_and_after do names = number.times.lazy .map { random_name } .map { |name| name.capitalize } .map { |name| "#{ name } Jr." } .select { |name| name[0] == "X" } .to_a end
ผลลัพธ์:
$ ruby lazy.rb 1_000_000 Before: 36 MB After: 52 MB
สุดท้ายนี้ ตัวอย่างที่ทำให้เราชนะการใช้หน่วยความจำอย่างมหาศาล โดยไม่ต้องเพิ่มโค้ดพิเศษมากมาย! โปรดทราบว่าหากเราไม่ต้องการสะสมผลลัพธ์ใดๆ ในตอนท้าย เช่น หากแต่ละรายการถูกบันทึกลงในฐานข้อมูลแล้วสามารถถูกลืมได้ ก็จะมีการใช้หน่วยความจำน้อยลงไปอีก หากต้องการทำการประเมินการแจงนับแบบขี้เกียจที่ส่วนท้ายของเชน เพียงเพิ่มการเรียกครั้งสุดท้ายเพื่อ force
อีกสิ่งหนึ่งที่ควรทราบเกี่ยวกับตัวอย่างคือ chain เริ่มต้นด้วยการเรียก times
ก่อน lazy
ซึ่งใช้หน่วยความจำน้อยมาก เนื่องจากมันเพิ่งส่งคืนตัวแจงนับที่จะสร้างจำนวนเต็มทุกครั้งที่เรียกใช้ ดังนั้นหากสามารถใช้การนับแทนอาร์เรย์ขนาดใหญ่ที่จุดเริ่มต้นของ chain ได้ นั่นจะช่วยได้
แอปพลิเคชั่นหนึ่งในโลกแห่งความเป็นจริงในการสร้างการนับจำนวนเพื่อป้อนอย่างเกียจคร้านในไปป์ไลน์การประมวลผลบางประเภทคือการประมวลผลข้อมูลที่มีเลขหน้า ดังนั้น แทนที่จะขอหน้าทั้งหมดและใส่ไว้ในอาร์เรย์ขนาดใหญ่เดียว พวกเขาอาจถูกเปิดเผยผ่านตัวแจงนับที่ซ่อนรายละเอียดการแบ่งหน้าทั้งหมดไว้อย่างดี นี่อาจมีลักษณะดังนี้:
def records Enumerator.new do |yielder| has_more = true page = 1 while has_more response = fetch(page) response.records.each { |record| yielder << record } page += 1 has_more = response.has_more end end end
บทสรุป
เราได้ทำการกำหนดลักษณะเฉพาะของการใช้หน่วยความจำใน Ruby และดูเครื่องมือทั่วไปบางอย่างสำหรับการติดตามปัญหาหน่วยความจำ เช่นเดียวกับกรณีทั่วไปและวิธีปรับปรุง กรณีทั่วไปที่เราสำรวจไม่ครอบคลุมและมีความลำเอียงอย่างมากจากปัญหาที่ฉันพบเป็นการส่วนตัว อย่างไรก็ตาม สิ่งที่ได้รับมากที่สุดอาจเป็นแค่การคิดว่าโค้ดจะส่งผลต่อการใช้หน่วยความจำอย่างไร