Creación de una biblioteca de enlace dinámico con R
Creación de una biblioteca de enlace dinámico con R
Sin necesidad de desarrollar un paquete completo, construyamos una biblioteca de enlace dinámico con R implementando una alternativa a la función expand.grid
.
El problema de las permutaciones con repetición
La función expand.grid
crea una tabla de datos (data.frame) con todas las combinaciones posibles de los elementos de una lista. Para una tarea necesitamos generar una retícula grid en formato matriz con unas determinadas características: debe obtener todas las permutaciones con repetición de un conjunto x
de números enteros o reales (distintos o no), donde cada elemento se repite hasta n
veces. Además requiere incluir unas columnas adicionales para almacenar posteriormente información sobre cada permutación.
El siguiente ejemplo detalla la forma de proceder con expand.grid
. Calcula las permutaciones con repetición de dos elementos tomados de tres en tres, obteniendo ocho permutaciones, y añade otras cuatro columnas.
x <- c(1:2) n <- 3L ntotal <- 7L listainicial <- vector("list", ntotal) for(i in 1:ntotal){ if(i <= n) { listainicial[[i]] <- x } else { listainicial[[i]] <- NA } } mpointsR <-as.matrix(expand.grid(listainicial, KEEP.OUT.ATTRS = FALSE)) dimnames(mpointsR) <- NULL print(mpointsR)
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [1,] 1 1 1 NA NA NA NA [2,] 2 1 1 NA NA NA NA [3,] 1 2 1 NA NA NA NA [4,] 2 2 1 NA NA NA NA [5,] 1 1 2 NA NA NA NA [6,] 2 1 2 NA NA NA NA [7,] 1 2 2 NA NA NA NA [8,] 2 2 2 NA NA NA NA
Los parámetros x
, n
y ntotal
varían y cuando alcanzan determinado tamaño, la función expand.grid
ralentiza considerablemente todo el proceso de computación.
Compilación de una biblioteca de enlace dinámico
La solución propuesta para superar esta limitación consiste en crear una versión especializada de expand.grid
, sin necesidad de considerar todas las casuísticas que incluye esta función. Primero se hizo una versión simplificada en lenguaje R, y como aún así el tiempo de ejecución también sobrepasaba lo razonable, se optó por diseñar una función en C/C++.
Para ello descargamos el siguiente material:
- El fichero
paquete.h
, que incluye las cabeceras usuales. - El fichero
expandgrid.cpp
, que contiene una versión especializada de la funciónexpand.grid
. - El fichero
test.r
, que compara los resultados de ambos procedimientos. - El fichero
Makefile
automatiza el proceso: crea una biblioteca de enlace dinámico y ejecuta el ficherotest.r
cuya salida se recoge en eltest.r.Rout
. El ficheroMakefile
debe borrarse si se incluye esta carpeta en elsrc
de un paquete. En ese caso se utiliza el ficheroMakevars
.
Descomprimimos el material, y en ese directorio ejecutamos la instrucción para crear una biblioteca dinámica.
$ R CMD SHLIB expandgrid.cpp -o paquete.so
Si todo funciona correctamente se crea una biblioteca llamada paquete.so
en Linux (en Windows utilice el nombre paquete.dll
).
El fichero test.r
carga la biblioteca, ejecuta la función expand_grid_cpp
definida en paquete.so
y comprueba que los resultados son iguales con ambos procedimientos.
## Load library. But first, we have to unload it if it is loaded. ## Windows: paquete.dll ## Unix: paquete.so sharedlibrary <- paste0("paquete",.Platform$dynlib.ext) tryCatch({ dyn.unload(file.path(".",sharedlibrary)); }, warning = function(w) { cat("Library is not loaded.\n") }, error = function(e) { cat("Library is not loaded.\n") }) dyn.load(file.path(".",sharedlibrary), local = TRUE, now = TRUE) mpointsCpp <- .Call("expand_grid_cpp",x, n, ntotal) print(mpointsCpp) class(mpointsCpp[1]) all.equal(mpointsR,mpointsCpp) # TRUE
Rendimiento
Si observamos el tiempo de ejecución, la función expand.grid
tarda veinte veces más que la diseñada ad hoc.
library(microbenchmark) microbenchmark( cpp=.Call("expand_grid_cpp",x, n, ntotal), R=expand.grid(listainicial, KEEP.OUT.ATTRS = FALSE) )
Unit: nanoseconds expr min lq mean median uq max neval cpp 735 831.0 1446.15 917.0 1918.5 11941 100 R 19065 19577.5 20709.61 20016.5 20706.5 60480 100
Lea el manual de tortura
Si ha llegado hasta aquí, debe avanzar un poco más y leer el manual Writing R Extensions, en particular la sección de problemas de memoria. Dado que supone una tortura, aquí puede encontrar una forma de proceder con valgrind, que hemos de aplicar siempre a nuestras bibliotecas.
Coda
Este ejemplo detalla cómo crear una biblioteca de enlace dinámico y cómo se utiliza en un programa de R sin necesidad de disponer de un paquete completo. Sirve para desarrollar poco a poco las funciones necesarias.
Referencias
Código en C/C++ de la versión alternativa a expand.grid
Aquí se presenta el código del fichero expandgrid.cpp y el de la cabecera paquete.h
.
El código de expandgrid.cpp consta de estas instrucciones:
#include "paquete.h" /* Create a grid of length(x)^n, with the values of x and add (ntotal - n) columns. Return a matrix with dimensions (length(x)^n , ntotal) Particular case of expand.grid x <- 1:3 n <- 4L ntotal <- 7L mpoints <- .Call("expand_grid_cpp",x, n, ntotal) listainicial <- vector("list", ntotal) for(i in 1:ntotal){ if(i <= n) { listainicial[[i]] <- x } else { listainicial[[i]] <- NA } } mpointsR <-as.matrix(expand.grid(listainicial, KEEP.OUT.ATTRS = FALSE)) dimnames(mpointsR) <- NULL all.equal(mpointsR,mpoints) # TRUE head(mpoints) [,1] [,2] [,3] [,4] [,5] [,6] [,7] [1,] 1 1 1 1 NA NA NA [2,] 2 1 1 1 NA NA NA [3,] 3 1 1 1 NA NA NA [4,] 1 2 1 1 NA NA NA [5,] 2 2 1 1 NA NA NA [6,] 3 2 1 1 NA NA NA ... */ extern "C" SEXP expand_grid_cpp(SEXP x, SEXP n, SEXP ntotal){ // X is a vector of integer or real numbers. // N and NTOTAL are scalar integers such that 0< N <= NTOTAL. // Return a matrix of the permutations with n repetitions of x, // and add (ntotal -n ) NA columns. if (!Rf_isVectorAtomic(x) || !LENGTH(x)) Rf_error("X must be an atomic vector with length greater than 0 (%d).", LENGTH(x)); if(!Rf_isInteger(n) || LENGTH(n) != 1) Rf_error("N is either not an integer value ('%s') or its length is not 1 (%d).", Rf_type2char(TYPEOF(n)), LENGTH(n)); if(!Rf_isInteger(ntotal) || LENGTH(ntotal) != 1) Rf_error("NTOTAL is either not an integer value ('%s') or its length is not 1 (%d).", Rf_type2char(TYPEOF(ntotal)), LENGTH(n)); const R_len_t nrepetitions = INTEGER(n)[0]; const R_len_t ncolumns = INTEGER(ntotal)[0]; if( nrepetitions < 1 || ncolumns < nrepetitions || nrepetitions == NA_INTEGER || ncolumns == NA_INTEGER ) Rf_error("N is '%d', lesser than 1 or greater than the number of columns NCOL (%d).", nrepetitions,ncolumns); // Number of combinations. It can become a very, very, very big magnitude. const R_len_t lx=LENGTH(x); const R_len_t nrows = pow(lx, nrepetitions); // BE CAREFUL! ISNAN(nrows) does not work here! if(nrows < 0 ) Rf_error("The number of combinations (%.0f) is too big! ('nrows'). Check it within R: Can you create a 'matrix(NA, nrow= %d^%d, ncol= %d)'?",pow(lx,nrepetitions), lx, nrepetitions, ncolumns); // We create the matrix. If it is too big, it gives the error 'Not enough memory for your dreams'. int protecti=0; SEXP result = PROTECT(Rf_allocMatrix(TYPEOF(x), nrows , ncolumns)); protecti++; // vcount is the count/value for each column. // vcount[jcol] = 0,1,2,..., lx-1 R_len_t *vcount = (R_len_t*) (R_alloc(nrepetitions,sizeof(R_len_t))); for(R_len_t i = 0; i < nrepetitions ; i++) vcount[i] = 0; const R_len_t maxcount = lx -1; // The maximum count that can appear in a column const R_len_t lastcolumn = nrepetitions -1; // Last column of the repetition submatrix. const R_len_t firstcolumn = 0; // First column. switch(TYPEOF(x)) { case LGLSXP: case INTSXP: { const int* px = INTEGER(x); int* presult = INTEGER(result); // for each row for(R_len_t irow = 0; irow < nrows ; irow++){ // for the columns to fill with values of x for(R_len_t jcol = 0; jcol < nrepetitions ; jcol++){ // When we are at the first column, // we decide that a count must vary // when the first column gets an excessive count. if(vcount[firstcolumn] > maxcount && jcol == firstcolumn ) { // Numk is the first position/column which its count/value can be increased. // Therefore, the previous columns have the maximum count/value. // So we reset the count/values for the previous columns // and increase the count/values of the position Numk. R_len_t numk = 0; for( numk =0; numk < nrepetitions && vcount[numk] >= maxcount; numk++); for(R_len_t k =0; k < numk; k++){ vcount[k]=0; } // If Numk is greater than the last column, we have finished the grid: // All the columns have the maximum count/value. // Otherwise, increase the value of the column numk if(numk < nrepetitions) vcount[numk]++; // numk <= lastcolumn } // Fill this position with the value of X that corresponds to // the count of this column. presult[ IDX(irow,jcol,nrows) ] = px[ vcount[jcol] ] ; // When it is the last column, the count of the first column always increases. // It is the signal to reset some columns. if(jcol == lastcolumn) vcount[firstcolumn]++; } // End of columns of permutations // Fill the others columns with NA for(R_len_t jcol = nrepetitions; jcol < ncolumns ; jcol++){ presult[ IDX(irow,jcol,nrows) ] = NA_INTEGER; } } break; } case REALSXP: { const double* px = REAL(x); double* presult = REAL(result); for(R_len_t irow = 0; irow < nrows ; irow++){ for(R_len_t jcol = 0; jcol < nrepetitions ; jcol++){ if(vcount[firstcolumn] > maxcount && jcol == firstcolumn ) { int numk = 0; for( numk =0; numk < nrepetitions && vcount[numk] >= maxcount; numk++); for(R_len_t k =0; k < numk; k++){ vcount[k]=0; } if(numk < nrepetitions){ vcount[numk ]++; } } presult[ IDX(irow,jcol,nrows) ] = px[ vcount[jcol] ] ; if(jcol == lastcolumn){ vcount[firstcolumn]++; } } for(R_len_t jcol = nrepetitions; jcol < ncolumns ; jcol++){ presult[ IDX(irow,jcol,nrows) ] = NA_REAL; } } break; } default: UNPROTECT(protecti); Rf_error("Unsupported type for X ('%s')",Rf_type2char(TYPEOF(x))); return R_NilValue; } UNPROTECT(protecti); return result; } // ============================================================ // ============================================================
El código de la cabecera paquete.h
consta de estas instrucciones:
#ifndef PAQUETE_H #define PAQUETE_H #include <R.h> #define R_NO_REMAP #include <Rinternals.h> #include <Rdefines.h> #ifdef _OPENMP #include <omp.h> #include <pthread.h> #endif // ============================================================ // Matrix[i,j] = Matrix[ i + j*dim(0) ] i=0,1,2,..,dim0-1; j=0,1,2,3,.. #define IDX(i, j, dim0) ( (i) + (j) * (dim0) ) // ============================================================ // expandgrid.cpp extern "C" SEXP expand_grid_cpp(SEXP x, SEXP n, SEXP ntotal); // ============================================================ #endif