Existe alguma otimização possível para acesso aleatório em uma matriz muito grande (atualmente uso uint8_t
e estou perguntando o que é melhor)
uint8_t MyArray[10000000];
quando o valor em qualquer posição na matriz é
- 0 ou 1 para 95% de todos os casos,
- 2 em 4% dos casos,
- entre 3 e 255 nos outros 1% dos casos?
Então, existe algo melhor do que uma uint8_t
matriz para usar para isso? Deve ser o mais rápido possível fazer um loop sobre toda a matriz em uma ordem aleatória, e isso é muito pesado na largura de banda da RAM; portanto, ao ter mais do que alguns threads fazendo isso ao mesmo tempo para matrizes diferentes, atualmente toda a largura de banda da RAM é rapidamente saturado.
Estou perguntando, pois parece muito ineficiente ter uma matriz tão grande (10 MB) quando se sabe que quase todos os valores, com exceção de 5%, serão 0 ou 1. Portanto, quando 95% de todos os valores na matriz precisaria apenas de 1 bit em vez de 8 bits, isso reduziria o uso de memória em quase uma ordem de magnitude. Parece que deve haver uma solução mais eficiente em termos de memória que reduziria bastante a largura de banda de RAM necessária para isso e, como resultado, também seria significativamente mais rápido para acesso aleatório.
Respostas:
Uma possibilidade simples que vem à mente é manter uma matriz compactada de 2 bits por valor para os casos comuns e uma matriz separada de 4 bytes por valor (24 bits para o índice do elemento original, 8 bits para o valor real, portanto
(idx << 8) | value)
). outros.Quando você pesquisa um valor, primeiro faz uma pesquisa na matriz 2bpp (O (1)); se você encontrar 0, 1 ou 2, é o valor que deseja; se você encontrar 3, significa que você deve procurar na matriz secundária. Aqui, você realizará uma pesquisa binária para procurar o índice de seu interesse deslocado para a esquerda em 8 (O (log (n) com um n pequeno, pois esse deve ser o 1%)) e extrair o valor do 4- byte thingie.
Para uma matriz como a que você propôs, isso deve levar 10000000/4 = 2500000 bytes para a primeira matriz, mais 10000000 * 1% * 4 B = 400000 bytes para a segunda matriz; portanto, 2900000 bytes, ou seja, menos de um terço da matriz original, e a parte mais usada é mantida em conjunto na memória, o que deve ser bom para o cache (pode até caber em L3).
Se você precisar de endereçamento de mais de 24 bits, precisará ajustar o "armazenamento secundário"; uma maneira trivial de estendê-lo é ter uma matriz de ponteiros de 256 elementos para alternar entre os 8 bits principais do índice e encaminhar para uma matriz classificada indexada de 24 bits, como acima.
Referência rápida
(código e dados sempre atualizados no meu Bitbucket)
O código acima preenche uma matriz de 10 milhões de elementos com dados aleatórios distribuídos como OP especificado em suas postagens, inicializa minha estrutura de dados e, em seguida:
(observe que, no caso de pesquisa seqüencial, a matriz sempre vence em grande escala, pois é a pesquisa mais amigável ao cache que você pode fazer)
Esses dois últimos blocos são repetidos 50 vezes e cronometrados; no final, a média e o desvio padrão para cada tipo de pesquisa são calculados e impressos, juntamente com a aceleração (lookup_mean / array_mean).
Compilei o código acima com o g ++ 5.4.0 (
-O3 -static
, mais alguns avisos) no Ubuntu 16.04 e o executei em algumas máquinas; a maioria deles está executando o Ubuntu 16.04, alguns Linux mais antigos, outros mais recentes. Eu não acho que o sistema operacional deva ser relevante nesse caso.Os resultados são ... misturados!
fonte
uint32_t
vai ficar bem. A exclusão de um elemento do buffer secundário obviamente o deixará classificado. A inserção de um elemento pode ser feita comstd::lower_bound
e depoisinsert
(em vez de anexar e reorganizar a coisa toda). As atualizações tornam a matriz secundária em tamanho muito mais atraente - eu certamente começaria com isso.(idx << 8) + val
você não precisa se preocupar com a parte do valor - basta usar uma comparação direta. Ele vai sempre comparar menos do que((idx+1) << 8) + val
e inferior a((idx-1) << 8) + val
populate
função que deve ser preenchidamain_arr
e desec_arr
acordo com o formatolookup
esperado. Eu realmente não experimentá-lo, por isso não espere que ele realmente funciona corretamente :-); de qualquer forma, deve lhe dar uma idéia geral.Outra opção poderia ser
Em outras palavras, algo como:
onde
bmap
usa 2 bits por elemento com o valor 3 que significa "outro".Essa estrutura é trivial para atualização, usa 25% mais memória, mas a maior parte é pesquisada apenas em 5% dos casos. Obviamente, como sempre, se é uma boa ideia ou não depende de muitas outras condições, a única resposta é experimentar o uso real.
fonte
if(code != 3) return code;
emif(code == 0) return 0; if(code==1) return 1; if(code == 2) return 2;
__builtin_expect
& co ou PGO também podem ajudar.Este é mais um "comentário longo" do que uma resposta concreta
A menos que seus dados sejam algo conhecido, duvido que alguém possa DIRETAMENTE responder à sua pergunta (e não conheço nada que corresponda à sua descrição, mas não sei TUDO sobre todos os tipos de padrões de dados para todos. tipos de casos de uso). Dados esparsos são um problema comum na computação de alto desempenho, mas geralmente é "temos uma matriz muito grande, mas apenas alguns valores são diferentes de zero".
Para padrões não conhecidos como o que eu acho que é o seu, ninguém SABE diretamente o que é melhor, e isso depende dos detalhes: quão aleatório é o acesso aleatório - o sistema está acessando grupos de itens de dados ou é completamente aleatório? um gerador uniforme de números aleatórios. Os dados da tabela são completamente aleatórios ou existem sequências de 0 e sequências de 1, com uma dispersão de outros valores? A codificação de comprimento de execução funcionaria bem se você tiver seqüências razoavelmente longas de 0 e 1, mas não funcionará se você tiver "tabuleiro de damas de 0/1". Além disso, você teria que manter uma tabela de "pontos de partida", para poder trabalhar rapidamente no local relevante.
Eu sei há muito tempo que alguns grandes bancos de dados são apenas uma tabela grande na RAM (dados de assinantes de troca telefônica neste exemplo) e um dos problemas é que os caches e as otimizações da tabela de páginas no processador são bastante inúteis. O chamador é tão raramente o mesmo que alguém que ligou recentemente para alguém, que não há dados pré-carregados de qualquer tipo, é puramente aleatório. Tabelas de páginas grandes são a melhor otimização para esse tipo de acesso.
Em muitos casos, comprometer-se entre "velocidade e tamanho pequeno" é uma daquelas coisas que você deve escolher na engenharia de software [em outra engenharia, não é necessariamente um compromisso]. Portanto, "desperdiçar memória para código mais simples" é frequentemente a escolha preferida. Nesse sentido, a solução "simples" provavelmente é melhor para velocidade, mas se você tiver um uso "melhor" para a RAM, a otimização do tamanho da tabela forneceria desempenho suficiente e uma boa melhoria no tamanho. Existem várias maneiras diferentes de conseguir isso - como sugerido em um comentário, um campo de 2 bits em que os dois ou três valores mais comuns são armazenados e, em seguida, algum formato de dados alternativo para os outros valores - uma tabela de hash seria minha primeira abordagem, mas uma lista ou árvore binária pode funcionar também - novamente, isso depende dos padrões de onde você "não é 0, 1 ou 2". Novamente, depende de como os valores estão "dispersos" na tabela - eles estão em clusters ou são mais de um padrão distribuído uniformemente?
Mas um problema é que você ainda está lendo os dados da RAM. Você está gastando mais código processando os dados, incluindo algum código para lidar com o "isso não é um valor comum".
O problema com os algoritmos de compactação mais comuns é que eles são baseados em sequências de desempacotamento, portanto você não pode acessá-los aleatoriamente. E a sobrecarga de dividir seus grandes dados em pedaços de, digamos, 256 entradas por vez, e descompactar os 256 em uma matriz uint8_t, buscar os dados desejados e depois jogar fora os dados não compactados é altamente improvável de lhe dar uma boa desempenho - supondo que isso tenha alguma importância, é claro.
No final, você provavelmente terá que implementar uma ou algumas das idéias nos comentários / respostas para testar, ver se isso ajuda a resolver seu problema ou se o barramento de memória ainda é o principal fator limitante.
fonte
uint8_t
matriz, a largura de banda da RAM fica saturada depois que ~ 5 threads estão trabalhando nisso ao mesmo tempo (em um sistema de canal quádruplo), portanto, o uso de mais de 5 threads não oferece mais nenhum benefício. Eu gostaria que ele usasse> 10 threads sem encontrar problemas de largura de banda da RAM, mas se o lado da CPU do acesso se tornar tão lento que 10 threads sejam menos executados que 5 threads antes, isso obviamente não seria um progresso.O que eu fiz no passado é usar um hashmap na frente de um bitset.
Isso reduz pela metade o espaço em comparação com a resposta de Matteo, mas pode ser mais lento se as pesquisas de "exceção" forem lentas (ou seja, existem muitas exceções).
Muitas vezes, no entanto, "cache é rei".
fonte
0
significa olharmain_arr
e1
significa olhar parasec_arr
(no caso do código Matteos)? No entanto, isso precisaria de mais espaço do que a resposta de Matteos, já que é uma matriz adicional. Eu não entendo bem como você faria isso usando apenas metade do espaço em comparação com a resposta Matteos.A menos que haja um padrão para seus dados, é improvável que exista uma otimização sensata de velocidade ou tamanho e - supondo que você esteja direcionando um computador normal - 10 MB não é tão importante assim.
Há duas suposições em suas perguntas:
Eu acho que essas duas suposições são falsas. Na maioria dos casos, a maneira apropriada de armazenar dados é armazenar a representação mais natural. No seu caso, é para isso que você procurou: um byte para um número entre 0 e 255. Qualquer outra representação será mais complexa e, portanto, todas as outras coisas iguais, mais lentas e propensas a erros. Para se desviar desse princípio geral, você precisa de um motivo mais forte do que seis bits "desperdiçados" em 95% dos seus dados.
Para sua segunda suposição, será verdade se, e somente se, alterar o tamanho da matriz resultar em substancialmente menos falhas de cache. Se isso acontecerá, pode ser determinado definitivamente apenas pela criação de perfil do código de trabalho, mas acho que é altamente improvável que faça uma diferença substancial. Como você acessará aleatoriamente a matriz em ambos os casos, o processador terá dificuldade em saber quais bits de dados armazenar em cache e manter em ambos os casos.
fonte
Se os dados e acessos forem uniformemente distribuídos aleatoriamente, o desempenho provavelmente dependerá de qual fração dos acessos evitar uma falta de cache no nível externo. A otimização exigirá o conhecimento de qual tamanho de matriz pode ser acomodada de maneira confiável no cache. Se seu cache for grande o suficiente para acomodar um byte para cada cinco células, a abordagem mais simples pode ser manter um byte nos cinco valores codificados de base três no intervalo de 0 a 2 (existem 243 combinações de 5 valores, portanto caber em um byte), juntamente com uma matriz de 10.000.000 de bytes que seria consultada sempre que um valor base-3 indicar "2".
Se o cache não for tão grande, mas puder acomodar um byte por 8 células, não seria possível usar um valor de byte para selecionar dentre todas as 6.561 combinações possíveis de oito valores de base 3, mas como o único efeito de alterar 0 ou 1 para 2 seria causar uma pesquisa desnecessária; a correção não exigiria suporte a todos os 6.561. Em vez disso, pode-se focar nos 256 valores mais "úteis".
Especialmente se 0 for mais comum que 1 ou vice-versa, uma boa abordagem pode ser usar 217 valores para codificar as combinações de 0 e 1 que contêm 5 ou menos 1's, 16 valores para codificar xxxx0000 a xxxx1111, 16 para codificar 0000xxxx a 1111xxxx e um para xxxxxxxx. Restariam quatro valores para qualquer outro uso que se possa encontrar. Se os dados forem distribuídos aleatoriamente conforme descrito, uma pequena maioria de todas as consultas atingiria bytes que continham apenas zeros e uns (em cerca de 2/3 de todos os grupos de oito, todos os bits seriam zeros e uns e cerca de 7/8 de aqueles teriam seis ou menos 1 bits); a grande maioria daqueles que não aterrissariam em um byte que continha quatro x's e teriam 50% de chance de pousar em um zero ou um. Portanto, apenas uma em cada quatro consultas exigiria uma pesquisa de grande variedade.
Se os dados forem distribuídos aleatoriamente, mas o cache não for grande o suficiente para manipular um byte por oito elementos, pode-se tentar usar essa abordagem com cada byte manipulando mais de oito itens, mas a menos que exista uma forte tendência a 0 ou a 1 , a fração de valores que podem ser manipulados sem precisar fazer uma pesquisa na grande matriz diminuirá à medida que o número manipulado por cada byte aumentar.
fonte
Vou acrescentar à resposta do @ o11c , pois as palavras dele podem ser um pouco confusas. Se eu precisar apertar o último bit e o ciclo da CPU, faça o seguinte.
Começaremos construindo uma árvore de pesquisa binária equilibrada que contém os 5% de casos "algo mais". Para cada pesquisa, você percorre a árvore rapidamente: possui 10000000 elementos: 5% dos quais estão na árvore: portanto, a estrutura de dados da árvore contém 500000 elementos. Caminhar isso no tempo O (log (n)) fornece 19 iterações. Não sou especialista nisso, mas acho que existem algumas implementações com eficiência de memória por aí. Vamos adivinhar:
Total, 4 bytes: 500000 * 4 = 1953 kB. Se encaixa no cache!
Para todos os outros casos (0 ou 1), você pode usar um vetor de bits. Observe que você não pode deixar de fora os 5% de outros casos para acesso aleatório: 1,19 MB.
A combinação desses dois usa aproximadamente 3.099 MB. Usando esta técnica, você salvará um fator 3.08 de memória.
No entanto, isso não supera a resposta de @Matteo Italia (que usa 2,76 MB), uma pena. Existe algo que possamos fazer extra? A parte que consome mais memória são os 3 bytes de índice na árvore. Se conseguirmos reduzir para 2, economizaríamos 488 kB e o uso total de memória seria: 2.622 MB, que é menor!
Como vamos fazer isso? Temos que reduzir a indexação para 2 bytes. Novamente, 10000000 leva 23 bits. Precisamos ser capazes de eliminar 7 bits. Podemos simplesmente fazer isso particionando o intervalo de 10000000 elementos em 2 ^ 7 (= 128) regiões de 78125 elementos. Agora podemos construir uma árvore equilibrada para cada uma dessas regiões, com 3906 elementos em média. A escolha da árvore correta é feita por uma simples divisão do índice de destino por 2 ^ 7 (ou um deslocamento de bits
>> 7
). Agora, o índice necessário para armazenar pode ser representado pelos 16 bits restantes. Observe que há alguma sobrecarga no comprimento da árvore que precisa ser armazenada, mas isso é insignificante. Observe também que esse mecanismo de divisão reduz o número necessário de iterações para percorrer a árvore, agora reduz para 7 iterações a menos, porque eliminamos 7 bits: restam apenas 12 iterações.Observe que teoricamente você pode repetir o processo para cortar os próximos 8 bits, mas isso exigiria a criação de 2 ^ 15 árvores balanceadas, com ~ 305 elementos em média. Isso resultaria em 2,143 MB, com apenas 4 iterações para percorrer a árvore, o que é uma aceleração considerável em comparação com as 19 iterações que iniciamos.
Como conclusão final: isso supera a estratégia de vetor de 2 bits com um pouquinho de uso de memória, mas é uma luta toda a ser implementada. Mas se puder fazer a diferença entre ajustar o cache ou não, pode valer a pena tentar.
fonte
Se você executar apenas operações de leitura, seria melhor não atribuir um valor a um único índice, mas a um intervalo de índices.
Por exemplo:
Isso pode ser feito com uma estrutura. Você também pode definir uma classe semelhante a essa se gostar de uma abordagem OO.
Agora você só precisa percorrer uma lista de intervalos e verificar se o índice está em um deles, o que pode consumir muito menos memória em média, mas custa mais recursos da CPU.
Se você solicitar os intervalos por tamanho decrescente, aumenta a probabilidade de encontrar o item que você procura mais cedo, o que diminui ainda mais o uso médio de memória e recursos da CPU.
Você também pode remover todos os intervalos com um tamanho de 1. Coloque os valores correspondentes em um mapa e verifique-os apenas se o item que você está procurando não foi encontrado nos intervalos. Isso também deve elevar um pouco o desempenho médio.
fonte
unt8_t
, mesmo que consiga muito menos memória.Há muito tempo, eu me lembro ...
Na universidade, temos a tarefa de acelerar um programa traçador de raios, que deve ler repetidamente por algoritmo a partir de matrizes de buffer. Um amigo me disse para sempre usar leituras de RAM que são múltiplos de 4Bytes. Então mudei a matriz de um padrão de [x1, y1, z1, x2, y2, z2, ..., xn, yn, zn] para um padrão de [x1, y1, z1,0, x2, y2, z2 , 0, ..., xn, yn, zn, 0]. Significa adicionar um campo vazio após cada coordenada 3D. Após alguns testes de desempenho: foi mais rápido. Resumindo a história: leia vários RAMs de 4 bytes da matriz e talvez também da posição inicial correta, para ler um pequeno cluster onde o índice pesquisado está nele e ler o índice pesquisado desse pequeno cluster na CPU. (No seu caso, você não precisará inserir campos de preenchimento, mas o conceito deve ser claro)
Talvez também outros múltiplos possam ser a chave em sistemas mais novos.
Não sei se isso funcionará no seu caso, portanto, se não funcionar: desculpe. Se funcionar, eu ficaria feliz em saber sobre alguns resultados dos testes.
PS: Ah, e se houver algum padrão de acesso ou índices acessados nas proximidades, você poderá reutilizar o cluster em cache.
PPS: Pode ser que o fator múltiplo seja mais parecido com 16Bytes ou algo assim, faz muito tempo, que eu me lembro exatamente.
fonte
Olhando para isso, você pode dividir seus dados, por exemplo:
Nesse caso, todos os valores aparecem até um determinado índice; portanto, você pode remover um dos conjuntos de bits e representar o valor que está faltando nos outros.
Isso economizará um pouco de memória para este caso, mas pioraria o pior. Você também precisará de mais energia da CPU para fazer as pesquisas.
Certifique-se de medir!
fonte
Como Mats menciona em sua resposta aos comentários, é difícil dizer qual é realmente a melhor solução sem saber especificamente que tipo de dados você tem (por exemplo, existem longas execuções de zeros e assim por diante) e qual é o seu padrão de acesso como ("aleatório" significa "em todo o lugar" ou apenas "não estritamente de maneira completamente linear" ou "todos os valores exatamente uma vez, apenas aleatoriamente" ou ...).
Dito isto, existem dois mecanismos que vêm à mente:
(index,value)
ou(value,index)
mesas. Ou seja, tenha uma tabela muito pequena para o caso de 1%, talvez uma tabela para o caso de 5% (que só precisa armazenar os índices, pois todos têm o mesmo valor) e uma grande matriz de bits compactados para os dois casos finais. E com "tabela" quero dizer algo que permite uma pesquisa relativamente rápida; ou seja, talvez um hash, uma árvore binária e assim por diante, dependendo do que você tem disponível e de suas necessidades reais. Se essas subtabelas se encaixam nos caches de primeiro / segundo nível, você pode ter sorte.fonte
Eu não estou muito familiarizado com C, mas em C ++ você pode usar char não assinado para representar um número inteiro no intervalo de 0 a 255.
Comparado ao int normal (novamente, eu sou do mundo Java e C ++ ) no qual são necessários 4 bytes (32 bits), um caracter não assinado requer 1 byte (8 bits). portanto, isso pode reduzir o tamanho total da matriz em 75%.
fonte
uint8_t
- 8 significa 8 bits.Você descreveu sucintamente todas as características de distribuição de sua matriz; atire a matriz .
Você pode facilmente substituir a matriz por um método aleatório que produz a mesma saída probabilística que a matriz.
Se a consistência for importante (produzindo o mesmo valor para o mesmo índice aleatório), considere usar um filtro de bloom e / ou mapa de hash para rastrear hits repetidos. Se os acessos de sua matriz forem realmente aleatórios, isso é totalmente desnecessário.
fonte