Sempre que uma conversa começa sobre o uso de vários bancos de dados como fonte de dados, o tópico identificadores de registro, objetos ou outra coisa aparece. Às vezes, a negociação de um protocolo de troca pode ser considerada pelos participantes por vários meses. int
- bigint
- guid
, então em um círculo. Para tarefas de volume, levando em conta que originalmente em R não há suporte grande (capacidade ~ 2 ^ 64), a escolha da apresentação correta desses identificadores pode ser crítica em termos de desempenho. Existe uma solução óbvia e universal? Abaixo estão algumas considerações práticas que podem ser usadas em projetos como um teste decisivo.
Como regra, os identificadores serão usados para três classes de tarefas:
- agrupamento;
- filtragem
- associação.
Com base nisso, avaliaremos várias abordagens.
É uma continuação de publicações anteriores .
Armazenar como string
A opção para pequenos dados é muito boa. Aqui, a capacidade de escolher qualquer comprimento do identificador e a capacidade de suportar não apenas identificadores numéricos, mas também alfanuméricos. Uma vantagem adicional é a capacidade garantida de receber dados corretamente através de qualquer protocolo de banco de dados não nativo, por exemplo, através da API REST do gateway.
Contras também são óbvias. Alto consumo de memória, aumento do volume de informações do banco de dados, degradação do desempenho no nível da rede e no nível computacional.
Nós usamos o pacote bit64
Muitos que ouviram apenas o nome deste pacote podem pensar que aqui está, a solução perfeita. Infelizmente, isso não é inteiramente verdade. Esse não é apenas um complemento sobre o código numeric
(citação: ' Novamente, a escolha é óbvia: R possui apenas um tipo de dados de 64 bits: duplos.
integer64 herda algumas funcionalidades, como is.atomic, length, length <-, nomes, nomes <-, dim, dim <-, dimnames, dimnames. ' ), ainda existe uma expansão maciça da aritmética básica e não há garantias de que ela não explodirá em lugar algum e que não haverá conflito com outros pacotes.
Nós usamos o tipo numeric
Esse é um truque completamente correto, que é um bom compromisso para quem sabe exatamente o que será oculto na resposta int64
do banco de dados. Afinal, nem todos os 64 bits estarão realmente envolvidos lá. Muitas vezes, pode haver um número muito menor que 2 ^ 64.
Essa solução é possível devido às especificidades do formato de ponto flutuante de precisão dupla. Detalhes podem ser encontrados no popular artigo no formato de ponto flutuante de precisão dupla .
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.
Se você tiver 15 ou menos dígitos decimais no identificador, poderá usar numeric
e não se preocupe.
O mesmo truque é bom quando você precisa trabalhar com dados temporários, especialmente aqueles que contêm milissegundos. A transferência de dados temporários pela rede em forma de texto leva tempo; além disso, no lado do recebimento, é necessário executar o analisador de texto -> POSIXct
, que também é extremamente intensivo em recursos (redução de desempenho às vezes). A transferência em formato binário não é um fato que todos os drivers suportam a transferência do fuso horário e dos milissegundos. Mas a transmissão do tempo exato em milissegundos na zona UTC na representação de carimbo de data / hora unix (13 casas decimais) é muito bem e sem perdas fornecida pelo formato numeric
.
Não é tão simples e óbvio
Se olharmos mais de perto a versão com linhas, a obviedade e a categoricidade da afirmação inicial diminuem um pouco. Trabalhar com seqüências de caracteres em R não é muito simples, mesmo omitindo as nuances do alinhamento de blocos de memória e pré-busca. A julgar pelos livros e pela documentação detalhada, as variáveis de sequência não são armazenadas sozinhas em uma variável, mas são colocadas em um conjunto global de sequências. Todas as linhas E esse pool é usado por matrizes de string para reduzir o consumo de memória. I.e. um vetor de texto será um conjunto de linhas no pool global + um vetor de links para registros desse pool.
library(tidyverse) library(magrittr) library(stringi) library(gmp) library(profvis) library(pryr) library(rTRNG) set.seed(46572) RcppParallel::setThreadOptions(numThreads = parallel::detectCores() - 1)
Vemos que, mesmo sem ir para o nível C ++, a hipótese não está tão longe da verdade. O volume do vetor string coincide quase com o volume dos ponteiros de 64 bits, e a própria variável ocupa significativamente menos espaço que o arquivo no disco.
File size: 62M. Constructed from file object's (m2) size: 7.65M. Pure pointer's size: 7.63M
E o conteúdo dos vetores antes de escrever e depois da leitura é idêntico - resp. elementos do vetor referem-se aos mesmos blocos de memória.
Portanto, uma análise mais detalhada do uso de cadeias de texto como identificadores não parece mais uma idéia tão louca. Os benchmarks para agrupar, filtrar e mesclar, usando dplyr
e data.table
fornecem leituras aproximadamente semelhantes para identificadores numeric
e de character
, o que fornece confirmação adicional de otimização devido ao pool global. Afinal, o trabalho está em andamento com ponteiros cujo tamanho é de 32 ou 64 bits, dependendo da montagem R (32/64), e esse é precisamente o tipo numeric
.
A propósito, o tamanho máximo da memória R disponível pode ser visualizado com o fs::fs_bytes(memory.limit())
.
Para ser sincero, deve-se notar que o dplyr
nem sempre tem dplyr
rápida de dplyr
; veja o caso "A junção por uma coluna de caractere é lenta, em comparação com a junção por uma coluna de fator. # 1386 {Closed}" . Esse thread propõe usar os recursos do pool global de strings e comparar não strings como tais, mas ponteiros para strings.
Detalhes de gerenciamento de memória
Fontes básicas
Conclusão
Naturalmente, essa pergunta é constantemente feita de uma forma ou de outra, vários links abaixo.
Mas, para entender conscientemente o que fazer certo, quais são as oportunidades e limitações, é melhor descer para o nível mais baixo possível. Como se vê, há uma massa de especificidades não óbvias. A publicação é de natureza de pesquisa, pois afeta aspectos bastante básicos da R, que poucas pessoas utilizam em seu trabalho diário. Se houver acréscimos, comentários ou correções significativos, será muito interessante conhecê-los.
A publicação anterior é “Usando R para tarefas utilitárias” .