Memória

Monitorar servidor em tempo real com o Netdata

O Netdata é uma ferramenta opensource para monitoramento em tempo real de sistemas baseados em Unix. Ela possui uma infinidade de coletores e métricas sobre o uso dos recursos do sistema como: memória, CPU, rede, operações de escrita/leitura no disco etc.

Painel do Netdata

Não obstante, a ferramenta também disponibiliza coletores para aplicações de bancos de dados, servidores web (como o Nginx) entre outros.

A melhor parte de usar o Netdata é que os resultados são imediatos, sem muitas configurações. Você instala e já começa a visualizar os dados coletados. Com o tempo você personaliza as configurações que julgar serem importantes pro seu caso de uso.

Esse artigo não tem o objetivo de teorizar todos os recursos do Netdata, para isso, você pode consultar o site oficial e a sua vasta documentação. A intenção aqui é ser um guia rápido de instalação para que você consiga monitorar o seu servidor em tempo real.

Você pode testar a interface fornecida pelo Netdata e ver as informações padrões que ela oferece através dessa demonstração: https://london.my-netdata.io/

Amazon Web Services (AWS) - EC2 - Fundamentos
Curso de Amazon Web Services (AWS) - EC2 - Fundamentos
CONHEÇA O CURSO

Instalando o Netdata

Existem diversos métodos de instalação do Netdata, sendo o mais simples, o que usa a ferramenta oficial de instalação automática e ela suporta todas as distribuições Linux.

Basta executar no seu servidor:

bash <(curl -Ss https://my-netdata.io/kickstart.sh)

O processo pode demorar alguns minutos. Muitas das coisas serão compiladas.

É isso. Tá tudo pronto. Na pós-instalação ele inicia o agente e você pode validar isso:

sudo service netdata status

Se exibir active (running), está tudo no ponto:

Netdata service running

Caso ele não tenha sido iniciado, você pode tentar forçar essa inicialização:

sudo service netdata start

O Netdata abre a porta 19999 para acesso à sua interface web, você pode acessá-lo assim http://seuservidor.com:19999/. Mas não é uma boa coisa deixar isso público para todas as pessoas que entrarem em seu site. Vamos restringir esse acesso para localhost.

Restringindo o acesso à localhost

Isso pode ser feito editando o arquivo /etc/netdata/netdata.conf e na seção [web] remover o comentário # da linha abaixo e definir o valor dela para o IP local:

[web]
    bind to = 127.0.0.1

Depois de cada alteração nos arquivos de configuração do Netdata, é preciso reiniciá-lo

sudo service netdata restart

Estratégias para acesso à interface do Netdata

Agora que bloqueamos o acesso ao socket do Netdata apenas para localhost, temos que definir por onde vamos visualizar os dados e as métricas coletadas do servidor. Existem duas principais estratégias:

  • 1) Através do seu servidor mesmo, criando um proxy reverso no Nginx e definindo uma senha para acessá-lo na web. Por exemplo, você poderia acessar assim: www.seusite.com.br/netdata ou netdata.seusite.com.br;
  • 2) Utilizando o Netdata Cloud, que é a forma mais simples (e é grátis). O Netdata Cloud permite coletar em tempo real os dados e métricas do servidor e organizá-los em espaços e salas de uma forma descentralizada. O Netdata Cloud funciona apenas no modo leitura e toda a comunicação nas duas pontas é feita via uma conexão segura.

Depois de se cadastrar no Netdata Cloud, ele pedirá um nome para o espaço onde as salas de nós monitorados ficarão agrupadas. Você pode colocar o nome que quiser:

Criar o primeiro espaço no netdata

Na tela seguinte, ele pedirá para criar uma “War Room”, que basicamente é uma forma de agrupar nós comuns que você estará monitorando. Por exemplo, se você tem 2 ou 3 servidores EC2 para monitorar, uma possível sala (war room) poderia se chamar EC2:

Netdata Cloud - War Room

Na próxima etapa, é hora de adicionar o nó do seu servidor:

Adicionar nó no Netdata

Em “Rooms” selecione a sala que você acabou de criar. Em seguida é só copiar o comando indicado e executar no servidor onde o agente do Netdata foi instalado. Só avançar até chegar na tela principal, onde o seu nó estará listado. Um exemplo de uma sala com vários nós:

Netdata nodes

É possível adicionar N nós e N salas no Netdata Cloud. A partir desse momento, é só você entrar no nó e usufruir das informações fornecidas por ele.

O que mais posso fazer?

Não existe lugar melhor para consultar os recursos, configurações e opções que a própria documentação do Netdata. Portanto, recomendo enormemente que você dê uma atenção especial a ela.

O que mais pode ser feito no Netdata?

  • Personalização de alertas e configuração de notificações para serem enviadas no seu Telegram, Slack, E-mail, SMS etc;
  • Coletores de outros serviços e aplicações;
  • Outras features e características você pode visualizar na documentação oficial;

Em um próximo artigo veremos como configurar o Netdata para monitorar a stack PHP-FPM e Nginx.

Até a próxima!

Amazon Web Services (AWS) - Fundamentos
Curso de Amazon Web Services (AWS) - Fundamentos
CONHEÇA O CURSO

Generators no PHP

iterGenerators foram adicionados no PHP na versão 5.5 (meados de 2013) e aqui estamos nós, anos depois, falando sobre eles. É que esse ainda não é um assunto muito difundido, digo, não é trivial encontrar casos e mais casos de uso para eles no contexto padrão de desenvolvimento para o PHP. Normalmente o uso deles fica abstraído em libraries e frameworks.

Mas, antes de tudo, é importante entendermos o que são Iterators, ademais, Iterators e Generators são assuntos intrinsecamente relacionados.

Para isso, recomendo a leitura do artigo: Iterators no PHP

Todos os exemplos desse artigo estão disponíveis no repositório: https://github.com/KennedyTedesco/php-iterators-generators

Desenvolvedor PHP Júnior
Formação: Desenvolvedor PHP Júnior
Nesta formação você aprenderá todos os fundamentos necessário para iniciar do modo correto com a linguagem PHP, uma das mais utilizadas no mercado. Além dos conceitos de base, você também conhecerá as características e a sintaxe da linguagem de forma prática.
CONHEÇA A FORMAÇÃO

O que é um Generator?

Numa explicação acessível, Generators são uma forma prática de se implementar Iterators. Isso quer dizer que, com Generators, podemos implementar complexos Iterators sem que precisemos criar objetos, implementar interfaces e tudo mais que vimos anteriormente. Generators dão para uma função a capacidade de retornar uma sequência de valores.

Para criar uma função Generator, basta que ela possua a palavra reservada yield. O operador yield é uma espécie de return, só que com algumas particularidades. E uma função desse tipo retorna um objeto da classe Generator, que é uma classe especial e específica para esse contexto, não sendo possível utilizá-la de outra forma. Esse objeto retornado pode ser iterado. É aí que entra a nossa base sobre Iterators que aprendemos anteriormente. A classe Generator implementa a interface Iterator.

Vejamos o exemplo mais elementar possível de uma função generator:

<?php

declare(strict_types=1);

function getBooks(): Generator
{
    yield 'Book 1';
    yield 'Book 2';
    yield 'Book 3';
    yield 'Book 4';
    yield 'Book 5';
}

foreach (getBooks() as $book) {
    echo $book . \PHP_EOL;
}

O resultado:

Book 1
Book 2
Book 3
Book 4
Book 5

Sempre que uma função usar o operador yield ela vai retornar um objeto do tipo Generator. E, se Generator é um Iterator, logo, ele pode ser iterado.

Não precisamos de um yield para cada registro da nossa coleção. Normalmente o que vamos ver é um yield dentro de um laço:

<?php

declare(strict_types=1);

function getBooks(): Generator
{
    for ($i = 0; $i < 100; $i++) {
        yield "Book {$i}";
    }
}

foreach (getBooks() as $book) {
    echo $book . \PHP_EOL;
}

E é possível que o yield retorne um conjunto de chave => valor:

<?php

declare(strict_types=1);

function getBooks(): Generator
{
    for ($i = 0; $i < 10; $i++) {
        yield $i * 2 => "Book {$i}";
    }
}

foreach (getBooks() as $key => $value) {
    echo "{$key} -> {$value}" . \PHP_EOL;
}

O resultado:

0 -> Book 0
2 -> Book 1
4 -> Book 2
6 -> Book 3
8 -> Book 4
10 -> Book 5
12 -> Book 6
14 -> Book 7
16 -> Book 8
18 -> Book 9

E a parte que “toca” a memória?

Um dos benefícios de se usar generators está no baixo consumo de memória associado. Com um generator recuperamos a informação sob demanda, sem alocar toda a coleção na memória. Trabalhamos com um dado por vez no buffer.

É relativamente comum aplicações que precisam trabalhar em grandes massas de dados. Vamos emular uma situação onde possamos visualizar essa relação do uso eficiente de memória?

<?php

declare(strict_types=1);

function getRecords(): array
{
    $records = [];
    for ($i = 0; $i < 100_000; $i++) {
        $records[] = "Record $i";
    }
    return $records;
}

function formatRecords(array $records): array
{
    $new = [];
    foreach ($records as $index => $record) {
        $new[] = "[$index] -> {$record}";
    }
    return $new;
}

$registros = formatRecords(getRecords());
foreach ($registros as $registro) {
    echo $registro . \PHP_EOL;
}

echo 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

A getRecords() aloca 100k registros na memória e os retorna. A formatRecords() recebe uma coleção de dados, formata-os, aloca-os na memória (em um novo array) e então os retorna.

O resultado da memória consumida por esse script foi:

Used memory: ~16.78MB

Agora, refatorando o exemplo para que as duas funções se transformem em generators:

<?php

declare(strict_types=1);

function getRecords(): Generator
{
    for ($i = 0; $i < 100_000; $i++) {
        yield "Record $i";
    }
}

function formatRecords(Generator $records): Generator
{
    foreach ($records as $index => $record) {
        yield "[$index] -> {$record}";
    }
}

$registros = formatRecords(getRecords());
foreach ($registros as $registro) {
    echo $registro . \PHP_EOL;
}

echo 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

O resultado:

Used memory: ~0.41MB

Uma diferença muito grande. Quanto maior o dataset, maior a diferença será.

Lendo grandes arquivos

Vamos supor que precisamos ler um documento de texto linha a linha. Uma das formas tradicionais de lidar com isso seria assim:

<?php

declare(strict_types=1);

function getLines(string $filePath): array
{
    $file = \fopen($filePath, 'rb');

    $lines = [];
    while (!\feof($file)) {
        $lines[] = \fgets($file);
    }

    \fclose($file);

    return $lines;
}

$lines = getLines(__DIR__.'/file.txt');

foreach ($lines as $line) {
    echo $line;
}

echo PHP_EOL . 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

Se o seu sistema é unix-based você pode gerar um arquivo com dados randômicos de 10MB (para testar o exemplo) executando:

base64 /dev/urandom | head -c 10000000 > file.txt

O resultado dessa execução no meu ambiente foi:

Used memory: ~18.26MB

Se refatorarmos o exemplo para:

<?php

declare(strict_types=1);

function getLines(string $filePath): Generator
{
    $file = \fopen($filePath, 'rb');

    while (!\feof($file)) {
        yield \fgets($file);
    }

    \fclose($file);
}

$lines = getLines(__DIR__.'/file.txt');

foreach ($lines as $line) {
    echo $line;
}

echo PHP_EOL . 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

O resultado será:

Used memory: ~0.41MB

Veja que a diferença do consumo de memória entre as duas abordagens é enorme.

Usando iterators e generators

No artigo sobre Iterators vimos sobre a interface SeekableIterator. Agora, vamos criar um Iterator personalizado que lê linha a linha de um arquivo e que também nos dará a opção de pular para uma linha arbitrária qualquer.

A classe se chama LineFileIterator:

<?php

declare(strict_types=1);

namespace Iterators;

use Generator;
use OutOfBoundsException;
use SeekableIterator;

final class LineFileIterator implements SeekableIterator
{
    private string $filePath;
    private Generator $generator;

    public function __construct(string $filePath)
    {
        $this->filePath = $filePath;
        $this->rewind();
    }

    public function rewind(): void
    {
        $this->generator = $this->getGenerator();
    }

    public function current()
    {
        return $this->generator->current();
    }

    public function key(): int
    {
        return (int) $this->generator->key();
    }

    public function next(): void
    {
        $this->generator->next();
    }

    public function valid(): bool
    {
        return $this->generator->valid();
    }

    public function seek($position): void
    {
        while ($this->valid()) {
            if ($this->generator->key() === $position) {
                return;
            }

            $this->generator->next();
        }

        throw new OutOfBoundsException("Invalid position ($position)");
    }

    private function getGenerator(): Generator
    {
        $file = \fopen($this->filePath, 'rb');

        while (!\feof($file)) {
            yield \fgets($file);
        }

        \fclose($file);
    }
}

Usando o mesmo arquivo de texto que criamos anteriormente, podemos testar essa implementação assim:

<?php

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Iterators\LineFileIterator;

$iterator = new LineFileIterator(__DIR__ . '/file.txt');

// Set the pointer to the line 50.000
$iterator->seek(50_000);

// Get the current line
echo $iterator->current();

// Move to the next line (50.001)
$iterator->next();

// Get the current line
echo $iterator->current();

echo 'Used memory: ~' . \round((\memory_get_peak_usage()/1024/1024), 2) . 'MB';

Nesse nosso exemplo construímos um iterator que por debaixo dos panos usa generators para acessar sob demanda linhas de um arquivo.

Palavras finais

O uso de Generators é indicado e muitas vezes se faz necessário quando é preciso iterar uma grande coleção de dados. Eles são, inclusive, base para programação assíncrona e um framework que se destaca utilizando-os é o AMP:

$iterator = new Producer(function (callable $emit) {
    yield $emit(1);
    yield $emit(new Delayed(500, 2));
    yield $emit(3);
    yield $emit(4);
});

Inclusive, também escrevi um artigo sobre Corrotinas e código assíncrono em PHP usando Generators, recomendo a leitura.

E ainda sobre programação assíncrona, recomendo a leitura dos artigos:

Até a próxima!

Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Estrutura interna de valor, referências e gerenciamento de memória no PHP

A forma com o que o PHP trabalha com referências e gerenciamento de memória não é um assunto muito popular e é compreensível que não seja, se levarmos em conta que na maior parte do tempo como desenvolvedores não precisamos nos preocupar com isso, ademais, o PHP implementa um garbage collector que cuida da contagem das referências e decide o melhor momento de liberar a memória, sem impactar na experiência do desenvolvedor.

Quando falo em “experiência do desenvolvedor”, me refiro ao fato de não precisarmos a todo momento usar unset() para destruir variáveis não mais utilizadas, tampouco passar estruturas mais complexas como objetos ou arrays por referência a todo momento etc. Há raros e específicos casos em que usar referência faz sentido. Como também há raros e específicos casos em que liberar a memória manualmente (sem aguardar o trabalho do garbage collector) faz sentido como, por exemplo, a liberação de um resource para alguma operação subsequente importante que vai consumir um tanto considerável de memória.

A ideia aqui é tentar ao máximo ter uma abordagem leve, de desenvolvedor para desenvolvedor, no sentido de usuários da linguagem. Não tenho a pretenção e nem o conhecimento necessário para explicar o funcionamento interno do PHP e, além do mais, existe gente muito boa e mais preparada pra isso (compartilharei links no final do artigo).

Em suma, esse artigo se propõe a responder as seguintes questões:

  • Como um valor é representado internamente no PHP?
  • Valores em PHP possuem que semântica? De valor ou de referência?
  • Referências? O que são?
  • Objetos são passados por referência?
  • Se tenho um array com milhões de registros e preciso passá-lo para o argumento de uma função, e agora? Vou dobrar o tanto de memória que estou usando?
Desenvolvedor PHP Júnior
Formação: Desenvolvedor PHP Júnior
Nesta formação você aprenderá todos os fundamentos necessário para iniciar do modo correto com a linguagem PHP, uma das mais utilizadas no mercado. Além dos conceitos de base, você também conhecerá as características e a sintaxe da linguagem de forma prática.
CONHEÇA A FORMAÇÃO

A estrutura de dado que representa os valores do PHP

No PHP existe uma estrutura de dado elementar chamada zval (zend value), ela é uma estrutura container para qualquer valor arbitrário que se trabalha no PHP:

struct _zval_struct {
    zend_value value;
    union {
        // ...
        uint32_t type_info;
    } u1;
    // ...
};

Para a nossa noção, o mais importante é notarmos que essa estrutura armazena o valor e o tipo desse valor. E, pelo fato de o PHP ser uma linguagem de tipagem dinâmica, o tipo é conhecido apenas em tempo de execução e não em tempo de compilação. Além disso, esse tipo pode mudar durante a execução, por exemplo, um inteiro pode simplesmente virar uma string em um dado momento.

O valor propriamente dito é armazenado na union zend_value (uma union define múltiplos membros de diferentes tipos mas só um deles pode ser ativo por vez):

typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

Por essa estrutura já podemos inferir os tipos internos das variáveis que declaramos. Objetos são armazenados numa estrutura separada do tipo zend_object, arrays são do tipo zend_array numa implementação de HashTable e assim por diante. Valores simples como inteiros e float são armazenados diretamente na zval, enquanto que valores complexos como strings e objetos são armazenados em estruturas separadas e representados na zval por ponteiros (como pode ser visto em zend_string *str;).

Outra noção refere-se ao fato de que internamente o PHP faz a contagem da quantidade de vezes que um valor é usado por outras variáveis (symbols) e isso é essencial para o gerenciamento da memória por parte do garbage collector, uma vez que esse contador estiver zerado, indica pro GC que a memória desse valor pode ser liberada.

A semântica dos valores

No PHP os valores possuem a semântica de valor, desde que explicitamente não peçamos por uma referência. Ou seja, quando passamos um valor para um argumento de uma função, ou quando atribuímos uma variável a outra, estamos sempre trabalhando em cópias do mesmo valor. Vamos esclarecer esse conceito com exemplos, para que depois possamos adentrar no assunto de referências.

Exemplo:

<?php

$foo = 1;
$bar = $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(1)

$bar aponta para uma zval diferente, apenas com a cópia do valor de $foo. $foo foi incrementada e isso não refletiu em $bar.

Outro exemplo:

<?php

function increment(int $value) : void {
    $value++;
}

$value = 1;

increment($value);

echo $value; // 1

O valor de $value dentro do escopo da função é um e fora dela, é outro. Por esse motivo, o que está sendo incrementado é o zval do escopo da função.

Mas, veja bem, cópias não são feitas arbitrariamente sem necessidade. Para valores comuns como inteiros, números de ponto flutuante etc, elas são imediatamente feitas. No entanto, o mesmo não acontece com valores complexos como strings e arrays, por exemplo. O PHP usa o princípio copy-on-write, onde uma deep-copy (duplicação) dos valores é feita apenas antes desses dados serem alterados.

O melhor exemplo para ilustrar isso:

<?php

// Etapa 1: Consumo atual de memória
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2: Criando um grande array dinâmicamente
$foo = [];
for ($i = 0; $i < 1000000; $i++) {
    $foo[] = random_int(1, 100);
}
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3: Atribuindo o array a duas novas variáveis
$bar = $foo;
$baz = $foo;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4: Fizemos uma alteração de escrita no array $bar
$bar[] = 1;
echo sprintf("4) Mem: %s \n", memory_get_usage());

// Etapa 5: Zerando os arrays previamente criados
$foo = [];
$bar = [];
$baz = [];
echo sprintf("5) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 395280 
2) Mem: 33953920 
3) Mem: 33953920 
4) Mem: 67512528 
5) Mem: 395312 

Veja que nas etapas 2 e 3 o consumo de memória permaneceu o mesmo. O senso comum seria imaginar que ele fosse triplicar, uma vez que atribuímos dois novos arrays com o valor de $foo. No entanto, o consumo de memória duplicou apenas na etapa 4, pois alteramos o array $bar, essa operação de escrita fez com que o PHP duplicasse o array para esse zval.

O mesmo comportamento pode ser visto com strings:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
$string2 = $string;
$string3 = $string;
echo sprintf("3) Mem: %s \n", memory_get_usage());

// Etapa 4
$string2 .= 'BAR';
$string3 .= 'BAR';
echo sprintf("4) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393504 
2) Mem: 696640 
3) Mem: 696640 
4) Mem: 1302848

Apenas na etapa 4 tivemos aumento da memória, pois as strings foram modificadas, forçando deep-copy.

Outro exemplo:

<?php

// Etapa 1
echo sprintf("1) Mem: %s \n", memory_get_usage());

// Etapa 2
$string = str_repeat('FOO', 100000);
echo sprintf("2) Mem: %s \n", memory_get_usage());

// Etapa 3
function foo(string $string) {
    $string2 = $string;
}

foo($string);

echo sprintf("3) Mem: %s \n", memory_get_usage());

Resultado:

1) Mem: 393400 
2) Mem: 696536 
3) Mem: 696536
Desenvolvedor PHP Pleno
Formação: Desenvolvedor PHP Pleno
Nesta formação você aprenderá aspectos mais avançados da linguagem PHP, indo adiante nas características da linguagem, orientação a objetos, práticas de mercado, além da parte de integração com banco de dados. Ao final, estará apto a desenvolver sistemas usando banco de dados relacionais.
CONHEÇA A FORMAÇÃO

Referências no PHP

Referência é um mecanismo que permite que múltiplas variáveis (símbolos) apontem pro mesmo container interno de valor (zval). Referências não são ponteiros como vemos em C ou em Go, por exemplo.

Voltando ao exemplo anterior de $foo e $bar, como podemos fazer para $bar apontar para o mesmo zval que $foo de tal forma que o incremento em $foo reflita em $bar? Aí que entra o operador de referência & na atribuição:

<?php

$foo = 1;
$bar =& $foo;
$foo++;

var_dump($foo, $bar); // int(2), int(2)

Agora, na prática, temos dois símbolos ($foo e $bar) que apontam para o mesmo valor, alterações em um ou em outro refletirão no mesmo container interno de valor. Por esse motivo o incremento de $foo agora refletiu em $bar.

Esse é o exemplo mais elementar para distinguir a semântica de valor da de referência.

Objetos são passados por referência?

Existe a noção entre desenvolvedores e artigos na web de que objetos são passados por referência, mas isso não reflete a realidade. Eles também são passados pela semântica de valor, como qualquer outro valor. Eles apenas possuem um comportamento parecido com “referência” mas que internamente não se trata de referência.

Vejamos esse exemplo:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = $node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=0)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=0)=class Node {  }

Nota: A função xdebug_debug_zval() retorna detalhes internos da zval e para ela funcionar, é preciso ter a extensão xdebug instalada.

Na prática, o que temos nesse exemplo, é que as duas variáveis são estruturas (zval) distintas, mas internamente apontam pro mesmo objeto.

O que é diferente de:

<?php

class Node
{
    // Todo
}

$node1 = new Node();
$node2 = &$node1;

xdebug_debug_zval('node1'); // node1: (refcount=2, is_ref=1)=class Node {  }
xdebug_debug_zval('node2'); // node2: (refcount=2, is_ref=1)=class Node {  }

Nesse exemplo, além de ambas as variáveis apontarem pro mesmo objeto, temos explicitamente uma referência, observe que o valor da flag is_ref agora é 1. Isso quer dizer que, $node1 e $node2 apontam para o mesmo zval.

A diferença, a princípio, parece sutil, mas tentemos ver isso na prática. Primeiro um exemplo onde explicitamente pedimos para receber um valor por referência:

<?php

$a = 1;

function change(&$a) {
    $a = 2;
}

change($a);

echo $a; // 2

A alteração dentro do escopo da função refletiu no mesmo container interno de valor (zval) da variável $a do escopo do script. Como era esperado.

Agora a dúvida é: se objetos são sempre passados por referência (como é o senso comum), não precisamos usar o operador de referência para receber um objeto nessa mesma função change(), certo?

Vejamos:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change($node) {
    $node = 10;
}

change($node);

var_dump($node); // class Node#1 (0) { }

Pois bem, se os objetos realmente fossem passados por referência, o valor da variável $node deveria ser 10 e não uma instância da classe Node. Ou seja, quando um objeto é passado para o argumento de uma função/método ou quando é atribuído em outra variável, não temos passagem por referência, o que fica caracterizado é que essas variáveis apenas estão apontando para a mesma estrutura de dado referente ao objeto, o que dá a sensação que está acontecendo passagem por referência.

O que causa essa confusão é esse comportamento:

<?php

class Node
{
    public int $count;
}

$node = new Node();
$node->count = 1;

function change(Node $node) {
    $node->count = 2;
}

change($node);

echo $node->count; // 2

O resultado é 2, mesmo sem passagem por referência, por causa do comportamento de estarmos trabalhando com o mesmo objeto em memória.

O exemplo abaixo demonstra que estamos lidando com duas estruturas (zval) diferentes, mas que ambas referenciam a mesma estrutura de dado do objeto criado:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    xdebug_debug_zval('node'); // node: (refcount=2, is_ref=1)=class Node {  }
}

change($node);

xdebug_debug_zval('node'); // node: (refcount=1, is_ref=0)=class Node {  }

Veja que dentro do escopo da função já ficou caracterizado que existe referência, mas fora dela, não. Ou seja, se $nodeFoo dentro de change() fosse alterada para 10, refletiria no mesmo zval de $node. Veja:

<?php

class Node
{
    // TODO
}

$node = new Node();

function change(Node $node) {
    $nodeFoo = &$node;

    $nodeFoo = 10; // Reflete no mesmo zval de $node

    var_dump($node); // int(10)
}

change($node);

var_dump($node); // class Node#1 (0) { }

Agora, a história muda quando explicitamente recebemos a variável por referência:

<?php

class Node
{
    // Todo
}

$node = new Node();

function change(&$node) {
    $node = 10; // Reflete no mesmo zval da variável $node fora desse escopo
}

change($node);

var_dump($node); // int(10)

Por fim, um exemplo de variáveis que não se referenciam, mas que apontam para o mesmo valor interno em memória até que uma operação de escrita seja realizada, aí é feita uma deep-copy:

<?php

$string = str_repeat('FOO ', 4);

xdebug_debug_zval('string'); // string: (refcount=1, is_ref=0)='FOO FOO FOO FOO '

$string2 = $string;

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

$string3 = $string;

xdebug_debug_zval('string'); // string: (refcount=3, is_ref=0)='FOO FOO FOO FOO '

$string3 .= 'BAR';

xdebug_debug_zval('string'); // string: (refcount=2, is_ref=0)='FOO FOO FOO FOO '

Observe que depois que $string3 fez uma operação de escrita o refcount do valor de $string foi decrementado.

Outro exemplo genérico:

<?php

class Foo {
    public $index;
}

$a = new Foo();
$b = $a;
$c = &$b;

$c->index = 10;

echo "({$a->index}, {$b->index}, {$c->index})" . PHP_EOL; // (10, 10, 10)

$c = "Hello";

echo "({$a->index}, {$b}, {$c})"; // (10, Hello, Hello)

Na prática temos:

Todos os símbolos apontam pro mesmo objeto, no entanto, $c é referência de $b, alterações feitas em $c refletirão na mesma zval de $b, mantendo $a intacta.

Em que momento devo usar referências?

As funções de sorting de arrays do PHP os recebem por referência, por exemplo:

sort ( array &$array [, int $sort_flags = SORT_REGULAR ] ) : bool

Sempre que você ver na assinatura de uma função &$var, significa que está recebendo por referência e que, portanto, vai trabalhar na mesma zval da variável informada.

E usar referências não é sobre performance, dependendo do uso, pode piorá-la. Usar referências é mais sobre lidar melhor com a memória, como no caso da função acima, não faria sentido duplicar o array inteiro só para aplicar o algoritmo de sorting.

Mas, no geral, a regra é que você dificilmente vai usar com frequência operações com referências. Usa-se referências quando realmente existe um objetivo muito bem definido, portanto, não se preocupe em querer “otimizar” o seu código pra usar referências, pois é bem possível que você não precisa delas.

O que mais posso fazer com referências?

Você pode ver todas as sintaxe possíveis para se usar referências no PHP através desse link da documentação oficial.

Palavras finais

Sumarizando o que foi visto nesse artigo:

  • PHP usa a semântica de valor;
  • Objetos não são passados por referência;
  • Valores complexos como arrays e strings são apenas duplicados quando alterados (usa-se o princípio copy-on-write);
  • Referências não deixam o código mais performático, se mal utilizadas, podem é deixar mais lento, devido aos vários apontamentos que precisam ser feitos. Referências são usadas em casos bem específicos em que o desenvolvedor tem a exata noção do comportamento que ele espera ter;
  • O desenvolvedor não precisa na maior parte do tempo se preocupar com memória (a não ser que vá trabalhar com grandes data sets, mas aí pode-se estudar o uso de generators no PHP ou alguma outra estratégia);

Não é nada fácil entender a figura completa desse conjunto de pecinhas, mas caso você tenha interesse em se aprofundar, recomendo a leitura dos artigos:

Até a próxima!

Desenvolvedor PHP Sênior
Formação: Desenvolvedor PHP Sênior
Nesta formação você aprenderá aspectos avançados da linguagem PHP e de seu ecossistema, conhecimentos essenciais para se tornar um desenvolvedor diferenciado, e com isso, você terá conhecimento para desenvolver aplicações PHP usando as práticas mais modernas do mercado.
CONHEÇA A FORMAÇÃO

O funcionamento do garbage collector no .NET

A CLR – o ambiente de execução do .NET Framework – é um ambiente gerenciado e, por isso, não precisamos nos preocupar diretamente com questões relacionadas a liberação de memória. Isso é possível por causa de um componente importantíssimo da CLR: trata-se do garbage collector. O garbage collector tem exatamente a responsabilidade de lidar com a liberação da memória associada a uma variável e/ou objeto quando este não é mais utilizado em nenhum ponto do nosso código.

Entender como o garbage collector funciona no .NET é essencial para que possamos entender como um todo como a CLR funciona. Questões relacionadas ao garbage collector também são muito úteis quando precisamos escrever código de alta performance para aplicações que exigem respostas próximas ao real-time “de verdade”. Por isso, vamos entender como o garbage collector funciona na CLR.

Como funciona o gerenciamento de memória no .NET?

No artigo C# Gerenciamento de memória no C#: stack, heap, value-types e reference-types que escrevi aqui para o blog da TreinaWeb, apresentei as duas principais divisões da área de memória durante a execução de uma aplicação .NET: a stack, para os value-types; e a heap, para os reference-types. Dependendo do tipo de “dado” que você aloca em uma variável, o dado pode ser deslocado ou para a stack – se estivermos falando de structs – ou para a heap – se estivermos falando de tipos ditos “complexos”, como objetos.

Desenvolvedor C# Sênior
Formação: Desenvolvedor C# Sênior
A formação Desenvolvedor C# nível Sênior da TreinaWeb tem como objetivo apresentar conceitos avançados do C#, como covariância, contravariância e invariância de interfaces; utilização extensiva de delegates, expressões lambda e métodos de extensão. Por fim, o LINQ e o Entity Framework, ferramentas importantíssimas e essenciais no .NET, são abordadas e estudadas.
CONHEÇA A FORMAÇÃO

Quando falamos sobre o garbage collector, é importante frisar que este atua organizando a memória na heap, embora ele utilize as referências guardadas na stack para determinar o que ainda está em uso ou não. Os objetos que são alocados na heap acabam sendo divididos em três grupos, também chamados de gerações:

  • geração 0: é a primeira geração onde um objeto é alocado. Assim que algo é alocado na heap,o objeto alocado é colocado imediatamente como sendo parte da geração 0;
  • geração 1: trata-se de uma geração de transição, onde objetos que são utilizados de maneira “média” são alocados;
  • geração 2: é uma geração que contém objetos que são utilizados por mais tempo e que, por isso, precisam existir na memória por um tempo maior.

O que o garbage collector faz basicamente é migrar os objetos entre estas três áreas distintas e eliminar as áreas de memória associadas a objetos que não são mais utilizados.

O ciclo de trabalho do garbage collector na heap

O garbage collector trabalha em ciclos de análise sobre as gerações que existem na heap. O que diferencia o funcionamento do garbage collector sobre as gerações é a peridiocidade da inspeção: a geração 0, por ser menor na maioria das vezes, sofre análises do garbage collector mais frequentes do que as gerações 1 e 2 por exemplo. Nestas análises, o que o garbage collector faz é verificar se os integrantes das gerações estão ainda sendo utilizados e, caso algum integrante não seja mais necessário, este é removido, fazendo com que a área de memória correspondente seja liberada e fique disponível para novas alocações.

É importante notar também que os ciclos de análise ocorrem na geração que é alvo do ciclo e nas gerações anteriores. Por exemplo: se o garbage collector precisa analisar a geração 0, somente ela é analisada. Se o garbage collector precisa analisar a geração 1, as gerações 1 e 0 são analisadas. Se o garbage collector precisa analisar a geração 2, as gerações 2, 1 e 0 são analisadas. Em decorrência desse funcionamento, o ciclo de análise na geração 2 ganha o nome de coleta completa, pois todas as gerações acabam sendo analisadas.

Essa análise do garbage collector pode ocorrer em situações pré-definidas:

  • Quando o sistema operacional informa que possui pouca memória física disponível;
  • O tamanho das gerações é estourado;
  • O método GC.Collect() é invocado, o que caracteriza uma chamada explícita para o processo de análise do garbage collector dentro da aplicação.

Promoções de geração em objetos

Os objetos são inicialmente alocados na geração 0 – portanto, na geração que sofre coletas mais rápidas – porque a CLR supõe que estes objetos não serão mais necessários muito rapidamente, o que faria com que estes objetos fossem removidos rapidamente da memória. E isso é verdade para a maioria dos cenários. Porém, alguns objetos podem precisar sobreviver por mais de um ciclo de análise do garbage collector… Quando o garbage collector detecta um objeto que precisa sobreviver por mais tempo na memória do que o esperado para a geração onde ele se encontra, o objeto sofre o que é chamado de promoção: o objeto é deslocado para a geração superior, sofrendo ciclos mais espaçados de análise do garbage collector. Por exemplo: se um objeto que está na geração 0 sobrevive a um ciclo de análise, o mesmo é deslocado para a geração 1. Se um objeto sobrevive a um ciclo de análise sob a geração 1, o mesmo é deslocado para a geração 2, sofrendo a análise do garbage collector de maneira mais espaçada ainda.

Esse processo de promoção pode tornar a gerência de memória um processo muito lento, já que esse deslocamento de gerações pode exigir uma quantidade de processamento computacional considerável, dependendo da quantidade de objetos alocados… Por isso, o CLR sempre está “supervisionando” o trabalho do garbage collector. Se o garbage collector passa a detectar que a taxa de sobrevivência de objetos em uma determinada geração é muito alta, a CLR aumenta o tamanho da geração em questão, evitando que as gerações tenham seu tamanho estourado frequentemente. A CLR sempre tenta equilibrar os ciclos de análise do garbage collector e o tamanho das gerações – as gerações também não podem ser sempre expandidas, pois isso deixaria o sistema operacional sem memória em um curto espaço de tempo.

Áreas efêmeras

O CLR e o garbage collector supõe que os objetos que fazem parte das gerações 0 e 1 terão um ciclo de vida muito curto, sendo eliminados rapidamente da memória. Por isso, estas gerações também são chamadas de gerações efêmeras.

As gerações também costumam ser agrupadas em segmentos de memória, segmentos estes que são gerenciados pelo garbage collector. Segmentos que possuem as gerações 0 e 1 também são chamados de segmentos efêmeros.

O garbage collector também realiza a manipulação destes segmentos. Os segmentos recém-criados também são definidos como segmentos efêmeros. Quando o garbage collector cria mais um novo segmento, o segmento que antes era efêmero deixa de ter essa característica, passando a ser considerado um segmento de geração 2 (a geração com objetos que sobrevivem por mais tempo). O novo segmento passa a ser considerado neste momento como o segmento efêmero.

No próximo artigo, iremos analisar em detalhes como o garbage collector realiza o trabalho de remoção e promoção de objetos entre os segmentos gerenciados pelo garbage collector.

Ponteiros em C, uma abordagem básica e inicial

Nesse artigo vou falar um pouco sobre o terror dos estudantes de programação, a verdadeira bruxa que come criancinhas em noite de lua cheia … Vou falar de ponteiros.

Mas calma, não se assuste! Vamos ver que esse assunto não é nenhum bicho de sete cabeças.

Pense em ponteiros como sendo aquele colega de trabalho “sacana” que não sabe nada a não ser apontar para você quando alguma pergunta é feita para ele. Quando seu chefe pergunta qualquer coisa o seu colega aponta para você responder, afinal é você quem têm na memória as informações.

Com ponteiros ocorre algo bem parecido, vamos ver uma explicação um pouco mais técnica sobre esse assunto.

Podemos dizer que ponteiros ou apontadores (também podemos nos referir a eles desta forma), são variáveis que armazenam endereços de memória.

Mas claro, não é qualquer endereço de memória, nossos ponteiros armazenam endereços de outras variáveis.

Então, veja que aquilo que chamamos de “apontar” na realidade é simplesmente a denominação que damos a um ponteiro que contém o endereço de uma variável qualquer (e de qualquer tipo).

Agora você deve estar se perguntando:

Por que devo aprender isso, qual é o grande benefício?

É simples: ponteiros são muito úteis quando temos uma situação em que uma variável precisa ser acessada em diferentes partes do programa.

Em um caso como esse o código pode ter vários ponteiros em diversas partes do programa apontando para uma variável específica.

E o melhor de tudo é que se o dado que estiver no local de memória apontado sofrer alguma alteração, não vai ter problema, pois os ponteiro espalhados no programa apontam para o endereço de memória e não exatamente para o valor.

Deu pra perceber como o uso de ponteiros ajuda o programador? Dificilmente você vai escrever um código com menos do que algumas dezenas até centenas de páginas e poder usar vários ponteiros em uma aplicação desse tipo é mais que uma mão na roda.

C Básico
Curso de C Básico
CONHEÇA O CURSO

Afinal como declaro um ponteiro?

Depois de ver como um ponteiro pode melhorar sua qualidade de vida, você deve estar se perguntando como declarar uma maravilha dessa em seus códigos.

É simples. A sintaxe de um ponteiro é a seguinte:

tipo * nome_Ponteiro;

No exemplo acima temos o tipo que é o tipo de dado da variável que vamos apontar, podendo ser int, float ou até mesmo uma struct.

Depois temos o * (asterisco) que nesse caso determina que a variável é um ponteiro. E por fim temos “Nome_Ponteiro” que, como o próprio nome diz, é o nome do ponteiro.

Seguindo esses passos teremos a declaração de um ponteiro como o apresentado abaixo:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int * ptr;

    return EXIT_SUCCESS;
}

Mas claro, isso não é o suficiente para que possamos usar um ponteiro, a única coisa que fizemos foi declarar um ponteiro e nada mais.

Agora precisamos atribuir a ele um endereço de memória de uma variável qualquer do tipo int. Para fazer isso é necessário que criemos essa variável. Por exemplo:

int valor = 10;

Depois disso teremos o endereço de memória que será atribuído a nosso ponteiro, mas é claro essa atribuição não é simples. Ela precisa ser diferenciada e isso é feito usando o & (E comercial), com esse caractere conseguimos atribuir o endereço de memória de uma variável a um ponteiro.

Veja a sintaxe:

ponteiro = &valor;

Bem simples não é mesmo? Então vamos misturar tudo isso em um código para ver no que vai dar.

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int * ptr;
    int valor = 10;

    ptr = &valor;

    printf("Endereço = %x", &valor);
    printf("Endereço = %x", ptr);
    printf("Valor = %d", *ptr);

    return EXIT_SUCCESS;
}

Exemplo de um possível output para essa execução:

Endereço = 5015936c 
Endereço = 5015936c 
Valor = 10 

Veja que no código acima temos a estrutura de um código em linguagem C e nele criamos uma variável do tipo int chamada valor a quem atribuímos o valor 10.

int valor = 10;

Depois declaramos nosso ponteiro ptr e atribuímos a ele o endereço da variável valor.

int * ptr;

ptr = &valor;

Veja bem, ponteiros só aceitam endereços de memória. Não adianta tentarmos atribuir algum valor primitivo, por exemplo.

E para se obter o endereço de uma variável usamos o operador &. Foi o que fizemos.

Feito isso usamos printf() para exibir o valor do endereço da variável valor:

printf("Endereço = %x", &valor);
printf("Endereço = %x", ptr);

Se o ponteiro ptr está armazenando o endereço da variável valor, então quando imprimirmos o ponteiro ptr teremos o mesmo resultado (o mesmo endereço) que imprimir &valor (que retorna o endereço de memória da variável), não é verdade? Pois bem, foi isso que aconteceu. Viu o resultado da execução que mostramos ali em cima?

Endereço = 5015936c 
Endereço = 5015936c 
Valor = 10 

Por ultimo, exibimos o valor que existe na variável valor, tal valor que se acessado pelo ponteiro usamos a sintaxe *ptr.

printf("Valor = %d", *ptr);

Perceba que para acessar o endereço de memória é necessário duas coisas muito importantes:

  • Primeiro: dentro de printf() use %x para exibir o endereço de memória, pois o mesmo se trata de um valor hexadecimal.

  • Segundo: para acessar o endereço de memória de uma variável use & antes dela.

É possível ainda acessar o endereço de memória de um ponteiro e isso nada tem a ver com o endereço de memória da variável, para isso, assim como fizemos com a variável valor, podemos fazer:

printf("Endereço de memória do ponteiro = %x", &ptr);

Então, recapitulando, dentro de um printf() se utilizarmos:

  • ptr estaremos acessando o endereço de memória associado ao ponteiro. Ou seja, o endereço de memória de uma variável.

  • &ptr aí já estaremos acessando o endereço de memória do ponteiro.

Para acessar o conteúdo daquele endereço associado ao ponteiro é necessário mudar um pouco a abordagem.

  • Primeiro: dentro de printf() use %d para que seja possível mostrar um inteiro.
  • Segundo: use o operador * (que nesse caso nada tem a ver com multiplicação, tudo bem?) antes do ponteiro para acessar seu valor: *ptr.

Altere o exemplo para:

printf("Endereço = %x", &valor);
printf("Endereço = %x", ptr);

printf("Valor = %d", *ptr);
printf("Valor = %d", valor);

Reiterando:

  • *ptr – A variável ptr tem o endereço da variável valor, não é? É meio caminho andado para encontrar o valor dela, não acha? E para encontrar esse valor usamos o operador * antes do nome do ponteiro.
  • valor – Estamos explicitamente imprimindo o conteúdo dessa variável valor do tipo inteiro.

É isso aí pessoal! simples, não é mesmo? Mas tudo que parece ser simples pode ser complicado.

Imagine que você queira se aventurar um pouco mais e sair da mesmice de ponteiros simples, se você esta nessa fase da vida, continue lendo …

Ponteiro para ponteiros

Opa. Quem diria, você por aqui? Bom, se chegou até aqui é porque quer se aventurar, né? To sabendo.

Então vamos lá! Para entender ponteiros para ponteiros precisamos de uma situação da vida real.

Imagine que você foi para uma balada e encontrou uma pessoa legal, vocês conversaram e essa pessoa escreveu em um papel velho seu número de telefone. Você pegou o papel, mas como ele está sujo e meio engordurado você resolve pegar um papel limpo e anotar o telefone novamente (e você está sem celular!).

Esse processo pode ser identificado em programação como um ponteiro para ponteiro. O também chamado ponteiro do ponteiro é aquele ponteiro que aponta para outro ponteiro.

Doeu minha cabeça! Vamos ver a sintaxe para ficar mais fácil de entender?

int *ptr;
int **pptr;

Veja que acima declaramos um ponteiro comum com apenas um *asterisco e depois declaramos o ponteiro do ponteiro, que nesse caso, utiliza-se dois ** asteriscos.

Essa é a declaração, já a atribuição é a seguinte:

ptr = &valor;
pptr = &ptr;

Bem simples, enquanto o ponteiro simples aponta para uma variável, o ponteiro do ponteiro aponta para o ponteiro simples.

Vamos misturar tudo isso e ver no que dá:

#include <stdio.h>
#include <stdlib.h>

int main(void) {

    int * ptr;
    int ** pptr;

    int valor = 10;

    ptr = &valor;
    pptr = &ptr;

    printf("Endereço de ptr = %x", &ptr);
    printf("Endereço de pptr = %x", &pptr);

    printf("Valor ptr = %d", *ptr);
    printf("Valor pptr = %d", **pptr);

    return EXIT_SUCCESS;
}

Veja que no código acima declaramos os ponteiros da forma que já foi explicado e suas atribuições também foram feitas.

E no final exibimos os endereços e valores dos ponteiros. Veja que apesar de os ponteiros terem endereços diferentes, o valor apontado é o mesmo.

Muito legal, não é mesmo? Finalizo por aqui. Nos vemos nos próximos artigos.

C Básico
Curso de C Básico
CONHEÇA O CURSO

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200