Почему Symbol # to_proc медленнее в Ruby 1.8.7? - PullRequest
4 голосов
/ 28 июня 2011

Относительная производительность Symbol # to_proc в популярных реализациях Ruby утверждает, что в MRI Ruby 1.8.7 * Symbol#to_proc медленнее, чем альтернатива в своем тесте, на 30% до 130%, но это не такдело в YARV Ruby 1.9.2.

Почему это так?Создатели 1.8.7 не писали Symbol#to_proc на чистом Ruby.

Кроме того, существуют ли драгоценные камни, которые обеспечивают более высокую производительность Symbol # to_proc для 1.8?

(Symbol # to_procначинает появляться, когда я использую ruby-prof, поэтому я не думаю, что я виновен в преждевременной оптимизации)

Ответы [ 3 ]

7 голосов
/ 29 июня 2011

Реализация to_proc в 1.8.7 выглядит следующим образом (см. object.c):

static VALUE
sym_to_proc(VALUE sym)
{
    return rb_proc_new(sym_call, (VALUE)SYM2ID(sym));
}

Принимая во внимание, что реализация 1.9.2 (см. string.c) выглядит следующим образом:

static VALUE
sym_to_proc(VALUE sym)
{
    static VALUE sym_proc_cache = Qfalse;
    enum {SYM_PROC_CACHE_SIZE = 67};
    VALUE proc;
    long id, index;
    VALUE *aryp;

    if (!sym_proc_cache) {
        sym_proc_cache = rb_ary_tmp_new(SYM_PROC_CACHE_SIZE * 2);
        rb_gc_register_mark_object(sym_proc_cache);
        rb_ary_store(sym_proc_cache, SYM_PROC_CACHE_SIZE*2 - 1, Qnil);
    }

    id = SYM2ID(sym);
    index = (id % SYM_PROC_CACHE_SIZE) << 1;

    aryp = RARRAY_PTR(sym_proc_cache);
    if (aryp[index] == sym) {
        return aryp[index + 1];
    }
    else {
        proc = rb_proc_new(sym_call, (VALUE)id);
        aryp[index] = sym;
        aryp[index + 1] = proc;
        return proc;
    }
}

Если вы отбросите всю занятую работу по инициализации sym_proc_cache, то у вас останется (более или менее) это:

aryp = RARRAY_PTR(sym_proc_cache);
if (aryp[index] == sym) {
    return aryp[index + 1];
}
else {
    proc = rb_proc_new(sym_call, (VALUE)id);
    aryp[index] = sym;
    aryp[index + 1] = proc;
    return proc;
}

Таким образом, реальная разница в том, что 1.9.2 to_proc кэширует сгенерированные Procs, а 1.8.7 генерирует новый каждый раз, когда вы вызываете to_proc. Разница в производительности между этими двумя показателями будет увеличиваться при любом тестировании, если вы не выполняете каждую итерацию в отдельном процессе; однако одна итерация для каждого процесса маскирует то, что вы пытаетесь сравнить с начальными затратами.

Кишки rb_proc_new выглядят практически одинаково (см. eval.c для 1.8.7 или proc.c для 1.9.2), но 1.9.2 может немного выиграть от любых улучшений производительности в rb_iterate. Кэширование - это, вероятно, большая разница в производительности.

Стоит отметить, что кэш символа к хешу имеет фиксированный размер (67 записей, но я не уверен, откуда берется 67, вероятно, связанных с количеством операторов и таких, которые обычно используются для преобразования символов в символы). -proc преобразований):

id = SYM2ID(sym);
index = (id % SYM_PROC_CACHE_SIZE) << 1;
/* ... */
if (aryp[index] == sym) {

Если вы используете более 67 символов в качестве проков или если ваши идентификаторы символов перекрываются (мод 67), вы не сможете в полной мере воспользоваться кешированием.

Стиль программирования Rails и 1.9 включает в себя множество сокращений, таких как:

    id = SYM2ID(sym);
    index = (id % SYM_PROC_CACHE_SIZE) << 1;

вместо более длинных явных блочных форм:

ints = strings.collect { |s| s.to_i }
sum  = ints.inject(0) { |s,i| s += i }

Учитывая этот (популярный) стиль программирования, имеет смысл обменять память на скорость, кэшируя поиск.

Вы вряд ли получите более быструю реализацию от гема, поскольку гем должен был бы заменить часть основной функциональности Ruby. Вы можете установить кеширование 1.9.2 в свой источник 1.8.7.

4 голосов
/ 29 июня 2011

Следующий обычный код Ruby:

if defined?(RUBY_ENGINE).nil? # No RUBY_ENGINE means it's MRI 1.8.7
  class Symbol
    alias_method :old_to_proc, :to_proc

    # Class variables are considered harmful, but I don't think
    # anyone will subclass Symbol
    @@proc_cache = {}
    def to_proc
      @@proc_cache[self] ||= old_to_proc
    end
  end
end

сделает MRI на Ruby 1.8.7 Symbol#to_proc чуть менее медленным, чем раньше, но не так быстро, как обычный блок или уже существующий процесс.

Тем не менее, это замедлит работу YARV, Rubinius и JRuby, поэтому if вокруг обезьяньего патча.

Медлительность использования Symbol # to_proc не только из-за MRI 1.8.7каждый раз создавая процесс - даже если вы повторно используете существующий, он все равно медленнее, чем использование блока.

Using Ruby 1.8 head

Size    Block   Pre-existing proc   New Symbol#to_proc  Old Symbol#to_proc
0       0.36    0.39                0.62                1.49
1       0.50    0.60                0.87                1.73
10      1.65    2.47                2.76                3.52
100     13.28   21.12               21.53               22.29

Полный тест и код см. в https://gist.github.com/1053502

1 голос
/ 10 октября 2013

В дополнение к отсутствию кэширования proc с, 1.8.7 также создает (приблизительно) один массив каждый раз, когда вызывается proc.Я подозреваю, что это потому, что сгенерированный proc создает массив для приема аргументов - это происходит даже с пустым proc, который не принимает аргументов.

Вот скрипт, демонстрирующий поведение 1.8.7.Здесь значение имеет только значение :diff, которое показывает увеличение числа массивов.

# this should really be called count_arrays
def count_objects(&block)
  GC.disable
  ct1 = ct2 = 0
  ObjectSpace.each_object(Array) { ct1 += 1 }
  yield
  ObjectSpace.each_object(Array) { ct2 += 1 }
  {:count1 => ct1, :count2 => ct2, :diff => ct2-ct1}
ensure
  GC.enable
end

to_i = :to_i.to_proc
range = 1..1000

puts "map(&to_i)"
p count_objects {
  range.map(&to_i)
}
puts "map {|e| to_i[e] }"
p count_objects {
  range.map {|e| to_i[e] }
}
puts "map {|e| e.to_i }"
p count_objects {
  range.map {|e| e.to_i }
}

Пример вывода:

map(&to_i)
{:count1=>6, :count2=>1007, :diff=>1001}
map {|e| to_i[e] }
{:count1=>1008, :count2=>2009, :diff=>1001}
map {|e| e.to_i }
{:count1=>2009, :count2=>2010, :diff=>1}

Кажется, что простой вызов proc создастмассив для каждой итерации, но литеральный блок, кажется, создает массив только один раз.

Но блоки с несколькими аргументами могут все еще страдать от проблемы:

plus = :+.to_proc
puts "inject(&plus)"
p count_objects {
  range.inject(&plus)
}
puts "inject{|sum, e| plus.call(sum, e) }"
p count_objects {
  range.inject{|sum, e| plus.call(sum, e) }
}
puts "inject{|sum, e| sum + e }"
p count_objects {
  range.inject{|sum, e| sum + e }
}

Пример вывода.Обратите внимание, как мы получаем двойной штраф в случае № 2, потому что мы используем блок из нескольких аргументов, а также вызываем proc.

inject(&plus)
{:count1=>2010, :count2=>3009, :diff=>999}
inject{|sum, e| plus.call(sum, e) }
{:count1=>3009, :count2=>5007, :diff=>1998}
inject{|sum, e| sum + e }
{:count1=>5007, :count2=>6006, :diff=>999}
...