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




copy-on-write