#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 в отношении общих массивов является правильным. Приложение действительно должно создавать общий массив с намерением делиться содержимым массива, а не переносить его.