tipos sirven relacionales que programacion para operadores los logicos algebraicos r boolean-logic

relacionales - para que sirven los operadores logicos



¿Por qué algunos de los operadores lógicos son tan lentos? (3)

Además, no olvide que en R, los lógicos se almacenan como números enteros en la memoria. Así que tiene sentido no tener una gran mejora en la velocidad.

> a=rep(TRUE,1000) > b=rep(1L,1000) > c=rep(1,1000) > class(a) [1] "logical" > class(b) [1] "integer" > class(c) [1] "numeric" > object.size(a) 4040 bytes > object.size(b) 4040 bytes > object.size(c) 8040 bytes

Al intentar optimizar mi código, encontré que algunas operaciones logical eran más lentas de lo que esperaba en comparación con operaciones similares en integer o numeric .

¡Así que fui a reescribir operadores booleanos básicos ! , & , | , xor como sigue:

my.not <- function(x) as.logical(1L - as.integer(x)) my.and <- function(e1, e2) as.logical(as.integer(e1) * as.integer(e2)) my.or <- function(e1, e2) as.logical(as.integer(e1) + as.integer(e2)) my.xor <- function(e1, e2) as.logical(as.integer(e1) + as.integer(e2) == 1L)

Prueba de que todo funciona como se espera:

a <- sample(c(TRUE, FALSE), 1e6, TRUE) b <- sample(c(TRUE, FALSE), 1e6, TRUE) identical(!a, my.not(a)) # TRUE identical(a & b, my.and(a, b)) # TRUE identical(a | b, my.or(a, b)) # TRUE identical(xor(a, b), my.xor(a, b)) # TRUE

Ahora benchmarking:

library(microbenchmark) microbenchmark(!a, my.not(a), a & b, my.and(a, b), a | b, my.or(a, b), xor(a, b), my.xor(a, b)) # Unit: milliseconds # expr min lq median uq max neval # !a 1.237437 1.459042 1.463259 1.492671 17.28209 100 # my.not(a) 6.018455 6.263176 6.414515 15.291194 70.16313 100 # a & b 32.318530 32.667525 32.769014 32.973878 50.55528 100 # my.and(a, b) 8.010022 8.592776 8.750786 18.145590 78.38736 100 # a | b 32.030545 32.383769 32.506937 32.820720 102.43609 100 # my.or(a, b) 12.089538 12.434793 12.663695 22.046841 32.19095 100 # xor(a, b) 94.892791 95.480200 96.072202 106.104000 164.19937 100 # my.xor(a, b) 13.337110 13.708025 14.048350 24.485478 29.75883 100

Mirando los resultados, el ! El operador es el único que parece hacer un trabajo decente en comparación con el mío. Los otros tres son un poco más lentos. Un poco embarazoso para las funciones Primitive . Incluso espero que los operadores booleanos bien implementados sean mucho más rápidos que las operaciones con enteros (cómo implementé mis propias funciones).

Pregunta: ¿Por qué? ¿Mala implementación? ¿O quizás las funciones primitivas están haciendo algunas cosas buenas (por ejemplo, verificación de errores, casos especiales) que mis funciones no están haciendo?


Aunque me gustan mucho sus métodos y me encanta el aumento de velocidad, lamentablemente pierden su habilidad cuando e1 e2 tienen una estructura más compleja que un vector.

> dim(a) <- c(1e2, 1e4) > dim(b) <- c(1e2, 1e4) > > identical(!a, my.not(a)) [1] FALSE > identical(a & b, my.and(a, b)) [1] FALSE > identical(a | b, my.or(a, b)) [1] FALSE > identical(xor(a, b), my.xor(a, b)) [1] FALSE

ESTRUCTURA / DIMENSIONALIDAD

Las funciones lógicas conservan la estructura y los atributos, lo cual es costoso, pero tiene valor.

T <- TRUE; F <- FALSE A <- matrix(c(T, F, T, F), ncol=2) B <- matrix(c(T, F, F, T), ncol=2) > A & B [,1] [,2] [1,] TRUE FALSE [2,] FALSE FALSE > my.and(A, B) [1] TRUE FALSE FALSE FALSE

Manejo de NA

Además, como se señaló en los comentarios, las NA también deben tenerse en cuenta, es decir, más gastos generales.

a <- c(T, F, NA, T) b <- c(F, NA, T, T) > identical(!a, my.not(a)) [1] TRUE > identical(a & b, my.and(a, b)) [1] FALSE > identical(a | b, my.or(a, b)) [1] FALSE > identical(xor(a, b), my.xor(a, b)) [1] TRUE

ATRIBUTOS

a <- c(T, F, NA, T) b <- c(F, NA, T, T) names(a) <- names(b) <- LETTERS[23:26] > a & b W X Y Z FALSE FALSE NA TRUE > my.and(a, b) [1] FALSE NA NA TRUE

No velocidad

Por supuesto, con todo lo que se dice, ¡sus funciones ofrecen un gran aumento! Si sabes que no tienes que preocuparte por las NA y los me gusta, y no te importa la estructura, ¡entonces por qué no usarlas!


Mirando un poco la implementación de C, las operaciones lógicas y matemáticas implementan sus bucles de manera diferente. Las operaciones lógicas hacen algo como (en logic.c: 327)

library(inline) or1 <- cfunction(c(x="logical", y="logical"), " int nx = LENGTH(x), ny = LENGTH(y), n = nx > ny ? nx : ny; SEXP ans = PROTECT(allocVector(LGLSXP, n)); int x1, y1; for (int i = 0; i < n; i++) { x1 = LOGICAL(x)[i % nx]; y1 = LOGICAL(y)[i % ny]; if ((x1 != NA_LOGICAL && x1) || (y1 != NA_LOGICAL && y1)) LOGICAL(ans)[i] = 1; else if (x1 == 0 && y1 == 0) LOGICAL(ans)[i] = 0; else LOGICAL(ans)[i] = NA_LOGICAL; } UNPROTECT(1); return ans; ")

Donde hay dos operadores de módulo % cada iteración. En contraste, las operaciones aritméticas (en Itermacros.h: 54) hacen algo como

or2 <- cfunction(c(x="logical", y="logical"), " int nx = LENGTH(x), ny = LENGTH(y), n = nx > ny ? nx : ny; SEXP ans = PROTECT(allocVector(LGLSXP, n)); int x1, y1, ix=0, iy=0; for (int i = 0; i < n; i++) { x1 = LOGICAL(x)[ix]; y1 = LOGICAL(x)[iy]; if (x1 == 0 || y1 == 0) LOGICAL(ans)[i] = 0; else if (x1 == NA_LOGICAL || y1 == NA_LOGICAL) LOGICAL(ans)[i] = NA_LOGICAL; else LOGICAL(ans)[i] = 1; if (++ix == nx) ix = 0; if (++iy == ny) iy = 0; } UNPROTECT(1); return ans; ")

Realizando dos pruebas de identidad. Aquí hay una versión que se salta la prueba para NA

or3 <- cfunction(c(x="logical", y="logical"), " int nx = LENGTH(x), ny = LENGTH(y), n = nx > ny ? nx : ny; SEXP ans = PROTECT(allocVector(LGLSXP, n)); int x1, y1, ix=0, iy=0; for (int i = 0; i < n; ++i) { x1 = LOGICAL(x)[ix]; y1 = LOGICAL(y)[iy]; LOGICAL(ans)[i] = (x1 || y1); if (++ix == nx) ix = 0; if (++iy == ny) iy = 0; } UNPROTECT(1); return ans; ")

Y luego una versión que evita la macro LOGICAL.

or4 <- cfunction(c(x="logical", y="logical"), " int nx = LENGTH(x), ny = LENGTH(y), n = nx > ny ? nx : ny; SEXP ans = PROTECT(allocVector(LGLSXP, n)); int *xp = LOGICAL(x), *yp = LOGICAL(y), *ansp = LOGICAL(ans); for (int i = 0, ix = 0, iy = 0; i < n; ++i) { *ansp++ = xp[ix] || yp[iy]; ix = (++ix == nx) ? 0 : ix; iy = (++iy == ny) ? 0 : iy; } UNPROTECT(1); return ans; ")

Aquí hay algunos tiempos

microbenchmark(my.or(a, b), a|b, or1(a, b), or2(a, b), or3(a, b), or4(a, b)) Unit: milliseconds expr min lq median uq max neval my.or(a, b) 8.002435 8.100143 10.082254 11.56076 12.05393 100 a | b 23.194829 23.404483 23.860382 24.30020 24.96712 100 or1(a, b) 17.323696 17.659705 18.069139 18.42815 19.57483 100 or2(a, b) 13.040063 13.197042 13.692152 14.09390 14.59378 100 or3(a, b) 9.982705 10.037387 10.578464 10.96945 11.48969 100 or4(a, b) 5.544096 5.592754 6.106694 6.30091 6.94995 100

La diferencia entre a|b y or1 refleja cosas no implementadas aquí, como atributos y dimensiones y manejo especial de objetos. De or1 a or2 refleja el costo de las diferentes formas de reciclaje; Me sorprendió que hubiera diferencias aquí. De or2 a or3 es el costo de NA-safety. Es un poco difícil saber si la aceleración adicional en or4 se vería en una implementación base R: en el código C del usuario C LOGICAL() es una macro, pero en la base R es una llamada de función integrada.

El código fue compilado con banderas -O2 y

> system("clang++ --version") Ubuntu clang version 3.0-6ubuntu3 (tags/RELEASE_30/final) (based on LLVM 3.0) Target: x86_64-pc-linux-gnu Thread model: posix

Los tiempos de my.or no fueron particularmente consistentes entre las sesiones de R independientes, a veces tomando un poco más de tiempo; No estoy seguro de por qué. Los tiempos anteriores fueron con la versión R 2.15.3 Patched (2013-03-13 r62579); El actual R-devel parecía un 10% más rápido.