Что быстрее в рубине - поиск хэша или функция с аргументом case?

У нас есть несколько мест в критическом по времени script, где мы конвертируем старые идентификаторы в строки. На данный момент мы используем операторы case внутри функции, например:

def get_name id
  case id
    when 1
      "one thing"
    when 3
      "other thing"
    else
      "default thing"
  end
end

Я рассматриваю возможность замены этого на хэш-поиск, например:

NAMES = {
  1 => "one thing",
  3 => "other thing",
}
NAMES.default = "default thing"

Похоже, что лучше использовать NAMES[id] чем get_name(id) - но это?

Ответы

Ответ 1

Сначала пара очков. Один из них заключается в том, что низкоуровневые языковые конструкции, подобные тому, что более или менее делают то же самое, почти никогда не являются узким местом в любом реальном приложении, поэтому (часто) глупо сосредоточиться на них. Во-вторых, как уже упоминалось, если вы действительно обеспокоены этим, вы должны сравнить его. Рубиновые бенчмаркинга и профильные инструменты, безусловно, не самые продвинутые в экосистеме программирования, но они выполняют свою работу.

Мой интуитивный инстинкт заключается в том, что хеши будут быстрее, потому что (опять же, я предполагаю) оператор case должен проверять каждое условие по очереди (делая поиск элементов O (n) вместо O (1)). Но пусть проверяет!

Полный код бенчмаркинга находится в https://gist.github.com/25 В основном он генерирует файл, который определяет соответствующий регистр/хэш, а затем использует их. Я пошел вперед и поместил хэш-поиск в вызове метода, так что накладные расходы не будут фактором, но в реальной жизни нет причин, по которым он должен зависеть внутри метода.

Вот что я получаю. В каждом случае я делаю 10 000 поисков. Время - это время пользователя в секундах

Case statement, 10 items  0.020000
Hash lookup, 10 items     0.010000

Case statement, 100 items  0.100000
Hash lookup, 100 items     0.010000

Case statement, 1000 items  0.990000
Hash lookup, 1000 items     0.010000

Таким образом, это очень похоже на то, что оператор case равен O (n) (там нет шокер). Также обратите внимание, что поиск 10K по-прежнему остается лишь секундой даже в случае case, поэтому, если вы не выполняете метрику, но загружаете эти запросы, вам лучше сосредоточиться на остальной части вашего кода.

Ответ 2

Так как это зависит от ряда факторов (сколько разных идентификаторов вы хотите конвертировать, насколько интеллектуально компилятор может скомпилировать statemens case when), мой совет: Измерить:

Напишите небольшую тестовую процедуру и конвертируйте, скажем, 10.000.000 идентификаторов в строки. Сделайте это пару раз с реализацией и сравните результаты. Если у вас нет существенной разницы, возьмите то, что вам больше нравится (я думаю, хеш-решение немного более элегантно...)

Ответ 3

$ ruby -v
ruby 1.9.2p0 (2010-08-18 revision 29036) [i686-linux]

$ cat hash_vs_case.rb 
require 'benchmark'

def get_from_case(id)
  case id
    when 1
      "one thing"
    when 3
      "other thing"
    else
      "default thing"
  end
end

NAMES = {
  1 => "one thing",
  3 => "other thing",
}
NAMES.default = "default thing"

def get_from_hash(arg)
  NAMES[arg]
end

n = 1000000
Benchmark.bm do |test|
  test.report("case  1") { n.times do; get_from_case(1); end }
  test.report("hash  1") { n.times do; get_from_hash(1); end}
  test.report("case 42") { n.times do; get_from_case(42); end }
  test.report("hash 42") { n.times do; get_from_hash(42); end}
end

$ ruby -w hash_vs_case.rb 
      user     system      total        real
case  1  0.330000   0.000000   0.330000 (  0.422209)
hash  1  0.220000   0.000000   0.220000 (  0.271300)
case 42  0.310000   0.000000   0.310000 (  0.390295)
hash 42  0.320000   0.010000   0.330000 (  0.402647)

И вот почему вы хотите обновить:

$ ruby -v
ruby 1.8.7 (2009-06-12 patchlevel 174) [i486-linux]

$ ruby -w hash_vs_case.rb 
      user     system      total        real
case  1  1.380000   0.870000   2.250000 (  2.738493)
hash  1  1.320000   0.850000   2.170000 (  2.642013)
case 42  1.500000   0.960000   2.460000 (  3.029923)
hash 42  1.890000   0.890000   2.780000 (  3.456969)

Ответ 4

Вот пример, который показывает случай быстрее для поиска символа. Предыдущие примеры были целыми ключами.

https://gist.github.com/02c8f8ca0cd4c9d9ceb2

Ответ 5

Почему бы не использовать оператор case внутри строки в критическом для времени фрагменте кода, а не сделать его собственным вызовом функции? Это позволяет избежать накладных расходов на стек вызовов, а также позволяет избежать накладных расходов на хэш-поиск.

Вы также можете всегда выполнять бенчмарк самостоятельно. Сделайте что-то вроде этого:

t = Time.now
1_000_000.times { ...your code... }
secs = Time.now - t

Замена "вашего кода" каждой опцией.

Ответ 6

Как сказал Мартин, это зависит от того, сколько идентификаторов вы хотите проверить. Вы выбираете идентификатор и соответствующие строки из базы данных или хотите его жестко закодировать. Если есть только несколько проверок, вы можете пойти с CASE. Но нет необходимости изменять пару ID/String, если это необходимо.

С другой стороны, если вы выбираете много идентификаторов из базы данных, вы можете использовать что-то вроде словаря для хранения пар имя/значение.

В то время как словарь находится в памяти, поиск может быть быстрым.

Но в конце концов все зависит от того, работаете ли вы с постоянно меняющейся парой ID/string или несколькими константами.

Ответ 7

case when имеет n сложность.
hash lookup имеет сложность ln (n), но использование дополнительного объекта (hash) и вызов его методов имеет свое собственное наказание.

Итак, если у вас много случаев (тысячи, миллионы,...) хэш-поиск лучше. Но в вашем случае, когда у вас есть только 3 варианта, case when, конечно, будет намного быстрее.