Algunos toques para trabajar con ID de bigint en R

Cada vez que comienza una conversación sobre el uso de varias bases de datos como fuente de datos, aparece el tema de identificadores de registro, objetos u otra cosa. En ocasiones, los participantes pueden considerar la coordinación del protocolo de intercambio durante varios meses. int - bigint - guid , luego en un círculo. Para las tareas de volumen, teniendo en cuenta que, de forma nativa, en R no hay soporte para bigint (capacidad ~ 2 ^ 64), la elección de la presentación correcta de dichos identificadores puede ser crítica en términos de rendimiento. ¿Existe una solución obvia y universal? A continuación se presentan algunas consideraciones prácticas que pueden usarse en proyectos como prueba de fuego.


Como regla, los identificadores se utilizarán para tres clases de tareas:


  • agrupación
  • filtrado
  • asociación.

En base a esto, evaluaremos varios enfoques.


Es una continuación de publicaciones anteriores .


Almacenar como string


La opción para datos pequeños es bastante buena. Aquí y la capacidad de recoger cualquier longitud del identificador, y la capacidad de admitir no solo identificadores numéricos, sino también alfanuméricos. Una ventaja adicional es la capacidad garantizada de recibir datos correctamente a través de cualquier protocolo de base de datos no nativo, por ejemplo, a través de la API REST de la puerta de enlace.


Los contras también son obvios. Alto consumo de memoria, mayor volumen de información de la base de datos, degradación del rendimiento tanto a nivel de red como a nivel computacional.


Usamos el paquete bit64


Muchos que han escuchado solo el nombre de este paquete pueden pensar que aquí está, la solución perfecta. Por desgracia, esto no es del todo cierto. No solo se trata de un complemento en la parte superior del valor numeric (cita: ' Una vez más, la elección es obvia: R solo tiene un tipo de datos de 64 bits: dobles. Al usar dobles,
integer64 hereda algunas funcionalidades como is.atomic, length, length <-, names, names <-, dim, dim <-, dimnames, dimnames. ' ), por lo que todavía hay una expansión masiva de la aritmética básica y no hay garantías de que no explotará en ningún lado y no habrá conflicto con otros paquetes.


Usamos tipo numeric


Este es un truco completamente correcto, que es un buen compromiso para aquellos que saben exactamente qué se int64 en la respuesta int64 de la base de datos. Después de todo, no todos los 64 bits realmente estarán involucrados allí. A menudo puede haber un número mucho menor que 2 ^ 64.


Tal solución es posible debido a los detalles del formato de coma flotante de doble precisión. Los detalles se pueden encontrar en el popular artículo de formato de punto flotante de precisión doble .


 The 53-bit significand precision gives from 15 to 17 significant decimal digits precision (2−53 ≈ 1.11 × 10−16). If a decimal string with at most 15 significant digits is converted to IEEE 754 double-precision representation, and then converted back to a decimal string with the same number of digits, the final result should match the original string. If an IEEE 754 double-precision number is converted to a decimal string with at least 17 significant digits, and then converted back to double-precision representation, the final result must match the original number. 

Si tiene 15 dígitos decimales o menos en el identificador, puede usar números y no preocuparse.


El mismo truco es bueno cuando necesita trabajar con datos temporales, especialmente aquellos que contienen milisegundos. La transferencia de datos temporales a través de la red en forma de texto lleva tiempo, además, en el lado de recepción, debe ejecutar el analizador de texto -> POSIXct , que también requiere una gran cantidad de recursos (reducción del rendimiento a veces). La transferencia en forma binaria no es un hecho que todos los controladores admitirán la transferencia de la zona horaria y milisegundos. Pero la transmisión de tiempo exacto a milisegundos en la zona UTC en la representación de marca de tiempo de Unix (13 lugares decimales) está muy bien y sin pérdidas por el formato numeric .


No tan simple y obvio


Si observamos más de cerca la versión con líneas, la obviedad y la categoridad de la declaración inicial retroceden un poco. Trabajar con cadenas en R no es del todo sencillo, incluso omitiendo los matices de alinear bloques de memoria y captar previamente. A juzgar por los libros y la documentación detallada, las variables de cadena no se almacenan por sí mismas en una variable, sino que se colocan en un grupo de cadenas global. Todas las lineas. Y este conjunto es utilizado por los arreglos de cadenas para reducir el consumo de memoria. Es decir un vector de texto será un conjunto de líneas en el grupo global + un vector de enlaces a registros de este grupo.


 library(tidyverse) library(magrittr) library(stringi) library(gmp) library(profvis) library(pryr) library(rTRNG) set.seed(46572) RcppParallel::setThreadOptions(numThreads = parallel::detectCores() - 1) #              options(scipen = 10000) options(digits = 14) options(pillar.sigfig = 14) pryr::mem_used() fname <- here::here("output", "dump.csv") #  10^4,       (    +  ) m1 <- sample(stri_rand_strings(100, 64, "[a0-9]"), 10^7, replace = TRUE) #     readr::write_csv(enframe(m1, name = NULL), fname) #       m2 <- readr::read_csv(fname, col_types = "c") %>% pull(value) pryr::object_size(m2) pryr::mem_used() #      print(glue::glue("File size: {fs::file_size(fname)}. ", "Constructed from file object's (m2) size: {fs::fs_bytes(pryr::object_size(m2))}. ", "Pure pointer's size: {fs::fs_bytes(8*length(m2))}")) .Internal(inspect(m1)) .Internal(inspect(m2)) 

Vemos que incluso sin ir al nivel de C ++, la hipótesis no está tan lejos de la verdad. El volumen del vector de cadena casi coincide con el volumen de los punteros de 64 bits, y la variable en sí ocupa mucho menos espacio que el archivo en el disco.


 File size: 62M. Constructed from file object's (m2) size: 7.65M. Pure pointer's size: 7.63M 

Y el contenido de los vectores antes de escribir y después de leer es idéntico - resp. Los elementos vectoriales se refieren a los mismos bloques de memoria.


Por lo tanto, una mirada más cercana al uso de cadenas de texto como identificadores ya no parece una idea tan loca. Los puntos de referencia para agrupar, filtrar y fusionar, usando dplyr y data.table dan lecturas aproximadamente similares para identificadores numeric y de character , lo que proporciona una confirmación adicional de la optimización debido al conjunto global. Después de todo, se está trabajando con punteros cuyo tamaño es de 32 o 64 bits, dependiendo del ensamblaje R (32/64), y este es precisamente el tipo numeric .


 #    gc() pryr::mem_used() bench::mark( string = id_df %>% group_by(id_string) %>% summarise(j = prod(j, na.rm = TRUE)), # bit64 = id_df %>% group_by(id_bit64) %>% summarise(j = prod(j, na.rm = TRUE)), numeric = id_df %>% group_by(id_numeric) %>% summarise(j = prod(j, na.rm = TRUE)), # gmp = id_df %>% group_by(id_gmp) %>% summarise(j = prod(j, na.rm = TRUE)), check = FALSE ) #    gc() pryr::mem_used() string_subset <- sample(unique(id_df$id_string), 20) numeric_subset <- sample(unique(id_df$id_numeric), 20) bench::mark( string = dplyr::filter(id_df, id_string %in% string_subset), numeric = dplyr::filter(id_df, id_numeric %in% numeric_subset), check = FALSE ) #    gc() pryr::mem_used() #        string_copy_df <- rlang::duplicate(dplyr::count(id_df, id_string)) numeric_copy_df <- rlang::duplicate(dplyr::count(id_df, id_numeric)) bench::mark( string = id_df %>% dplyr::left_join(string_copy_df, by = "id_string"), numeric = id_df %>% dplyr::left_join(numeric_copy_df, by = "id_numeric"), iterations = 10, check = FALSE ) 

Por cierto, el tamaño máximo de la memoria R disponible se puede ver con el fs::fs_bytes(memory.limit()) .


Para ser sincero, debe tenerse en cuenta que dplyr no siempre tuvo una dplyr rápida de dplyr , vea el caso "Unirse por una columna de caracteres es lento, en comparación con unirse por una columna de factor. # 1386 {Cerrado}" . Este hilo propone utilizar las capacidades del conjunto global de cadenas y comparar no cadenas como tales, sino punteros a cadenas.


Detalles de gestión de memoria


Fuentes basicas



Conclusión


Naturalmente, esta pregunta se hace constantemente de una forma u otra, una serie de enlaces a continuación.



Pero para comprender conscientemente qué hacer bien, cuáles son las oportunidades y limitaciones, es mejor bajar al nivel más bajo posible. Como resultado, hay una masa de detalles no obvios. La publicación es de naturaleza investigadora, ya que afecta aspectos bastante básicos de R, que pocas personas usan en su trabajo diario. Si hay adiciones, comentarios o correcciones importantes, será muy interesante conocerlos.


La publicación anterior es "Uso de R para tareas de utilidad" .

Source: https://habr.com/ru/post/466829/


All Articles