Статьи

Создание Рубина для Производительности

Ремесленник делает вазу из свежей мокрой глины на гончарном круге

В прошлом Ruby имел репутацию медленного языка программирования. Но с появлением Ruby 2.0+ это теперь миф. В этой статье мы рассмотрим производительность МРТ Руби и то, что это значит для ваших программ.

Сначала я хотел бы упомянуть несколько хороших практик в ООП:

  • Определите ключевые абстракции для превращения в «объекты».
  • Инкапсулируйте каждый объект в отдельные обязанности с уникальными проблемами.
  • Назначьте одну цель для каждой инкапсуляции и далее изолируйте ответственность.
  • Ударьте себя по лбу, когда абстракция перегружена слишком многими обязанностями.
  • Повторение.

Во-вторых, я хотел бы подчеркнуть, что эти методы влияют на производительность. Создание монолитного универсального приложения может быть веселым и сложным. Это может даже работать на одной процедуре! Но если вы откроете много запутанных обязанностей для простого запроса, ваш конечный пользователь испытает это.

Однажды я услышал, как мудрый мудрец сказал: «Если производительность вашего веб-приложения отстой, это ваша вина».

Давайте рассмотрим, как производительность Ruby соотносится с этими лучшими практиками, не так ли?

Локальные переменные

Для моих тестов я буду использовать «лабораторную крысу» со следующими характеристиками:

describe "A labrat" do it "has binding information" do assert LabRat.get_binding.kind_of?(Binding) end it "is a class" do assert LabRat.kind_of?(Class) end end 

Мне также нужен способ получить уникальные имена переменных, поэтому попробуйте:

 describe "A unique name" do it "is in fact unique" do names = {} 20.times{ names[get_unique_name] = 1 } assert names.keys.length == 20 end end 

Я позволю себе представить, что нужно, чтобы пройти эти испытания.

Теперь для моего теста. В этом случае я сосредоточусь на локальных переменных:

 b = LabRat.get_binding Benchmark.bm do |bench| bench.report "create #{Benchmarks::T_DATA} local variables" do Benchmarks::T_DATA.times do eval("#{get_unique_name} = 1", b) end end end lvarr = eval("local_variables", b) Benchmark.bm do |bench| bench.report "retrieve #{Benchmarks::T_DATA} local variables" do Benchmarks::T_DATA.times do eval("#{lvarr.sample}", b) end end end 

Здесь происходит много магии. Я снова открываю свою лабораторную крысу и наполняю ее кучей локальных переменных. Затем я проверяю, как работает Ruby.

Итак, вот результаты:

 user system total real create 5000 local variables 0.330000 0.000000 0.330000 (0.332978) user system total real retrieve 5000 local variables 0.450000 0.010000 0.460000 (0.448220) 

Создание занимает примерно то же самое, что поиск локальных переменных. Это может быть связано с накладными расходами, которые я добавляю в привязку к моим переменным.

Создание и получение 5000 локальных переменных с помощью моего MacBook Air 2013 года занимает менее секунды. Неплохо.

Итак, что насчет переменных экземпляра?

Переменные экземпляра

Со всеми модульными тестами и эталонным фундаментом. Давайте углубимся в код:

 o = LabRat.new Benchmark.bm do |bench| bench.report "create #{Benchmarks::T_DATA} instance variables" do Benchmarks::T_DATA.times do o.instance_eval("@#{get_unique_name} = 1") end end end ivarr = o.instance_variables Benchmark.bm do |bench| bench.report "retrieve #{Benchmarks::T_DATA} instance variables" do Benchmarks::T_DATA.times do o.instance_eval("#{ivarr.sample}") end end end 

Я использую instance_eval чтобы наполнить мою лабораторную крысу кучей переменных экземпляра. Давайте посмотрим эти цифры:

 user system total real create 5000 instance variables 0.170000 0.000000 0.170000 (0.165712) user system total real retrieve 5000 instance variables 0.030000 0.000000 0.030000 (0.033587) 

Захватывающий. Переменные экземпляра в два или три раза быстрее локальных переменных. Эти ориентиры начинают рассказывать историю.

Будет интересно посмотреть, как работают переменные класса. Только из этих двух тестов кажется, что шансы в пользу инкапсуляции вашего кода. Инкапсуляция является важной частью ООП. Тот факт, что переменные экземпляра превосходят локальные переменные, говорит, что Ruby предпочитает писать код таким образом.

Но как насчет переменных класса?

Переменные класса

Продвигаясь дальше, давайте посмотрим на этот тестовый код:

 Benchmark.bm do |bench| bench.report "create #{Benchmarks::T_DATA} class variables" do Benchmarks::T_DATA.times do LabRat.class_eval("@@#{get_unique_name} = 1") end end end cvarr = LabRat.class_variables Benchmark.bm do |bench| bench.report "retrieve #{Benchmarks::T_DATA} class variables" do Benchmarks::T_DATA.times do LabRat.class_eval("#{cvarr.sample}") end end end 

Как и в предыдущем примере, я наполняю своего маленького парня кучей переменных класса. Волшебство происходит через class_eval для создания моих переменных. Давайте посмотрим на эти результаты:

 user system total real create 5000 class variables 0.160000 0.010000 0.170000 (0.163653) user system total real retrieve 5000 class variables 0.030000 0.000000 0.030000 (0.035327) 

Похоже, что переменные класса работают примерно так же, как переменные экземпляра. Теперь пришло время рассказать вам историю.

Как уже упоминалось, в ООП инкапсуляция имеет большое значение. Это хлеб с маслом того, что движет вашими абстракциями. Чтобы поддерживать производительность Ruby на должном уровне, вы должны избегать больших методов. Хорошее практическое правило: если вы пишете слишком много локальных переменных, самое время разбить их. Хороший принцип проектирования здесь состоит в том, чтобы выделить одну проблему в один метод. Инкапсуляция может координировать обязанности между проблемами.

Подводя итог, давайте кратко рассмотрим хэши и массивы. Поскольку мы уже рассмотрели переменные, мы можем ожидать, что они также следуют аналогичной схеме.

Итак, давайте проверим.

Массивы

Есть небольшие различия с кодом. Я подведу итог здесь.

Для создания локальных массивов вы делаете:

 name = get_unique_name eval("#{name} = []; #{name} << 1", b) 

Локальные массивы можно получить с помощью:

 laarr = eval("local_variables", b) name = eval("#{laarr.sample}", b) eval("#{name}[0]", b) 

Присвоение имени локальной переменной добавляет некоторых накладных расходов. Давайте посмотрим на эти результаты:

 user system total real create 5000 local arrays 0.360000 0.000000 0.360000 (0.356111) user system total real retrieve 5000 local arrays 0.500000 0.010000 0.510000 (0.508840) 

Неплохо, примерно так же, как мои локальные переменные.

Теперь для создания массивов экземпляров вы делаете:

 o.instance_eval("@#{name} = []; @#{name} << 1") 

Массивы экземпляров можно получить с помощью:

 iaarr = o.instance_variables o.instance_eval("#{iaarr.sample}[0]") 

Результаты:

 user system total real create 5000 instance arrays 0.230000 0.000000 0.230000 (0.229147) user system total real retrieve 5000 instance arrays 0.050000 0.000000 0.050000 (0.047157) 

Медленнее, чем мои переменные экземпляра. Но все же, намного быстрее, чем мои локальные массивы.

Теперь для создания массивов классов вы делаете:

 LabRat.class_eval("@@#{name} = []; @@#{name} << 1") 

Массивы классов можно получить с помощью:

 caarr = LabRat.class_variables LabRat.class_eval("#{caarr.sample}[0]") 

Теперь результаты:

 user system total real create 5000 class arrays 0.210000 0.000000 0.210000 (0.215799) user system total real retrieve 5000 class arrays 0.050000 0.000000 0.050000 (0.048175) 

С этими результатами мы получаем последовательную картину. Это хорошо, так как показывает стабильность в языке. Как и ожидалось, массивы классов работают примерно так же, как массивы экземпляров.

Хэш

Код теста для хэшей очень похож на массив, поэтому я не буду его здесь показывать.

Результаты:

 user system total real create 5000 local hashes 0.390000 0.000000 0.390000 (0.384721) user system total real retrieve 5000 local hashes 0.550000 0.010000 0.560000 (0.558132) user system total real create 5000 instance hashes 0.260000 0.000000 0.260000 (0.253513) user system total real retrieve 5000 instance hashes 0.050000 0.000000 0.050000 (0.048339) user system total real create 5000 class hashes 0.230000 0.000000 0.230000 (0.236260) user system total real retrieve 5000 class hashes 0.060000 0.000000 0.060000 (0.052424) 

Эти результаты рассказывают мне ту же историю. В Ruby вы получите повышение производительности, когда будете использовать переменные экземпляра и класса вместо локальных переменных.

Вывод

Производительность Ruby значительно улучшилась за эти годы. Одно из самых замечательных улучшений в Ruby MRI 2.0 — это сборщик мусора.

Принимая во внимание все эти результаты, помните, что производительность приложений — это больше искусство, чем наука. Однажды я слышал о проекте, где клиенты жаловались, что приложение «медленное». Все тесты производительности показали, что он может справиться с нагрузкой. Оказывается, это был запутанный пользовательский интерфейс и рабочий процесс, который замедлял людей. По моему опыту, разумные принципы проектирования ООП приводят к интуитивно понятным интерфейсам. Отношения между объектами становятся понятными. Это делает приложение «быстрым» с точки зрения клиента. Руби, как показано, является идеальным кандидатом для этого типа дизайна.

Если вы заинтересованы, вы можете скачать код на GitHub.

Счастливого взлома!