Перевод еще одной виньетки по dplyr.
Hybrid evaluation
2015-06-15
Перевод
https://cran.r-project.org/web/packages/dplyr/vignettes/hybrid-evaluation.html
Рассмотрим этот вызов
summarise
:summarise(per_day, flights = sum(flights))
Один из способов, которым dplyr достигает значительного ускорения, состоит в том, что выражения могут вычисляться не R, а альтернативным кодом, который работает быстрее и использует меньше памяти.В принципе, вызов функции
summarise
, представленный выше, вычисляет выражение sum(flights)
на каждом поднаборе flights
в соответствии с группировкой per_day
. Это включает в себя создание нового вектора в R для хранения блока и вычисление выражения R.Вычисление выражения R может повлечь расходы, которых можно избежать, т.е. S3 диспетчеризацию…
dplyr распознаёт выражение
sum(flights)
как функцию sum
, применяемую к известному столбцу данных, что делает возможным выполнить обработку диспетчеризации заранее и один раз, избегая ненужного выделения памяти и вычисляя результат быстрее.Гибридное вычисление способно работать на подвыражениях. Рассмотрим:
foo <- function(x) x*x
summarise(per_day, flights = foo(sum(flights)) )
dplyr заменяет подвыражения, которые он умеет обрабатывать, и остваляет остальное для стандартного вычисления в R. Вместо вычисления foo(sum(flights))
R будет вычислять только foo(z)
, где z
является результатов внутреннего вычисления sum(flights)
.Реализация
Гибридное вычисление призвано быть расширяемым. Прежде чем мы приступим к регистрации пользовательских обработчиков гибридных вычислений, нам нужно понять систему.Первый строительный блок, который нам необходимо охватить, это класс
Result
.namespace dplyr {
class Result {
public:
Result(){}
virtual ~Result(){} ;
virtual SEXP process( const GroupedDataFrame& gdf) = 0 ;
virtual SEXP process( const FullDataFrame& df ) = 0 ;
virtual SEXP process( const SlicingIndex& index ){
return R_NilValue ;
}
} ;
}
Первые два метода работают со сгруппированными и несгруппированными таблицами данных. Мы в основном сосредоточимся на последнем методе, работающем с SlicingIndex
.SlicingIndex
- это класс, инкапсулирующий индексы отдельного блока сгруппированной таблицы данных.Гибридное вычисление в действительности наследуется от класса
Result
. Давайте рассмотрим более простую версию sum
, которая обрабатывает только числовые векторы. (Внутренняя версия является более полной, обрабатывает пропущенные значения, …).class Sum : public Result {
public:
Sum( NumericVector data_ ): data(data_){}
SEXP process( const SlicingIndex& index ){
double res = 0.0 ;
for( int i=0; i<index.size(); i++) res += data[ index[i] ] ;
return NumericVector::create( res );
}
virtual SEXP process( const GroupedDataFrame& gdf){
...
}
virtual SEXP process( const FullDataFrame& df ){
...
}
private:
NumericVector data ;
} ;
Использования Processor
Реализация классов, наследуемых от классаResult
, может быть упрощена с помощью шаблона класса Processor
. Processor
является шаблоном с двумя параметрами: типом результатов R (REALSXP
, STRSXP
, …) и классом, который вы опеределяете. (Используется паттерн CRTP).При использовании
Processor
мы лишь должны обеспечить метод process_chunk
, который принимает const SlicingIndex&
и возвращает объект, подходящий для превращения в вектор, тип которого задается первым параметром шаблона.Цель шаблона
Processor
состоит в генерировании повторно используемого кода для трех методов process
, определенных в интерфейсе Result
.Возможная реализация
Sum
может выглядеть примерно так:lass Sum : public Processor<REALSXP, Sum> {
public:
Sum( NumericVector data_ ): data(data_){}
double process_chunk( const SlicingIndex& index ){
double res = 0.0 ;
for( int i=0; i<index.size(); i++) res += data[ index[i] ] ;
return res;
}
private:
NumericVector data ;
}
Учитывая данную типичность, мы можем захотеть создать шаблон класса Sum
, чтобы обрабатывать не только числовые векторы:template <int RTYPE>
class Sum : public Processor<REALSXP, Sum<RTYPE> > {
public:
typedef typename Rcpp::traits::storage_type<RTYPE>::type STORAGE ;
Sum( Vector<RTYPE> data_ ): data(data_){}
STORAGE process_chunk( const SlicingIndex& index ){
STORAGE res = 0.0 ;
for( int i=0; i<index.size(); i++) res += data[ index[i] ] ;
return res;
}
private:
Vector<RTYPE> data ;
}
Кроме отсутствия обработки пропущенных значений и использования внутреннего знания класса SlicingIndex
, эта реализация Sum
близка к внутренней реализации в dplyr.Получение гибридных обработчиков
Функции dplyr используют функциюget_handler
для получения обработчиков конкретных выражений.Result* get_handler( SEXP call, const LazySubsets& subsets ){
int depth = Rf_length(call) ;
HybridHandlerMap& handlers = get_handlers() ;
SEXP fun_symbol = CAR(call) ;
if( TYPEOF(fun_symbol) != SYMSXP ) return 0 ;
HybridHandlerMap::const_iterator it = handlers.find( fun_symbol ) ;
if( it == handlers.end() ) return 0 ;
return it->second( call, subsets, depth - 1 );
}
get_handler
выполняет поиск типа HybridHandlerMap
в хеш-таблице.typedef dplyr::Result* (*HybridHandler)(SEXP, const dplyr::LazySubsets&, int) ;
typedef dplyr_hash_map<SEXP,HybridHandler> HybridHandlerMap ;
HybridHandlerMap
является просто хеш-картой, где символ функции и тип значения карты является указателем функции, определяемым HybridHandler
.Параметры указателя фунции
HybridHandler
: вызов, который мы хотим гибридизировать, т.е. что-то типа sum(flights)
; ссылка LazySubsets
(единственное, что имеет отношение к этому классу, это то, что он определяет метод get_variable
, который принимает символ Sexp
и возвращает соответствующую переменную из таблицы данных); количество аргументов в вызове. Например, для For sum(flights)
количество аргументов равно 1
.Цель функции гибридного обработчика является возвращение
Result*
, если он может обрабатывать вызов, или 0, если не может.С нашим предыдущим шаблоном класса
Sum
мы можем оределить функцию гибридного обработчика так:Result* sum_handler(SEXP call, const LazySubsets& subsets, int nargs ){
// we only know how to deal with argument
if( nargs != 1 ) return 0 ;
// get the first argument
SEXP arg = CADR(call) ;
// if this is a symbol, extract the variable from the subsets
if( TYPEOF(arg) == SYMSXP ) arg = subsets.get_variable(arg) ;
// we know how to handle integer vectors and numeric vectors
switch( TYPEOF(arg) ){
case INTSXP: return new Sum<INTSXP>(arg) ;
case REALSXP: return new Sum<REALSXP>(arg) ;
default: break ;
}
// we are here if we could not handle the call
return 0 ;
}
Регистрация гибридных обработчиков
dplyr позволяет пользователям, преимущественно для пакетов, регистрировать свои собственные гибридные обработчики посредствомregisterHybridHandler
.void registerHybridHandler( const char* , HybridHandler ) ;
Для регистрации обработчика, созданного выше, мы затем просто делаем следующее:registerHybridHandler( "sum", sum_handler ) ;
Объединение всех компонентов
Мы собираемся зарегистрировать обработчик под названиемhitchhiker
, который всегда возвращает ответ на всё, то есть 42
(отсылка к роману “Автостопом по галактике - прим. пер.).Код ниже подходит для запуска через
sourceCpp
.#include <dplyr.h>
// [[Rcpp::depends(dplyr,BH)]]
using namespace dplyr ;
using namespace Rcpp ;
// the class that derives from Result through Processor
class Hitchhiker : public Processor<INTSXP,Hitchhiker>{
public:
// always returns 42, as it is the answer to everything
int process_chunk( const SlicingIndex& ){
return 42 ;
}
} ;
// we actually don`t need the arguments
// we can just let this handler return a new Hitchhiker pointer
Result* hitchhiker_handler( SEXP, const LazySubsets&, int ){
return new Hitchhiker ;
}
// registration of the register, called from R, so exprted through Rcpp::export
// [[Rcpp::export]]
void registerHitchhiker(){
registerHybridHandler( "hitchhiker", hitchhiker_handler );
}
/*** R
require(dplyr)
registerHitchhiker()
n <- 10000
df <- group_by( tbl_df( data.frame(
id = sample( letters[1:4], 1000, replace = TRUE ),
x = rnorm(n)
) ), id )
summarise( df, y = hitchhiker() )
# Source: local data frame [4 x 2]
# Groups:
#
# id y
# 1 a 42
# 2 b 42
# 3 c 42
# 4 d 42
summarise(df, y = mean(x) + hitchhiker())
# Source: local data frame [4 x 2]
# Groups:
#
# id y
# 1 a 42.00988
# 2 b 42.00988
# 3 c 42.01440
# 4 d 41.99160
*/
Регистрация гибридных обработчиков с пакетом
Лучшим местом для регистрации пользовательских обработчиков в пакете является начальная точкаinit
, которую R автоматически вызывает при загрузке пакета.Вместо использования функции
registerHitchhiker
, как показано выше, пакеты обычно регистрируют обработчики подобно этому:#include <Rcpp.h>
#include <dplyr.h>
// R automatically calls this function when the maypack package is loaded.
extern "C" void R_init_mypack( DllInfo* info ){
registerHybridHandler( "hitchhiker", hitchhiker_handler );
}
Для этого ваш пакет должен знать о заголовках Rcpp и dplyr, что требует эту информацию в файле DESCRIPTION
:LinkingTo: Rcpp, dplyr, BH
Makevars
и Makevars.win
аналогичны тем, которые используются для любого пакета с функциями Rcpp
. См. виньетки Rcpp
для подробностей.
Комментариев нет:
Отправить комментарий