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

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

К счастью, 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

UPDATE: rb_ary_memsize, которая отвечает за оценку размера массива, только делает это для массивов, которые владеют кучей (т.е. не разделены и не внедрены), и возвращает ноль в противном случае. В целом это имеет смысл, потому что, если вы планируете вычислять размер всех массивов в приложениях, в конечном итоге номера должны совпадать, в то время как с моим патчем содержимое общих массивов будет засчитываться несколько раз. Я предполагаю, что основная проблема заключается в том, как массив обтекания, построенный на рубиновой стороне: по существу ссылка на внутренний массив потеряна и недоступна по коду приложения и выглядит несчетной. Мой патч только демонстрирует, как добраться до корня общего массива, чтобы показать размер, но я не думаю, что это должно быть интегрировано в восходящий поток каким-либо образом. Аналогичная проблема будет связана со встроенными массивами, поскольку 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}}