Запомните функцию Rcpp?

Я написал рекурсивную функцию на R и использовал мемоиз, чтобы ускорить ее. Я попытался еще больше ускорить его, написав в Rcpp, а затем запомнив функцию Rcpp, но функция R работает быстрее. Почему это так и есть ли способ ускорить это в моем использовании?

require(microbenchmark)
require(Rcpp)
require(memoise)

Функция Rcpp:

cppFunction('
double FunCpp (unsigned int i, double d1, double d2, 
                double p, double s, unsigned int imax, 
                double n, double k, double r, 
                double m, double t) {

  if (i == 0) return 0;
  if (i == 1) return log2(-1*d1);
  if (i == 2) return log2(d2*d1 - p*s);

  double x = log2(fabs(-(((imax - (n - i))/imax)*k*r + m + (n - i)*t)));
  x = x + FunCpp(i-1, d1, d2, p, s, imax, n, k, r, m, t);

  double y = log2((n - i + 1)*t*k*r*((imax - ((n - i + 1) - 1))/imax));
  y = y + FunCpp(i-2, d1, d2, p, s, imax, n, k, r, m, t);

  return x + log2(1 - pow(2,y-x));
}
')
FunCpp = memoise(FunCpp)   

Функция R:

FunR = memoise(function(i, d1, d2, p, s, imax, n, k, r, m, t) {

  if(i == 0) 0
  else if(i == 1) log2(-1*d1)
  else if(i == 2) log2(d2*d1 - p*s)
  else {
    x = log2(abs(-(((imax - (n - i))/imax)*k*r + m + (n - i)*t)))
    x = x + FunR(i-1, d1, d2, p, s, imax, n, k, r, m, t)

    y = log2((n - i + 1)*t*k*r*((imax - ((n - i + 1) - 1))/imax))
    y = y + FunR(i-2, d1, d2, p, s, imax, n, k, r, m, t)  

    x + log2(1 - 2^(y-x))
  }
})

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

TestFunR = function() {
  x = sapply(1:31, function(i) {
    FunR(i = 31-i, d1 = -152, d2 = -147.33, p = 150, s = 0.03, 
         imax = 30, n = 31, k = 1, r = 1, m = 2, t = 5)
  })
  forget(FunR)
}

TestFunCpp = function() {
  x = sapply(1:31, function(i) {
    FunCpp(i = 31-i, d1 = -152, d2 = -147.33, p = 150, s = 0.03, 
           imax = 30, n = 31, k = 1, r = 1, m = 2, t = 5)
  })
  forget(FunCpp)
}

microbenchmark(TestFunR(), TestFunCpp())


Unit: milliseconds
         expr        min       lq      mean    median        uq       max neval cld
   TestFunR()   9.979738  10.4910  12.83228  10.91887  11.89264  61.61513   100  a 
 TestFunCpp() 520.955483 528.6965 547.31103 536.73058 547.66377 729.57631   100   b

Изменить: я получил метод, работающий из книги Дирка, до публикации этого.

includeText = '
#include <algorithm>
#include <vector>
#include <stdexcept>
#include <cmath>
#include <iostream>

class F {

  public:
    F(unsigned int n = 200, double d1 = 0, double d2 = 0, double p = 0, double s = 0) {
      memo.resize(n); 
      std::fill( memo.begin(), memo.end(), NAN ); 
      memo[0] = 0;          
      memo[1] = log2(-1*d1);  
      memo[2] = log2(d2*d1 - p*s);
    }

  double FunIL(int i, double d1, double d2, double p, double s, double imax, 
                  double n, double k, double r, double m, double t) {

      if (i < 0) return((double) NAN);
      if (i >= (int) memo.size()) throw std::range_error(\"i too large\");
      if (!std::isnan(memo[i])) return(memo[i]); 

      double x = log2(fabs(-(((imax - (n - i))/imax)*k*r + m + (n - i)*t)));
      x = x + FunIL(i-1, d1, d2, p, s, imax, n, k, r, m, t);

      double y = log2((n - i + 1)*t*k*r*((imax - ((n - i + 1) - 1))/imax));
      y = y + FunIL(i-2, d1, d2, p, s, imax, n, k, r, m, t);

      memo[i] = x + log2(1 - pow(2,y-x));
      return(memo[i]); 
    }
  private:
    std::vector< double > memo; 
};
'
bodyText = '
  int is = Rcpp::as<int>(i);
  double d1s = Rcpp::as<double>(d1);
  double d2s = Rcpp::as<double>(d2);
  double ps = Rcpp::as<double>(p);
  double ss = Rcpp::as<double>(s);
  double imaxs = Rcpp::as<double>(imax);
  double ns = Rcpp::as<double>(n);
  double ks = Rcpp::as<double>(k);
  double rs = Rcpp::as<double>(r);
  double ms = Rcpp::as<double>(m);
  double ts = Rcpp::as<double>(t);
  F f(ns, d1s, d2s, ps, ss);
  return Rcpp::wrap( f.FunIL(is, d1s, d2s, ps, ss, imaxs, ns, ks, rs, ms, ts) );
'

FunInline = cxxfunction(signature(i = "integer", d1 = "numeric", d2 = "numeric", p = "numeric",
                                  s = "numeric", imax = "numeric", n = "numeric", k = "numeric",
                                  r = "numeric", m = "numeric", t = "numeric"),
                        plugin = "Rcpp",
                        verbose = T,
                        incl = includeText,
                        body = bodyText)

Он работает одинаково хорошо (см. TestFunInline):

microbenchmark(TestFunR(), TestFunCpp(), TestFunCpp_Mem(), TestFunInline())
Unit: microseconds
             expr        min         lq        mean      median          uq        max neval cld
       TestFunR()   8871.251   9067.758  10301.8003   9287.5725   9593.1310  19270.081   100  b 
     TestFunCpp() 514415.356 517160.251 522431.2980 519321.6130 523811.7640 584812.731   100   c
 TestFunCpp_Mem()    245.474    264.291    284.8908    281.6105    292.0885    526.870   100 a  
  TestFunInline()    279.686    295.723    378.2134    306.8425    316.0370   6621.364   100 a  

Однако мне не удалось заставить его работать с doParallel. Я оптимизирую целевую функцию для каждого процесса, используя пакеты optim и optimx, и когда я использую% do%, он работает, но когда я использую% dopar%, все, что я вижу, это то, что целевую функцию нельзя оценить по начальным параметрам. Я воспользовался советом Дирка из его множества других сообщений и поместил метод Coatless в пакет, но я не уверен, как поместить метод из книги Дирка в пакет. Это просто моя неопытность в C ++.

Изменить 2: Наконец, он щелкнул, как поместить метод Дирка в исходный файл в моем пакете. Я знаю, что есть и другие дискуссии об использовании Rcpp с doParallel, но я помещаю этот код здесь, потому что это еще один хороший способ решить мою проблему, и, добавив этот код в исходный файл в моем пакете, это оказалось намного проще для меня, чтобы заставить это работать в моем параллельном подходе, чем был встроенный.

class F {

  public:
    F(unsigned int n = 200, double d1 = 0, double d2 = 0, double p = 0, double s = 0) {
      memo.resize(n); 
      std::fill( memo.begin(), memo.end(), NAN ); 
      memo[0] = 0;          
      memo[1] = log2(-1*d1);  
      memo[2] = log2(d2*d1 - p*s);
    }

    double FunIL(int i, double d1, double d2, double p, double s, double imax, 
      double n, double k, double r, double m, double t) {

      if (i < 0) return((double) NAN);
      if (i >= (int) memo.size()) throw std::range_error("\"i too large\"");
      if (!std::isnan(memo[i])) return(memo[i]); 

      double x = log2(fabs(-(((imax - (n - i))/imax)*k*r + m + (n - i)*t)));
      x = x + FunIL(i-1, d1, d2, p, s, imax, n, k, r, m, t);

      double y = log2((n - i + 1)*t*k*r*((imax - ((n - i + 1) - 1))/imax));
      y = y + FunIL(i-2, d1, d2, p, s, imax, n, k, r, m, t);

      memo[i] = x + log2(1 - pow(2,y-x));
      return(memo[i]); 
    }
  private:
    std::vector< double > memo; 
};

// [[Rcpp::export]]
double FunDirk(int i, double d1, double d2, double p, double s, 
                  double imax, double n, double k, double r, 
                  double m, double t) {
    F f(n, d1, d2, p, s);
    return f.FunIL(i, d1, d2, p, s, imax, n, k, r, m, t);

}

person Ben Ernest    schedule 01.05.2016    source источник


Ответы (1)


Запомни меня

Что ж, сначала давайте подумаем о назначении memoise. Цель memoise - кэшировать результаты функции и повторно использовать их. Таким образом, после одного вычисления ему больше не нужно повторно вычислять значение для любой другой последовательности в вычислении, он может просто получить значение из кеша. Это особенно актуально для настройки рекурсивной структуры.

memoise доступ к кеш-памяти в отношении R и C ++

Настройка Memoisize заключается в кэшировании значений функции R. В данном случае эти значения кэшируются. Однако код C ++ не может получить доступ к кешированным значениям. Таким образом, версия C ++ пересчитывает каждое из этих значений. По сути, вы используете:

x = sapply(1:31, function(i) {
    FunCpp(i = 31-i, d1 = -152, d2 = -147.33, p = 150, s = 0.03, 
           imax = 30, n = 31, k = 1, r = 1, m = 2, t = 5)
  })

Big O Algo

Заявление об ограничении ответственности. Следующим должен быть более формальный аргумент, но это было какое-то время.

Чтобы понять алгоритмы, иногда нам нужно использовать так называемое нотацию Big O, которая позволяет нам наблюдать, как код работает асимптотически. Теперь Большой O в этом случае равен O (2 ^ N) из-за двух вызовов вычисления: Fun(i-1) и FunR(i-2). Однако memoise использует хэш-карту / справочную таблицу, вероятно, с большим O равным O(n) в худшем случае и O(1) в лучшем случае. По сути, у нас есть постоянные и экспоненциальные асимптотические результаты.

Улучшение микробенчмарков - без запоминания в C ++

Однако это не обязательно означает, что функция C ++ - мусор. Одним из недостатков R to Rcpp и обратного моста является время задержки между передачей значений между двумя доменами. Таким образом, один из способов немного сократить время вычислений - полностью перенести цикл на C ++.

e.g.

// [[Rcpp::export]]
Rcpp::NumericVector FunCpp_loop(unsigned int e, 
                                double d1, double d2, 
                                double p, double s, unsigned int imax, 
                                double n, double k, double r, 
                                double m, double t){

  Rcpp::NumericVector o(e);

  for(unsigned int i = 0; i < e; i++){

    o(i) = FunCpp(31-(i+1), -152, -147.33, 150, 0.03, 30, 31, 1, 1, 2, 5);

  }

  return o;
}

Однако тесты здесь не особо улучшают ситуацию (даже если предварительно создать вектор 1:31).

Unit: milliseconds
              expr        min         lq       mean     median        uq       max neval
      TestFunR(tv)   8.467568   9.077262   9.986837   9.449952  10.60555  14.91243   100
    TestFunCpp(tv) 476.678391 482.489094 487.687811 486.351087 490.25346 579.38161   100
 TestFunCpp_loop() 478.348070 482.588307 488.234200 486.211347 492.33965 521.10918   100

Мемоциация в C ++

Мы можем применить те же методы запоминания, которые описаны в memoise в C ++. Реализация не такая красивая и приятная, но показывает, что применимы те же принципы.

Для начала сделаем хэш-карту.

// Memoization structure to hold the hash map
struct mem_map{

  // Initializer to create the static (presistent) map
  static std::map<int, double> create_map()
  {
    std::map<int, double> m;
    m.clear();
    return m;
  }

  // Name of the static map for the class
  static std::map<int, double> memo;

};

// Actuall instantiate the class in the global scope (I know, bad me...)
std::map<int, double> mem_map::memo =  mem_map::create_map();

Теперь нам, вероятно, следует сделать некоторые аксессоры для работы с объектом карты.

// Reset the map
// [[Rcpp::export]]
void clear_mem(){
  mem_map::memo.clear();
}

// Get the values of the map.
// [[Rcpp::export]]
std::map<int, double> get_mem(){
  return mem_map::memo;
}

Наконец, давайте изменим некоторые внутренние вещи в вашей функции.

// Users function
// [[Rcpp::export]]
double FunCpp_Mem (int i, double d1, double d2, 
                   double p, double s, unsigned int imax, 
                   double n, double k, double r, 
                   double m, double t) {

  // We have already computed the value...
  if(mem_map::memo.count(i) > 0)
    return mem_map::memo[i];


  // Otherwise, let us get ready to compute it!
  double res = 0; 

  if (i <= 2){ 
    if (i <= 0) { // i == 1 
      res = 0.0;
    }else if (i == 1) {
      res = log2(-1.0*d1);
    }else { // i == 2
      res = log2(d2*d1 - p*s);
    }

    // Store result in hashmap
    mem_map::memo[i] = res;

    return res;
  }

  // Calculate if not in special case.

  double x = log2(fabs(-(((imax - (n - i))/imax)*k*r + m + (n - i)*t)));
  x = x + FunCpp_Mem(i-1, d1, d2, p, s, imax, n, k, r, m, t);

  double y = log2((n - i + 1)*t*k*r*((imax - ((n - i + 1) - 1))/imax));
  y = y + FunCpp_Mem(i-2, d1, d2, p, s, imax, n, k, r, m, t);


  res = x + log2(1 - pow(2,y-x));


  // Update the hashmap for uncalculated value
  mem_map::memo[i] = res;

  return res;
}

Уф Много работы. Давайте проверим, стоит ли оно того.

# Benchmark for Rcpp Memoization
TestFunCpp_mem = function(tv) {
  x = sapply(tv, function(i) {
    FunCpp_Mem(i = 31-i, d1 = -152, d2 = -147.33, p = 150, s = 0.03, 
               imax = 30, n = 31, k = 1, r = 1, m = 2, t = 5)
  })
  clear_mem()
}

TestFunR = function(tv) {
  x = sapply(tv, function(i) {
    FunR(i = 31-i, d1 = -152, d2 = -147.33, p = 150, s = 0.03, 
         imax = 30, n = 31, k = 1, r = 1, m = 2, t = 5)
  })
  forget(FunR)
}

# Pre-generate vector
tv = 1:31

microbenchmark(TestFunR(tv),TestFunCpp_mem(tv))

И результаты ....

microbenchmark(TestFunR(tv),TestFunCpp_mem(tv))
Unit: microseconds
               expr      min       lq      mean   median       uq       max neval
       TestFunR(tv) 8246.324 8662.694 9345.6947 9009.868 9797.126 13001.995   100
 TestFunCpp_mem(tv)  203.832  214.939  253.7931  228.898  240.906  1277.325   100

Функция Cpp с мемоизацией примерно в 40,5 раз быстрее, чем версия R! Мемоизация определенно того стоила!

person coatless    schedule 02.05.2016
comment
Вау, большое спасибо за код и полное объяснение. Красиво работает! Я использую это для оптимизации параметров модели, и это дает мне хорошее ускорение. Я очень ценю это. - person Ben Ernest; 02.05.2016
comment
Отлично сделано. Как бы то ни было, я также обсуждаю мемоизацию, в том числе ее выполнение на C ++, во вводной главе книги Rcpp, используя рекурсивную последовательность Фибоначчи в качестве рабочего примера, который должен быть оптимизирован. - person Dirk Eddelbuettel; 02.05.2016
comment
Привет, Дирк, пожалуйста, ознакомьтесь с моей редакцией метода из вашей книги. Спасибо. - person Ben Ernest; 02.05.2016
comment
@BenErnest, основная цель этого ответа состояла в том, чтобы подчеркнуть, почему memoise не работал в структуре serial (например, не parallel). Пожалуйста, создайте новый вопрос, если есть проблема с распараллеливанием. (Я ожидаю, что там будет ...) - person coatless; 02.05.2016
comment
@ Точка без шубы взята. Я отвечал Дирку, говоря, что его метод тоже отлично работает, но ваш изначально мне было проще реализовать в моем параллельном подходе, потому что ваш код работал как есть в моем исходном файле. Подумав еще немного, стало ясно, как добавить метод Дирка в мой исходный файл, поэтому я добавил его в свой пост. Дело в том, что оба метода работают отлично, и, поскольку они находятся в исходном файле в пакете, их также легко реализовать с помощью doParallel. Еще раз спасибо, вы действительно помогли мне лучше понять, что я делаю. - person Ben Ernest; 02.05.2016