Quelques touches pour travailler avec des identifiants bigint dans R

Chaque fois qu'une conversation commence sur l'utilisation de diverses bases de données comme source de données, le sujet des identificateurs d'enregistrement, des objets ou de quelque chose d'autre apparaît. Parfois, la coordination du protocole d'échange peut être envisagée par les participants pendant plusieurs mois. int - bigint - guid , puis en cercle. Pour les tâches de volume, bigint que nativement dans R il n'y a pas de support bigint (capacité ~ 2 ^ 64), le choix de la présentation correcte de ces identifiants peut être critique en termes de performances. Existe-t-il une solution de contournement évidente et universelle? Voici quelques considérations pratiques qui peuvent être utilisées dans les projets comme test décisif.


En règle générale, les identifiants seront utilisés pour trois classes de tâches:


  • regroupement;
  • filtrage
  • association.

Sur cette base, nous évaluerons différentes approches.


Il s'agit d'une continuation des publications précédentes .


Stocker sous forme de string


L'option pour les petites données est plutôt bonne. Ici et la possibilité de récupérer n'importe quelle longueur de l'identifiant, et la possibilité de prendre en charge non seulement les identificateurs numériques, mais aussi alphanumériques. Un avantage supplémentaire est la capacité garantie de recevoir correctement les données via n'importe quel protocole de base de données non natif, par exemple, via l'API REST de la passerelle.


Les inconvénients sont également évidents. Grande consommation de mémoire, augmentation du volume d'informations de la base de données, dégradation des performances à la fois au niveau du réseau et au niveau du calcul.


Nous utilisons le package bit64


Beaucoup de ceux qui n'ont entendu que le nom de ce package peuvent penser que le voici, la solution parfaite. Hélas, ce n'est pas tout à fait vrai. Non seulement est-ce un add-on en plus du numeric (citation: ' Encore une fois, le choix est évident: R n'a qu'un seul type de données 64 bits: doubles. En utilisant des doubles,
integer64 hérite de certaines fonctionnalités telles que is.atomic, length, length <-, names, names <-, dim, dim <-, dimnames, dimnames. ' ), il y a donc toujours une expansion massive de l'arithmétique de base et il n'y a aucune garantie qu'elle n'explosera nulle part et il n'y aura pas de conflit avec d'autres packages.


Nous utilisons le type numeric


C'est une astuce complètement correcte, qui est un bon compromis pour ceux qui savent exactement ce qui sera caché dans la réponse int64 de la base de données. Après tout, tous les 64 bits n'y seront pas vraiment impliqués. Souvent, il peut y en avoir beaucoup moins que 2 ^ 64.


Une telle solution est possible en raison des spécificités du format à virgule flottante double précision. Les détails peuvent être trouvés dans l'article populaire en format à virgule flottante double précision .


 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 vous avez 15 chiffres décimaux ou moins dans l'identifiant, vous pouvez utiliser des numeric et ne vous inquiétez pas.


La même astuce est bonne lorsque vous devez travailler avec des données temporaires, en particulier celles contenant des millisecondes. Le transfert de données temporaires sur le réseau sous forme de texte prend du POSIXct outre, du côté de la réception, vous devez exécuter l'analyseur de texte -> POSIXct , qui est également extrêmement gourmand en ressources (baisse des performances parfois). Le transfert sous forme binaire n'est pas un fait que tous les pilotes prendront en charge le transfert du fuseau horaire et des millisecondes. Mais la transmission du temps précis en millisecondes dans la zone UTC dans la représentation d'horodatage unix (13 décimales) est très bien et sans perte fournie par le format numeric .


Pas si simple et évident


Si nous regardons de plus près la version avec des lignes, l'évidence et la catégorisation de l'énoncé initial s'éloignent un peu. Travailler avec des chaînes dans R n'est pas très simple, même en omettant les nuances d'alignement des blocs de mémoire et de prélecture. À en juger par les livres et la documentation approfondie, les variables de chaîne ne sont pas stockées par elles-mêmes dans une variable, mais sont placées dans un pool de chaînes global. Toutes les lignes. Et ce pool est utilisé par les tableaux de chaînes pour réduire la consommation de mémoire. C'est-à-dire un vecteur texte sera un ensemble de lignes dans le pool global + un vecteur de liens vers les enregistrements de ce pool.


 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)) 

On voit que même sans aller au niveau C ++, l'hypothèse n'est pas si loin de la vérité. Le volume du vecteur chaîne coïncide presque avec le volume des pointeurs 64 bits, et la variable elle-même prend beaucoup moins d'espace que le fichier sur le disque.


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

Et le contenu des vecteurs avant l'écriture et après la lecture est identique - resp. les éléments vectoriels font référence aux mêmes blocs de mémoire.


Donc, regarder de plus près l'utilisation des chaînes de texte comme identificateurs ne semble plus une idée si folle. Les repères de regroupement, de filtrage et de fusion, à l'aide de dplyr et de data.table donnent des lectures approximativement similaires pour numeric identificateurs numeric et de character , ce qui donne une confirmation supplémentaire de l'optimisation en raison du pool global. Après tout, le travail est en cours avec des pointeurs dont la taille est de 32 ou 64 bits, selon l'assemblage R (32/64), et c'est précisément le type 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 ) 

Soit dit en passant, la taille maximale de la mémoire R disponible peut être consultée avec la commande fs::fs_bytes(memory.limit()) .


Pour être honnête, il convient de noter que dplyr n'a pas toujours eu une dplyr rapide des dplyr , voir le cas "La jonction par une colonne de caractères est lente, par rapport à la jonction par une colonne de facteurs. # 1386 {Closed}" . Ce thread propose d'utiliser les capacités du pool global de chaînes et de comparer non pas des chaînes en tant que telles, mais des pointeurs vers des chaînes.


Détails de la gestion de la mémoire


Sources de base



Conclusion


Naturellement, cette question est constamment posée sous une forme ou une autre, un certain nombre de liens ci-dessous.



Mais afin de comprendre consciemment ce qu'il faut faire correctement, quelles sont les opportunités et les limites, il est préférable de descendre au niveau le plus bas possible. Il s'avère qu'il y a une masse de spécificités pas évidentes. La publication est de nature recherche, car elle affecte des aspects assez basiques de la R, que peu de gens utilisent dans leur travail quotidien. S'il y a des ajouts, commentaires ou corrections significatifs, il sera très intéressant de les connaître.


La publication précédente est «Utilisation de R pour les tâches utilitaires» .

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


All Articles