Получение memsize общего пространства массива

#ruby #memory-management

#ruby #управление памятью

Вопрос:

tl; dr

 require 'objspace'

ObjectSpace.memsize_of([0] * 1_000_000)
#=> 8000040
ObjectSpace.memsize_of(Array.new([0] * 1_000_000))
#=> 40
  

Куда это делось?

Более длинная версия

Похоже, что целая куча вещей внутри массива имеет концепцию «общего массива», в котором блок данных перемещается в общее пространство кучи. Я знаю, что memsize_of это дает понять, что оно может быть неполным, но есть ли (хорошее?) способ проанализировать распределение этих общих блоков массива? Они не кажутся «объектами» с точки зрения ObjectSpace.each_object . Для целей этого профилировщика памяти было бы неплохо, по крайней мере, иметь возможность отслеживать общий размер общего пространства кучи массива, даже если я не могу отследить его до конкретных объектов.

Комментарии:

1. ObjectSpace.memsize_of(([0] * 1_000_000).dup) (или clone ) 40 тоже возвращает.

2. Вещь, которая может быть частью ответа: ObjectSpace.dump сообщит вам, что массив является общим, и даст вам указатель, который, я думаю, является основным общим распределением, но вы не можете превратить это обратно во что-то, что можно передать memsize_of . Может быть достаточно (в сочетании со знанием размера) для оценки использования памяти общего массива.

3. Дальнейшее усложнение вышесказанного, если вы пытаетесь измерить использование памяти для какого-то определенного фрагмента кода, у вас может быть объект массива, указывающий на общий буфер, который был создан перед вашим блоком, поэтому мне нужно проверить самое раннее поколение с тем же reference указателем, который я думаю.

4. Кстати, почему вы хотите отслеживать эти общие массивы?

5. Я работаю над инструментом профилирования памяти для Chef, и из-за того, как работают внутренние структуры данных Chef, большинство массивов оборачиваются эквивалентом Array.new , что, возможно, вызывает поведение перемещения общей кучи. Я мог бы просто проигнорировать это, поскольку в любом случае это наилучший подход, но было бы неплохо отслеживать.

Ответ №1:

К счастью rb_ary_memsize , это общедоступная функция, поэтому с небольшим взломом вы можете это сделать:

 #include <ruby.h>
#include <assert.h>

/* private macros from array.c */
#define ARY_OWNS_HEAP_P(a) (!FL_TEST((a), ELTS_SHARED|RARRAY_EMBED_FLAG))
#define ARY_SHARED_P(ary) 
    (assert(!FL_TEST((ary), ELTS_SHARED) || !FL_TEST((ary), RARRAY_EMBED_FLAG)), 
     FL_TEST((ary),ELTS_SHARED)!=0)

RUBY_FUNC_EXPORTED size_t
rb_ary_memsize(VALUE ary)
{
    if (ARY_OWNS_HEAP_P(ary)) {
        return RARRAY(ary)->as.heap.aux.capa * sizeof(VALUE);
    }
/* -------8<------8<------- */
    else if (ARY_SHARED_P(ary)){
        /* if it is a shared array, calculate size using length of shared root */
        return RARRAY_LEN(RARRAY(ary)->as.heap.aux.shared) * sizeof(VALUE);
    }
/* ------->8------>8------- */
    else {
        return 0;
    }
}
  

Скомпилируйте его в общий объект:

 gcc $(ruby -rrbconfig 
  -e'puts RbConfig::CONFIG.values_at("rubyhdrdir","rubyarchhdrdir").map{|d| " -I#{d}"}.join') 
  -Wall -fpic -shared -o ary_memsize_hack.so ary_memsize_hack.c
  

И загрузка в процесс, заменяющий исходную функцию:

 LD_PRELOAD="$(pwd)/ary_memsize_hack.so" ruby -robjspace 
  -e 'p ObjectSpace.memsize_of([0] * 1_000_000); 
      p ObjectSpace.memsize_of(Array.new([0] * 1_000_000))'
  

Это приведет к желаемому результату:

 8000040
8000040
  

Обновить:
rb_ary_memsize функция, отвечающая за оценку размера массива, делает это только для массивов, которым принадлежит куча (т. Е. Не Является общим и не встроенным), и возвращает ноль в противном случае. В общем, это имеет смысл, потому что, если вы предполагаете вычислить размер всех массивов в приложениях, в конечном итоге числа должны совпадать, в то время как с моим патчем содержимое общих массивов будет подсчитываться несколько раз. Я думаю, основная проблема заключается в том, как обертывающий массив создается на стороне ruby: по сути, ссылка на внутренний массив теряется, недоступна для кода приложения и выглядит как несчетная. Мой патч демонстрирует только, как добраться до корня общего массива, чтобы указать размер, но я не думаю, что это должно быть каким-либо образом интегрировано в восходящий поток. Аналогичная проблема была бы со встроенными массивами, поскольку ruby также возвращает 0 в качестве размера, который не показывает, что ожидает увидеть приложение:

 require 'objspace'

puts ObjectSpace.dump([1])
#=> {"address":"0x000008033f9bd8", "type":"ARRAY", "class":"0x000008029f9a98", "length":1, 
#    "embedded":true, "memsize":40, "flags":{"wb_protected":true}}
puts ObjectSpace.dump([1, 2])
#=> {"address":"0x000008033f9b38", "type":"ARRAY", "class":"0x000008029f9a98", "length":2, 
#    "embedded":true, "memsize":40, "flags":{"wb_protected":true}}
puts ObjectSpace.dump([1, 2, 3])
#=> {"address":"0x000008033f9ac0", "type":"ARRAY", "class":"0x000008029f9a98", "length":3, 
#    "embedded":true, "memsize":40, "flags":{"wb_protected":true}}
puts ObjectSpace.dump([1, 2, 3, 4])
#=> {"address":"0x000008033f9a48", "type":"ARRAY", "class":"0x000008029f9a98", "length":4,
#    "memsize":72, "flags":{"wb_protected":true}}
  

Комментарии:

1. Не могли бы вы добавить больше объяснений, почему это происходит в первую очередь? Также есть ли менее хакерский способ перекомпилировать нашу собственную версию? Может быть, что-то с Fiddle ?

2. Я обновил ответ разъяснениями, к сожалению, я ранее не использовал Fiddle API, возможно, это было бы проще, но с LD_PRELOAD ним было бы более надежно. Также я думаю, что поведение ruby в отношении общих массивов является правильным. Приложение действительно должно создавать общий массив с намерением делиться содержимым массива, а не переносить его.