ruby - रूबी 2.2 में कचरा कलेक्टर अप्रत्याशित कुंडल भड़काने वाले
garbage-collection fork (1)
UPD2
अचानक पता चला कि यदि आप स्ट्रिंग को स्वरूपित करते हैं तो सभी मेमोरी निजी क्यों जा रही है - आप स्वरूपण के दौरान कचरा उत्पन्न करते हैं, जीसी अक्षम कर देते हैं, फिर जीसी सक्षम करें, और आपके उत्पन्न डेटा में आपको जारी वस्तुओं के छेद मिले हैं। फिर आप कांटा, और नए कचरा इन छेदों पर कब्जा करना शुरू करते हैं, अधिक कचरा - अधिक निजी पेज।
इसलिए मैंने प्रत्येक 2000 चक्रों में जीसी को चलाने के लिए एक क्लीनअप फ़ंक्शन जोड़ा (बस आलसी जीसी को सक्षम करने में सक्षम नहीं किया गया):
count.times do |i|
cleanup(i)
result << "%20.18f" % rand
end
#......snip........#
def cleanup(i)
if ((i%2000).zero?)
GC.enable; GC.start; GC.disable
end
end
##### main #####
जिसके परिणामस्वरूप (फोर्क के बाद memory_object( 1000 * 1000 * 10)
पैदा करने के साथ):
RUBY_GC_HEAP_INIT_SLOTS=600000 ruby gc-test.rb 0
ruby version 2.2.0
proces pid log priv_dirty shared_dirty
Parent 2501 post alloc 35 0
Parent 2501 4 fork 0 35
Child 2503 4 initial 0 35
Child 2503 8 empty GC 28 22
हां, यह प्रदर्शन को प्रभावित करता है, लेकिन फर्क करने से पहले, यानी आपके मामले में लोड अवधि में वृद्धि।
UPD1
बस मानदंड मिला जिसके द्वारा रूबी 2.2 पुराने ऑब्जेक्ट बिट्स सेट करता है, यह 3 जीसी है, इसलिए यदि आप फोरिंग से पहले जोड़ते हैं तो:
GC.enable; 3.times {GC.start}; GC.disable
# start the forking
आपको मिलेगा (विकल्प कमांड लाइन में 1
है):
$ RUBY_GC_HEAP_INIT_SLOTS=600000 ruby gc-test.rb 1
ruby version 2.2.0
proces pid log priv_dirty shared_dirty
Parent 2368 post alloc 31 0
Parent 2368 4 fork 1 34
Child 2370 4 initial 1 34
Child 2370 8 empty GC 2 32
लेकिन भविष्य में जीसी के कम से कम 100 जीसी के बाद इस तरह के ऑब्जेक्ट्स के व्यवहार के बारे में आगे जांच की जानी चाहिए :old_objects
स्थिर :old_objects
, इसलिए मुझे लगता है कि यह ठीक होना चाहिए
GC.stat
साथ लॉग इन करें यहाँ है
वैसे भी RGENGC_OLD_NEWOBJ_CHECK
विकल्प को शुरुआत से पुराने ऑब्जेक्ट बनाने के लिए भी है, लेकिन मुझे संदेह है कि यह एक अच्छा विचार है, लेकिन एक विशेष मामले के लिए उपयोगी हो सकता है।
पहला उत्तर
ऊपर टिप्पणी में मेरा प्रस्ताव गलत था, वास्तव में बिटमैप टेबल उद्धारकर्ता हैं।
(option = 1)
ruby version 2.0.0
proces pid log priv_dirty shared_dirty
Parent 14807 post alloc 27 0
Parent 14807 4 fork 0 27
Child 14809 4 initial 0 27
Child 14809 8 empty GC 6 25 # << almost everything stays shared <<
इसके अलावा हाथ से और रूबी एंटरप्राइज संस्करण का परीक्षण किया गया था यह सबसे खराब मामलों से केवल आधी बेहतर है।
ruby version 1.8.7
proces pid log priv_dirty shared_dirty
Parent 15064 post alloc 86 0
Parent 15064 4 fork 2 84
Child 15065 4 initial 2 84
Child 15065 8 empty GC 40 46
(मैंने स्क्रिप्ट को 1 जीसी चलाया, जो RUBY_GC_HEAP_INIT_SLOTS
को 600k तक बढ़ाकर)
मैं जीसी को कॉपी-ऑन-लिखित प्रलोभन से कैसे रोकूं, जब मैंने मेरी प्रक्रिया को फोर्क किया? मैंने हाल ही में रूबी में कचरा कलेक्टर के व्यवहार का विश्लेषण किया है, मेरे स्मृति में कुछ मेमोरी मुद्दों के कारण मैंने (मेरे छोटे से छोटे कार्यों के लिए मेरी 60कोर 0.5 टीबी मशीन पर स्मृति से बाहर निकलते हुए) का सामना किया। मेरे लिए यह वास्तव में मल्टीकोर सर्वर पर प्रोग्राम चलाने के लिए रूबी की उपयोगिता को सीमित करता है मैं अपने प्रयोगों और परिणाम यहां प्रस्तुत करना चाहूंगा।
मुद्दा तब उठता है जब कचरा कलेक्टर फर्किंग के दौरान चलता रहता है। मैंने तीन मामलों की जांच की है जो इस मुद्दे को स्पष्ट करते हैं।
मामला 1: हम एक सरणी का उपयोग करके स्मृति में बहुत सारे ऑब्जेक्ट्स (स्ट्रिंग्स को 20 बाइट्स से अधिक नहीं) आवंटित करते हैं। स्ट्रिंग को यादृच्छिक संख्या और स्ट्रिंग फ़ॉर्मेटिंग का उपयोग करके बनाया जाता है। जब प्रक्रिया कांटा जाता है और हम जीसी को बच्चे में चलाने के लिए मजबूर करते हैं, तो सभी साझा स्मृति निजी हो जाती है, जिससे प्रारंभिक स्मृति का दोहराव हो जाता है।
मामले 2: हम एक सरणी का उपयोग करते हुए मेमोरी में बहुत सी ऑब्जेक्ट्स (स्ट्रिंग्स) आवंटित करते हैं, लेकिन स्ट्रैंड को rand.to_s फ़ंक्शन का उपयोग कर बनाया जाता है, इसलिए हम पिछले केस की तुलना में डेटा के स्वरूपण को निकालते हैं। हम कम मात्रा में इस्तेमाल होने वाली स्मृति के साथ समाप्त होते हैं, संभवतः कम कचरा के कारण। जब प्रक्रिया कांटा जाता है और हम जीसी को बच्चे में चलाने के लिए मजबूर करते हैं, तो स्मृति का एक हिस्सा निजी चला जाता है हमारे पास प्रारंभिक स्मृति का दोहराव है, लेकिन एक छोटी सी सीमा तक।
मामला 3: हम पहले की तुलना में कम वस्तुओं को आवंटित करते हैं, लेकिन वस्तुओं बड़ी होती हैं, जैसे कि आवंटित स्मृति की मात्रा पिछले मामलों की तरह ही रहती है। जब प्रक्रिया कांटा जाता है और हम जीसी को बच्चे में चलाने के लिए मजबूर करते हैं तो सभी स्मृति साझा रहती है, अर्थात् कोई मेमोरी दोहराव नहीं।
यहां मैं रूबी कोड पेस्ट कर रहा हूं जिसका इस्तेमाल इन प्रयोगों के लिए किया गया है। मामलों के बीच स्विच करने के लिए आपको केवल स्मृति_ऑब्जेक्ट फ़ंक्शन में "विकल्प" मान को बदलना होगा। उबंटु 14.04 मशीन पर रूबी 2.2.2, 2.2.1, 2.1.3, 2.1.5 और 1.9.3 का उपयोग करके कोड का परीक्षण किया गया था।
मामला 1 के लिए नमूना आउटपुट:
ruby version 2.2.2
proces pid log priv_dirty shared_dirty
Parent 3897 post alloc 38 0
Parent 3897 4 fork 0 37
Child 3937 4 initial 0 37
Child 3937 8 empty GC 35 5
सटीक कोड को पायथन में लिखा गया है और सभी मामलों में सीओडब्ल्यू पूरी तरह से ठीक काम करता है।
मामला 1 के लिए नमूना आउटपुट:
python version 2.7.6 (default, Mar 22 2014, 22:59:56)
[GCC 4.8.2]
proces pid log priv_dirty shared_dirty
Parent 4308 post alloc 35 0
Parent 4308 4 fork 0 35
Child 4309 4 initial 0 35
Child 4309 10 empty GC 1 34
रूबी कोड
$start_time=Time.new
# Monitor use of Resident and Virtual memory.
class Memory
shared_dirty = '.+?Shared_Dirty:\s+(\d+)'
priv_dirty = '.+?Private_Dirty:\s+(\d+)'
MEM_REGEXP = /#{shared_dirty}#{priv_dirty}/m
# get memory usage
def self.get_memory_map( pids)
memory_map = {}
memory_map[ :pids_found] = {}
memory_map[ :shared_dirty] = 0
memory_map[ :priv_dirty] = 0
pids.each do |pid|
begin
lines = nil
lines = File.read( "/proc/#{pid}/smaps")
rescue
lines = nil
end
if lines
lines.scan(MEM_REGEXP) do |shared_dirty, priv_dirty|
memory_map[ :pids_found][pid] = true
memory_map[ :shared_dirty] += shared_dirty.to_i
memory_map[ :priv_dirty] += priv_dirty.to_i
end
end
end
memory_map[ :pids_found] = memory_map[ :pids_found].keys
return memory_map
end
# get the processes and get the value of the memory usage
def self.memory_usage( )
pids = [ $$]
result = self.get_memory_map( pids)
result[ :pids] = pids
return result
end
# print the values of the private and shared memories
def self.log( process_name='', log_tag="")
if process_name == "header"
puts " %-6s %5s %-12s %10s %10s\n" % ["proces", "pid", "log", "priv_dirty", "shared_dirty"]
else
time = Time.new - $start_time
mem = Memory.memory_usage( )
puts " %-6s %5d %-12s %10d %10d\n" % [process_name, $$, log_tag, mem[:priv_dirty]/1000, mem[:shared_dirty]/1000]
end
end
end
# function to delay the processes a bit
def time_step( n)
while Time.new - $start_time < n
sleep( 0.01)
end
end
# create an object of specified size. The option argument can be changed from 0 to 2 to visualize the behavior of the GC in various cases
#
# case 0 (default) : we make a huge array of small objects by formatting a string
# case 1 : we make a huge array of small objects without formatting a string (we use the to_s function)
# case 2 : we make a smaller array of big objects
def memory_object( size, option=1)
result = []
count = size/20
if option > 3 or option < 1
count.times do
result << "%20.18f" % rand
end
elsif option == 1
count.times do
result << rand.to_s
end
elsif option == 2
count = count/10
count.times do
result << ("%20.18f" % rand)*30
end
end
return result
end
##### main #####
puts "ruby version #{RUBY_VERSION}"
GC.disable
# print the column headers and first line
Memory.log( "header")
# Allocation of memory
big_memory = memory_object( 1000 * 1000 * 10)
Memory.log( "Parent", "post alloc")
lab_time = Time.new - $start_time
if lab_time < 3.9
lab_time = 0
end
# start the forking
pid = fork do
time = 4
time_step( time + lab_time)
Memory.log( "Child", "#{time} initial")
# force GC when nothing happened
GC.enable; GC.start; GC.disable
time = 8
time_step( time + lab_time)
Memory.log( "Child", "#{time} empty GC")
sleep( 1)
STDOUT.flush
exit!
end
time = 4
time_step( time + lab_time)
Memory.log( "Parent", "#{time} fork")
# wait for the child to finish
Process.wait( pid)
पायथन कोड
import re
import time
import os
import random
import sys
import gc
start_time=time.time()
# Monitor use of Resident and Virtual memory.
class Memory:
def __init__(self):
self.shared_dirty = '.+?Shared_Dirty:\s+(\d+)'
self.priv_dirty = '.+?Private_Dirty:\s+(\d+)'
self.MEM_REGEXP = re.compile("{shared_dirty}{priv_dirty}".format(shared_dirty=self.shared_dirty, priv_dirty=self.priv_dirty), re.DOTALL)
# get memory usage
def get_memory_map(self, pids):
memory_map = {}
memory_map[ "pids_found" ] = {}
memory_map[ "shared_dirty" ] = 0
memory_map[ "priv_dirty" ] = 0
for pid in pids:
try:
lines = None
with open( "/proc/{pid}/smaps".format(pid=pid), "r" ) as infile:
lines = infile.read()
except:
lines = None
if lines:
for shared_dirty, priv_dirty in re.findall( self.MEM_REGEXP, lines ):
memory_map[ "pids_found" ][pid] = True
memory_map[ "shared_dirty" ] += int( shared_dirty )
memory_map[ "priv_dirty" ] += int( priv_dirty )
memory_map[ "pids_found" ] = memory_map[ "pids_found" ].keys()
return memory_map
# get the processes and get the value of the memory usage
def memory_usage( self):
pids = [ os.getpid() ]
result = self.get_memory_map( pids)
result[ "pids" ] = pids
return result
# print the values of the private and shared memories
def log( self, process_name='', log_tag=""):
if process_name == "header":
print " %-6s %5s %-12s %10s %10s" % ("proces", "pid", "log", "priv_dirty", "shared_dirty")
else:
global start_time
Time = time.time() - start_time
mem = self.memory_usage( )
print " %-6s %5d %-12s %10d %10d" % (process_name, os.getpid(), log_tag, mem["priv_dirty"]/1000, mem["shared_dirty"]/1000)
# function to delay the processes a bit
def time_step( n):
global start_time
while (time.time() - start_time) < n:
time.sleep( 0.01)
# create an object of specified size. The option argument can be changed from 0 to 2 to visualize the behavior of the GC in various cases
#
# case 0 (default) : we make a huge array of small objects by formatting a string
# case 1 : we make a huge array of small objects without formatting a string (we use the to_s function)
# case 2 : we make a smaller array of big objects
def memory_object( size, option=2):
count = size/20
if option > 3 or option < 1:
result = [ "%20.18f"% random.random() for i in xrange(count) ]
elif option == 1:
result = [ str( random.random() ) for i in xrange(count) ]
elif option == 2:
count = count/10
result = [ ("%20.18f"% random.random())*30 for i in xrange(count) ]
return result
##### main #####
print "python version {version}".format(version=sys.version)
memory = Memory()
gc.disable()
# print the column headers and first line
memory.log( "header") # Print the headers of the columns
# Allocation of memory
big_memory = memory_object( 1000 * 1000 * 10) # Allocate memory
memory.log( "Parent", "post alloc")
lab_time = time.time() - start_time
if lab_time < 3.9:
lab_time = 0
# start the forking
pid = os.fork() # fork the process
if pid == 0:
Time = 4
time_step( Time + lab_time)
memory.log( "Child", "{time} initial".format(time=Time))
# force GC when nothing happened
gc.enable(); gc.collect(); gc.disable();
Time = 10
time_step( Time + lab_time)
memory.log( "Child", "{time} empty GC".format(time=Time))
time.sleep( 1)
sys.exit(0)
Time = 4
time_step( Time + lab_time)
memory.log( "Parent", "{time} fork".format(time=Time))
# Wait for child process to finish
os.waitpid( pid, 0)
संपादित करें
दरअसल, जीसी को कई बार फांकने से पहले इस मुद्दे को सुलझाने के मुद्दे को हल किया जाता है और मैं काफी आश्चर्यचकित हूं। मैंने रूबी 2.0.0 का इस्तेमाल करते हुए कोड भी चलाया है और यह समस्या भी दिखाई नहीं दे रही है, इसलिए इस तरह के जनरेटेड जीसी से संबंधित होना चाहिए जैसे आपने उल्लेख किया है। हालांकि, अगर मैं किसी भी चर (मैं केवल कचरा पैदा कर रहा हूँ) को आउटपुट निर्दिष्ट किए बिना memory_object फ़ंक्शन कॉल करता हूं, तो स्मृति दोहराया जाता है। जो प्रतिलिपि बनाई गई मेमोरी की मात्रा मेरे द्वारा बनाए गए कचरे की मात्रा पर निर्भर करती है - अधिक कचरा, अधिक मेमोरी निजी हो जाती है
कोई भी विचार मैं इसे कैसे रोक सकता हूं?
यहां कुछ परिणाम दिए गए हैं
2.0.0 में जीसी चलाना
ruby version 2.0.0
proces pid log priv_dirty shared_dirty
Parent 3664 post alloc 67 0
Parent 3664 4 fork 1 69
Child 3700 4 initial 1 69
Child 3700 8 empty GC 6 65
बच्चे में memory_object (1000 * 1000) को कॉल करना
ruby version 2.0.0
proces pid log priv_dirty shared_dirty
Parent 3703 post alloc 67 0
Parent 3703 4 fork 1 70
Child 3739 4 initial 1 70
Child 3739 8 empty GC 15 56
कॉलिंग मेमोरी_ऑब्जेक्ट (1000 * 1000 * 10)
ruby version 2.0.0
proces pid log priv_dirty shared_dirty
Parent 3743 post alloc 67 0
Parent 3743 4 fork 1 69
Child 3779 4 initial 1 69
Child 3779 8 empty GC 89 5