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


Ровно поэтому в настоящей заметке будут затронуты 5 важных моментов (можно назвать и 6 и 10, но пусть будет 5), работа над которыми очень часто помогает преобразить медленный код. Запись, скорее, реферативная и в приземлении на R, поскольку сами принципы давно известны. "Шпаргалка" для начала исследований по возможной оптимизации существующего кода.


  1. Все расчетные данные должны быть в оперативной памяти.


  2. Все инварианты по отношению к переменным цикла должны вычисляться за пределами цикла.


  3. Используйте копирование объектов эффективно.


  4. Следите за структурой объектов, чистите мусор.


  5. Оптимизируйте с умом.

Теперь по шагам.


Все в оперативной памяти


Не занимайтесь «просто» обработкой данных. Проанализируйте задачу, посчитайте объемы данных с которыми работаете, посмотрите, как можно разделить данные на автономные блоки данных, соразмерные доступной вам оперативной памяти. Можно эти блоки обсчитывать последовательно, а можно можно, если есть такая возможность, распараллелить работу над каждым независимым блоком, («Map — Reduce»). Своп на диск — просаживание в производительности на несколько порядков.


«Выносите за скобки»


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


Следите за копированием


В языках высокого уровня за каждым объектом\переменной могут скрываться достаточно сложные структуры, содержащие массу дополнительной служебной и мета информации. И тогда простая процедура выполнения операции над объектом, наподобие такой:


t <- f(t, n)

может привести к динамическому выделению памяти и копированию далеко не одного десятка байт. Будучи помещенным в цикл, такая процедура может съесть любую частоту любого процессора и попросить еще.


Выбрасывайте мусор


Берегите оперативную память. Использовали объект и больше он вам не нужен? Удалите вручную, не дожидайтесь работы мусорщика. Можете и не дождаться.


Посчитали регрессию и нужна только таблица значений? А вы знаете сколько весит lm объект и что там лишнего?
В качестве примера запись из блога: «Reducing your R memory footprint by 7000x»


А вы уверены, что применяете функции эффективно? Знаете, как они устроены? Вот еще интересный пример:


t <- raw.df$timestamp
object.size(t) # имеем на входе 130 кб
m <- lapply(t, function(x){round(x, units="hours")}) # так из 130 кб получаем 35 Мб
m <- lapply(t, function(x){round_date(x, unit = "hour")}) # тут получаем 8Мб !!!
m <- round_date(t, unit = "hour") # 130 кб, самый быстрый и компактный вариант

Если дальше работать с этим объектом m не только на чтение, что будет быстрее обрабатывать 130кб или 35Мб? По-моему, ответ вполне очевиден.


Оптимизация кода


Если код исполняется медленно, посмотрите профайл исполнения. Найдите узкие места и оптимизируйте именно их. Не тратьте время на оптимизацию некритичных узлов.


И не забываем высказывание великого ученого: "The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming." — Дональд Кнут, лекция «Computer Programming as an Art», напечатанная в сборнике «Communications of the ACM» (Vol. 17, Issue 12, декабрь 1974, стр. 671)


Полезные ссылки с рекомендациями по отладке и профилировке.



Заключение


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

Поделиться с друзьями
-->

Комментарии (11)


  1. knagaev
    20.09.2016 17:22

    Может второе правило лучше написать в виде «Старайтесь не использовать циклы, но если уж используете, то...» и далее ваш текст?
    P.S. Спасибо за ещё одну статью-мотивацию.


    1. i_shutov
      20.09.2016 17:32

      Я сознательно не стал писать такую формулировку.


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


      С учетом современного развития пакетов в R очень удобно пользоваться итераторами и функциональным подходом. Естественно, что предварительно необходимо их понять и научиться легкости владения.
      Современную позицию про итераторы можно почитать у Hadley: "21. Iteration"


      1. knagaev
        20.09.2016 17:42

        Часто «встроенные» средства не чуть медленнее, а как бык овцу.
        Для питона (и наверняка для R) особенно это хорошо видно на научных библиотеках типа numpy.
        В этой статье, которая условно для новичков, хорошо было бы написать.
        Можно и в конце этого пункта добавить типа «но ещё бОльшего ускорения вы можете добиться путём умного применения итераторов».
        Как наживку для пытливых умов:)

        И насчёт «понятнее и проще» тоже не соглашусь — код с map-функциями часто намного легче прочесть, и смысл сразу на поверхности, чем когда разбираешься в хитросплетениях циклов.


        1. i_shutov
          20.09.2016 17:53

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


          Эти рекомендации изначально были выработаны для студентов физиков и математиков, которые занимаются сложными расчетными задачами и не погружены глубоко в детали вычислительного механизма. Не одна дипломная работа была спасена путем катастрофической оптимизации вычислений (с нескольких лет до нескольких суток), причем эти подходы применялись и в C++, и в R, и в Mathematica, и Matlab. Да, красота и четкость немного страдали, но временнОй выйгрыш того стоил.


          map\walk подходы очень хороши и компактны, но надо их принять и полюбить. По моему опыту, функциональное программирование не для всех органично.


        1. i_shutov
          20.09.2016 17:57

          Как я указал в тексте, это всего-лишь '"шпаргалка" для начала исследований по возможной оптимизации существующего кода.'


          Но даже в такой простой повествовательной форме этот пост вызывает сильную неоднозначность у читающих. Я вот по оценкам статьи в целом вижу, что + и — идут в почти одинаковой пропорции. И это без каких-либо комментариев, видимо просто по первым абзацам.


    1. i_shutov
      20.09.2016 17:43

      Кстати, самое интересное, что тема для очередного поста появляется в ходе дискуссий и вопросов по предыдущим. Дискуссии идут как в комментариях, так по почте. Приходится доставать кусочки кода из различных задач, решавшихся ранее, и актуализировать подходы, в соответствии с состоянием R на текущий день.


  1. artem_klevtsov
    20.09.2016 17:26

    У R есть ещё один замечательный инструмент оптимизации — пакет Rcpp, который даже при минимальном уровне владения синтаксисом C++ позволяет в некоторых случаях в разы ускорить код.


  1. i_shutov
    20.09.2016 17:31

    Это уже следующий шаг, выход за рамки R.


  1. i_shutov
    22.09.2016 13:00

    Новый термин — Tidyverse, который полезно взять на заметку.



    Не далее как вчера применение parallel + hadleyverse позволило ускорить решение одной процессинговой задачки почти на порядок. Кода при этом стало существенно меньше.


  1. Ananiev_Genrih
    26.09.2016 13:50

    В тему данной статьи состоялась переписка с автором по применимости советов из этого поста к реальной рабочей задаче.
    Есть корпус из 32536 документов (один документ = предложение из 2-10 термов) и словарь термов (дата фрейм: 1-й столбец — какой терм брать, 2-й столбец — на какой заменить)
    Размер словаря = 4005 термов (используется в качестве стеммера с учетом отраслевой специализации)
    Общая цель задачи — классификация документов
    Ранее тупым перебором словаря в цикле с отдачей каждой пары значений в регулярку (gsub) на замену слов корпуса уходило примерно 230 секунд (тупо загружалось одно ядро)
    Благодаря советам автора распараллеливание на 4 ядра дало ускорение в 10 раз (т.е. до 23 секунд).
    + к этому в процессе переписки с Ильей переосмыслил некоторые подходы, на будущее теперь есть хороший инструментарий «турбонаддува» R за что автору большое спасибо.
    Илья, спасибо большое.
    P.S. Использовались следующие пакеты:
    library(dplyr)
    library(tidyr)
    library(magrittr)
    library(purrr)
    library(stringr)
    library(tibble)
    library(iterators)
    library(foreach)
    library(doParallel)


    1. i_shutov
      26.09.2016 14:57

      Генрих, спасибо, за комментарий.