Em linguagens como C e C ++, ao usar ponteiros para variáveis, precisamos de mais um local de memória para armazenar esse endereço. Então isso não é uma sobrecarga de memória? Como isso é compensado? Os ponteiros são usados em aplicativos críticos de pouca memória?
29
Respostas:
Na verdade, a sobrecarga realmente não está nos 4 ou 8 bytes extras necessários para armazenar o ponteiro. Na maioria das vezes, os ponteiros são usados para alocação dinâmica de memória , o que significa que invocamos uma função para alocar um bloco de memória, e essa função retorna para nós um ponteiro que aponta para esse bloco de memória. Esse novo bloco, por si só, representa uma sobrecarga considerável.
Agora, você não precisa se envolver na alocação de memória para usar um ponteiro: você pode ter uma matriz
int
declarada estaticamente ou na pilha e pode usar um ponteiro em vez de um índice para visitar osint
, e é tudo muito bonito, simples e eficiente. Nenhuma alocação de memória é necessária, e o ponteiro normalmente ocupa exatamente tanto espaço na memória quanto um índice inteiro.Além disso, como Joshua Taylor nos lembra em um comentário, ponteiros são usados para passar algo por referência. Por exemplo,
struct foo f; init_foo(&f);
alocaria f na pilha e depois chamariainit_foo()
com um ponteiro para issostruct
. Isso é muito comum. (Apenas tome cuidado para não passar esses ponteiros "para cima".) No C ++, você pode ver isso sendo feito com uma "referência" (foo&
) em vez de um ponteiro, mas as referências nada mais são do que ponteiros que você não pode alterar e eles ocupam o mesma quantidade de memória.Mas a principal razão pela qual os ponteiros são usados é para alocação dinâmica de memória, e isso é feito para resolver problemas que não poderiam ser resolvidos de outra forma. Aqui está um exemplo simplista: imagine que você deseja ler todo o conteúdo de um arquivo. Onde você vai guardá-los? Se você tentar com um buffer de tamanho fixo, poderá ler apenas arquivos que não excedam esse buffer. Mas, usando a alocação de memória, você pode alocar a quantidade de memória necessária para ler o arquivo e prosseguir para lê-lo.
Além disso, o C ++ é uma linguagem orientada a objetos e há certos aspectos do OOP, como abstração, que só são possíveis usando ponteiros. Mesmo linguagens como Java e C # fazem uso extensivo de ponteiros, elas simplesmente não permitem que você manipule diretamente os ponteiros, de modo a impedir que você faça coisas perigosas com eles, mas ainda assim, essas linguagens só começam a fazer sentido quando você tiver percebi que nos bastidores tudo é feito usando ponteiros.
Portanto, os ponteiros não são usados apenas em aplicativos com pouco tempo de memória crítica, são usados em qualquer lugar .
fonte
struct foo f; init_foo(&f);
alocariaf
na pilha e depois chamariainit_foo
com um ponteiro para essa estrutura. Isso é muito comum. (Só tome cuidado para não passar os ponteiros "para cima".)malloc
cabeçalho MUITO BAIXO sobrecarregam, pois agrupam blocos alocados em "buckets". Por outro lado, isso se traduz em sobre-alocação em geral: você perguntar para 35 bytes e obter 64 (sem o seu conhecimento) desperdiçando assim 29 ...Claro, um endereço extra (geralmente 4/8 bytes, dependendo do processador).
Não é. Se você precisar do indireto necessário para os ponteiros, poderá pagar por isso.
Não fiz muito trabalho lá, mas presumo que sim. O acesso ao ponteiro é um aspecto elementar da programação de montagem. São necessárias quantidades triviais de memória e as operações do ponteiro são rápidas - mesmo no contexto desses tipos de aplicativos.
fonte
Não tenho a mesma opinião sobre Telastyn.
Globais do sistema em um processador incorporado podem ser endereçados com endereços específicos e codificados.
Globais em um programa serão endereçados como um deslocamento de um ponteiro especial que aponta para o local na memória onde globais e estáticos são armazenados.
As variáveis locais aparecem quando uma função é inserida e são endereçadas como um deslocamento de outro ponteiro especial, geralmente chamado de "ponteiro de quadro". Isso inclui os argumentos para a função. Se você for cuidadoso com os empurrões e pops com o ponteiro da pilha, poderá eliminar o ponteiro do quadro e acessar as variáveis locais diretamente do ponteiro da pilha.
Portanto, você paga pelo indireto dos ponteiros, quer esteja percorrendo uma matriz ou apenas pegando alguma variável local ou global não digna de nota. É apenas baseado em um ponteiro diferente, dependendo da variável que é. O código que é bem compilado manterá esse ponteiro em um registro da CPU, em vez de recarregá-lo sempre que for usado.
fonte
Sim, claro. Mas é um ato de equilíbrio.
Normalmente, os aplicativos com pouca memória são construídos tendo em mente a compensação entre a sobrecarga de algumas variáveis de ponteiro em comparação com a sobrecarga do que seria uma enorme programa (que deve ser armazenado na memória, lembre-se!) Se ponteiros não pudessem ser usados .
Essa consideração se aplica a todos os programas, porque ninguém deseja criar uma bagunça horrível e impossível de manter, com o código duplicado à direita e no centro, vinte vezes maior do que precisa.
fonte
Você assume que o ponteiro precisa ser armazenado. Esse não é sempre o caso. Toda variável é armazenada em algum endereço de memória. Digamos que você tenha um
long
declarado comolong n = 5L;
. Isso aloca armazenamento paran
em algum endereço. Podemos usar esse endereço para fazer coisas sofisticadas, como*((char *) &n) = (char) 0xFF;
manipular partes den
. O endereço den
não é armazenado em nenhum lugar como uma sobrecarga extra.Mesmo que os ponteiros sejam armazenados explicitamente (por exemplo, em estruturas de dados como listas), a estrutura de dados resultante geralmente é mais elegante (mais simples, mais fácil de entender, mais fácil de manusear etc.) do que uma estrutura de dados equivalente sem ponteiros.
Sim. Os dispositivos que usam microcontroladores geralmente contêm muito pouca memória, mas o firmware pode usar ponteiros para lidar com vetores de interrupção ou gerenciamento de buffer, etc.
fonte
gcc -fverbose-asm -S -O2
a compilar algum código C)Ter um ponteiro definitivamente consome alguma sobrecarga, mas você também pode ver o lado positivo. Em C, você pode usar estruturas de dados complexas, como cadeias e estruturas, apenas devido a ponteiros. Até suas variáveis normais têm uma entrada na tabela de símbolos que armazena o endereço para onde sua variável está apontando. Portanto, acho que isso não gera muita sobrecarga em termos de memória (apenas 4 ou 8 bytes). Mesmo linguagens como java usam ponteiros internamente (referência), eles simplesmente não permitem que você os manipule, pois isso tornará a JVM menos segura. Você deve usar ponteiros somente quando não tiver outra opção, como tipos de dados ausentes, estruturas (em c), pois o uso de ponteiros pode levar a erros se não forem tratados adequadamente e são comparativamente mais difíceis de depurar.
De fato, suponha que você queira passar uma variável por referência, então é fácil manter um ponteiro em vez de replicar toda a estrutura e sincronizar as alterações entre elas (mesmo para copiá-las, você precisará de ponteiro). Como você lidaria com alocações e desalocações de memória não contíguas sem ponteiro?
fonte
Sim não Talvez?
Essa é uma pergunta incômoda, porque imagine o intervalo de endereçamento de memória na máquina e um software que precisa acompanhar permanentemente onde as coisas estão na memória de uma maneira que não pode ser ligada à pilha.
Por exemplo, imagine um reprodutor de música em que o arquivo de música é carregado com o botão pressionado pelo usuário e descarregado da memória volátil quando o usuário tenta carregar outro arquivo de música.
Como podemos rastrear onde os dados de áudio são armazenados? Precisamos de um endereço de memória para ele. O programa não apenas precisa acompanhar os blocos de dados de áudio na memória, mas também onde eles estão na memória. Portanto, precisamos manter em torno de um endereço de memória (ou seja, um ponteiro). E o tamanho do armazenamento necessário para o endereço de memória corresponderá ao intervalo de endereçamento da máquina (por exemplo: ponteiro de 64 bits para um intervalo de endereçamento de 64 bits).
Portanto, é uma espécie de "sim", requer armazenamento para rastrear um endereço de memória, mas não é possível evitá-lo para uma memória alocada dinamicamente desse tipo.
Falando apenas do tamanho de um ponteiro, você pode evitar o custo em alguns casos, utilizando a pilha. Por exemplo, nesse caso, os compiladores podem gerar instruções que efetivamente codificam o endereço de memória relativo, evitando o custo de um ponteiro. No entanto, isso deixa você vulnerável a estouros de pilha, se você fizer isso para alocações grandes e de tamanho variável, e também tende a ser impraticável (se não totalmente impossível) para uma série complexa de ramificações direcionadas pela entrada do usuário (como no exemplo de áudio acima).
Outra maneira é usar estruturas de dados mais contíguas. Por exemplo, uma sequência baseada em matriz pode ser usada em vez de uma lista duplamente vinculada que requer dois ponteiros por nó. Também podemos usar um híbrido desses dois como uma lista não rolada que armazena apenas ponteiros entre cada grupo contíguo de N elementos.
Sim, muito comumente, como muitos aplicativos críticos para o desempenho são escritos em C ou C ++ que são dominados pelo uso do ponteiro (eles podem estar atrás de um ponteiro inteligente ou de um contêiner como
std::vector
oustd::string
, mas a mecânica subjacente se resume a um ponteiro usado para acompanhar o endereço de um bloco de memória dinâmico).Agora, de volta a esta pergunta:
Os ponteiros costumam ser muito baratos, a menos que você os armazene como um milhão deles (que ainda é um mísero * 8 megabytes em uma máquina de 64 bits).
* Observe como Ben apontou que um "mísero" 8 megas ainda é do tamanho do cache L3. Aqui eu usei "míseros" mais no sentido do uso total de DRAM e do tamanho relativo típico dos blocos de memória que um uso saudável dos ponteiros apontará.
Onde os ponteiros ficam caros não são os ponteiros, mas:
Alocação dinâmica de memória. A alocação dinâmica de memória tende a ser cara, pois precisa passar por uma estrutura de dados subjacente (por exemplo, alocador de amigos ou lajes). Embora muitas vezes sejam otimizados até a morte, eles são de uso geral e projetados para lidar com blocos de tamanho variável que exigem que eles façam pelo menos um pouco de trabalho semelhante a uma "pesquisa" (embora leve e possivelmente até constante) para encontre um conjunto gratuito de páginas contíguas na memória.
Acesso à memória. Isso costuma ser a maior sobrecarga para se preocupar. Sempre que acessamos a memória alocada dinamicamente pela primeira vez, há uma falha de página obrigatória e falhas de cache, movendo a memória para baixo na hierarquia de memória e para dentro de um registro.
Acesso à memória
O acesso à memória é um dos aspectos mais críticos do desempenho, além dos algoritmos. Muitos campos críticos de desempenho, como os mecanismos de jogos AAA, concentram grande parte de sua energia em otimizações orientadas a dados, que se resumem a padrões e layouts de acesso à memória mais eficientes.
Uma das maiores dificuldades de desempenho de linguagens de nível superior que desejam alocar cada tipo definido pelo usuário separadamente por meio de um coletor de lixo, por exemplo, é que eles podem fragmentar bastante a memória. Isso pode ser especialmente verdadeiro se nem todos os objetos forem alocados de uma só vez.
Nesses casos, se você armazenar uma lista de um milhão de instâncias de um tipo de objeto definido pelo usuário, o acesso a essas instâncias sequencialmente em um loop pode ser bastante lento, pois é análogo a uma lista de um milhão de ponteiros que apontam para regiões diferentes da memória. Nesses casos, a arquitetura deseja buscar a memória dos níveis superior, mais lento e mais alto da hierarquia em blocos grandes e alinhados, com a esperança de que os dados ao redor desses blocos sejam acessados antes da remoção. Quando cada objeto dessa lista é alocado separadamente, geralmente acabamos pagando por ele com falhas de cache, quando cada iteração subsequente pode ser carregada de uma área completamente diferente da memória, sem que objetos adjacentes sejam acessados antes da remoção.
Atualmente, muitos compiladores para esses idiomas estão fazendo um ótimo trabalho na seleção de instruções e na alocação de registros, mas a falta de controle mais direto sobre o gerenciamento de memória aqui pode ser fatal (embora muitas vezes menos propensa a erros) e ainda criar idiomas como C e C ++ bastante popular.
Otimizando indiretamente o acesso ao ponteiro
Nos cenários mais críticos para o desempenho, os aplicativos geralmente usam pools de memória que agrupam memória de partes contíguas para melhorar a localidade de referência. Nesses casos, mesmo uma estrutura vinculada, como uma árvore ou uma lista vinculada, pode ser otimizada para cache, desde que o layout da memória de seus nós seja de natureza contígua. Isso está efetivamente tornando a desreferenciação de ponteiros mais barata, embora indiretamente, melhorando a localidade de referência envolvida ao desmarcá-las.
Perseguindo indicadores
Suponha que tenhamos uma lista vinculada individualmente como:
O problema é que, se alocarmos todos esses nós separadamente em relação a um alocador de uso geral (e possivelmente nem todos de uma vez), a memória real poderá ser dispersada da seguinte maneira (diagrama simplificado):
Quando começamos a procurar por ponteiros e acessar o
Foo
nó, começamos com uma falha obrigatória (e possivelmente uma falha de página) movendo um pedaço de sua região de memória de regiões mais lentas da memória para regiões mais rápidas da memória, como:Isso nos leva a armazenar em cache (possivelmente também a página) uma região de memória para acessar apenas uma parte dela e expulsar o restante, à medida que procuramos indicadores nesta lista. Ao assumir o controle sobre o alocador de memória, no entanto, podemos alocar essa lista de forma contígua assim:
... e, assim, melhorar significativamente a velocidade com a qual podemos desreferenciar esses ponteiros e processar seus apontadores. Portanto, embora muito indiretos, podemos acelerar o acesso ao ponteiro dessa maneira. Obviamente, se apenas armazenássemos esses contíguos em uma matriz, não teríamos esse problema em primeiro lugar, mas o alocador de memória aqui, que nos fornece controle explícito sobre o layout da memória, pode salvar o dia em que uma estrutura vinculada é necessária.
* Nota: este é um diagrama e discussão muito simplificados demais sobre a hierarquia de memória e a localidade de referência, mas espero que seja apropriado para o nível da pergunta.
fonte
É de fato uma sobrecarga de memória, mas muito pequena (ao ponto da insignificância).
Não é compensado. Você precisa perceber que o acesso aos dados através de um ponteiro (desreferenciando um ponteiro) é extremamente rápido (se bem me lembro, ele usa apenas uma instrução de montagem por desreferencia). É rápido o suficiente para que, em muitos casos, seja a alternativa mais rápida que você tem.
Sim.
fonte
Você só precisa do uso de memória extra (4-8 bytes por ponteiro, normalmente) enquanto precisar desse ponteiro. Existem muitas técnicas que tornam isso mais acessível.
A técnica mais fundamental que torna os ponteiros poderosos é que você não precisa manter todos os ponteiros. Às vezes você pode usar um algoritmo para construir um ponteiro de um ponteiro para outra coisa. O exemplo mais trivial disso é a aritmética de matriz. Se você alocar uma matriz de 50 números inteiros, não precisará manter 50 ponteiros, um para cada número inteiro. Você normalmente acompanha um ponteiro (o primeiro) e usa a aritmética do ponteiro para gerar os outros rapidamente. Às vezes, você pode manter temporariamente um desses ponteiros para um elemento específico da matriz, mas apenas enquanto precisar. Quando terminar, você pode descartá-lo, desde que tenha informações suficientes para regenerá-las mais tarde, se necessário. Isso pode parecer trivial, mas é exatamente o tipo de ferramenta de conservação que você
Em situações de memória extremamente apertada, isso pode ser usado para minimizar custos. Se você estiver trabalhando em um espaço de memória muito pequeno, geralmente tem uma boa noção de quantos objetos precisa manipular. Em vez de alocar um número inteiro de um por vez e manter ponteiros completos para eles, você pode aproveitar o conhecimento do desenvolvedor de que nunca terá mais de 256 números inteiros nesse algoritmo específico. Nesse caso, você pode manter um ponteiro para o primeiro número inteiro e acompanhar um índice usando um caractere (1 byte) em vez de usar um ponteiro completo (4/8 bytes). Você também pode usar truques algorítmicos para gerar alguns desses índices em tempo real.
Esse tipo de consciência de memória era muito popular no passado. Por exemplo, os jogos NES dependeriam amplamente de sua capacidade de compilar dados e gerar ponteiros algoritmicamente, em vez de terem que armazenar todos eles por atacado.
Situações extremas de memória também podem levar alguém a fazer coisas como alocar todos os espaços nos quais você opera no momento da compilação. Em seguida, o ponteiro que você precisa armazenar para essa memória é armazenado no programa e não nos dados. Em muitas situações de restrição de memória, você tem memória de dados e programa separada (geralmente ROM x RAM), portanto, poderá ajustar a maneira como usa seu algoritmo para enviar os ponteiros para a memória do programa.
Fundamentalmente, você não pode se livrar de toda a sobrecarga. No entanto, você pode controlá-lo. Usando técnicas algorítmicas, você pode minimizar o número de ponteiros que pode armazenar. Se você estiver usando ponteiros para a memória dinâmica, nunca ficará abaixo do custo de manter 1 ponteiro nesse ponto de memória dinâmica, porque essa é a quantidade mínima de informações necessárias para acessar qualquer coisa nesse bloco de memória. No entanto, em cenários de restrição de memória ultra-apertada, esse costuma ser o caso especial (restrições de memória dinâmica e de memória ultra-apertada tendem a não aparecer nas mesmas situações).
fonte
Em muitas situações, os ponteiros realmente economizam memória. Uma alternativa comum ao uso de ponteiros é fazer uma cópia de uma estrutura de dados. Uma cópia completa de uma estrutura de dados será maior que um ponteiro.
Um exemplo de aplicativo de tempo crítico é uma pilha de rede. Uma boa pilha de rede será projetada para ser "cópia zero" - e, para isso, é necessário o uso inteligente de ponteiros.
fonte