Entre para a LISTA VIP da Black Friday

00

DIAS

00

HORAS

00

MIN

00

SEG

Clique para saber mais
Alura > Cursos de Programação > Cursos de C e C++ > Conteúdos de C e C++ > Primeiras aulas do curso Avançando com C++: performance e otimização

Avançando com C++: performance e otimização

Strings - Apresentação

E aí, pessoal! Sejam muito bem-vindos à Alura! Eu sou o Vinícius Dias e vou guiar vocês por mais um treinamento de C++. Nesse treinamento nós vamos avançar um pouco nos nossos conhecimentos, aprendendo um pouco sobre técnicas de performance e otimização.

Vamos falar sobre alguns recursos mais avançados, que se tudo der certo, não precisaremos implementar, mas quando precisarmos, será muito interessante saber.

Nesse treinamento vamos começar falando sobre strings, porque elas são muito utilizadas. Tanto em projetos simples, como os que fazemos nos treinamentos, ou em projetos muito complexos - como em criação de jogos, GameRanger e etc.

Então vamos começar aprendendo a sobrescrever o operador new para sabermos quantas alocações de memória estão acontecendo. Vamos ver não só como manipular uma string através de substring, pegar seu tamanho e encontrar caracteres mas vamos aprender a poupar memória e evitar a locação através de outra classe.

Vamos conhecer a famosa small string optimization e vamos ir evoluindo com isso. Depois vamos falar sobre cópia de valores. Vamos implementar copy constructors, vamos entender o que é e para que serve. Depois começaremos uma parte de teoria, falando sobre L value, R value e referências para esses 2 tipos de valores.

E no final vamos falar sobre move semantics. Vamos evitar cópias de dados através de move constructors. No final de tudo, eu vou deixar alguns textos, porque é importante ler também e não só ouvir minha voz, para que vocês entendam sobre as regras.

Regra do 0, regra de 3 e regra de 5. Sobre quando falamos de gerenciamento de recursos - como ponteiros, memória no geral em C++. Vai ser um conteúdo um tanto quanto teórico, mas muito importante e interessante.

Então espero que você aproveite bastante. Se ficar com alguma dúvida, algum problema, alguma questão ou algo que você não entendeu no meio do processo, não hesite, abra uma dúvida no fórum, que eu tento responder pessoalmente sempre que possível. Quando eu não consigo, a nossa comunidade de alunos, moderadores e instrutores é muito solícita e com certeza alguém vai conseguir te ajudar!

Então te espero no próximo vídeo para nós começarmos a colocar a mão na massa!

Strings - Verificando alocações

E aí, pessoal! Bem-vindos de volta! Antes de aprendermos recursos avançados e técnicas de otimização muito complexas, vamos falar de coisas simples.

Durante o desenvolvimento do treinamento anterior e de qualquer um dos outros treinamentos, utilizamos bastante strings. Para definirmos nossos testes, os nomes dos usuários e a descrição do leilão.

Ou seja, string é algo amplamente utilizado. Não só nos nossos projetos de exemplo, que são bem simples e super simplificações do mundo real, mas também no mundo real. No mundo real usamos até mais strings! Então vamos conversar sobre isso, porque eu vou te dar um spoiler.

Quando utilizamos o tipo string que está naquele cabeçalho string, normalmente estamos falando de alocação de memória na heap. Não sempre, como vamos ver, mas muitas das vezes.

E como já vimos também em treinamentos anteriores, alocar memória na heap é bastante custoso. Então, sempre que pudermos evitar, é uma boa coisa.

Então vamos entender como funciona a string e um pouco melhor a alocação de memória dela, mas para isso, eu vou remover os nossos testes, para nós não precisarmos ficar rodando um monte de códigos e podermos focar no que realmente importa - que é essa parte do projeto.

Então vou vir no menu lateral da tela “Leilao > leilao > tests > Botão direito do mouse > Delete” eu só vou remover a referência, eu não vou mover essa pasta para o lixo. Então essa pasta continua existindo, mas não faz mais parte do meu projeto.

Então, vamos lá! Eu vou criar fora um novo arquivo, eu vou chamar ele de “main.cpp”. Então vai ser o nosso arquivo “main.cpp”, onde vamos fazer nossas brincadeirinhas para conseguirmos entender como as coisas funcionam. Vou criar na raiz. Vamos nessa!

Vou incluir, como sempre, o #include <iostream>, porque nós vamos precisar exibir algumas coisas. Como eu vou trabalhar com strings, vou incluir também #include <string>.

Então vamos criar nossa int main() {! O que eu quero fazer é analisar quanta memória é alocada, ou pelo menos quantas vezes acontece a alocação de memória heap. Então, como posso fazer isso? Eu quero saber quantas vezes o operador new é chamado.

Se eu quero verificar de alguma forma como funciona um operador, eu posso sobrescrever esse operador, como já vimos em aulas anteriores de sobrecarga de operadores. Então, vamos lá!

Eu vou sobrescrever o operador new. Ele devolve o endereço de memória, então é um ponteiro para qualquer coisa, é um ponteiro para void. É um operador, então eu uso o operator e o nome dele é newvoid* operator new.

E o que ele recebe por parâmetro? Um tamanho de bytes, ou seja, quanto de memória ele vai alocar. Vamos nessa! Eu preciso retornar esse endereço. Como eu aloco memória e pego esse endereço sem ser com o operador new? Eu poderia utilizar o operador new sem ser esse aqui sobrescrito, mas o que eu vou fazer é chamar diretamente a função malloc, ou seja, alocação de memória.

Então sobrescrevemos o operador new, então sempre que uma nova alocação de memória acontecer, essa nossa função vai ser chamada. Então eu vou exibir uma mensagem std::cout << “Alocando” << bytes << “bytes” << std::endl;. Então vai exibir alocando 32 bytes, alocando 64 bytes. Sempre que uma locação na heap acontecer, essa mensagem vai ser exibida.

Então agora eu vou executar esse projeto sem nada, só para você ver um negócio bastante interessante. Se eu não tiver dado, nenhuma bobeira, a nossa compilação vai rodar com sucesso. Está rodando!

Agora assim! Demorou um tempo, como já comentei em cursos anteriores, XCode leva um tempo para compilar, mas tudo bem. Repare quanta alocação de memória está acontecendo, sendo que eu nem tenho código ainda!

Isso aqui não é um problema e nem um erro, é que a forma com que o XCode compila, o compilador provido pelo XCode, adiciona muita coisa no nosso código em modo de desenvolvimento.

Então durante o processo de desenvolvimento, ele adiciona muito código para nos ajudar a debugar código, a entender a stack trace, a entender a pilha de execução do nosso código...

Então ele adiciona muitas coisas para nos ajudar, para evitarmos que estouremos memória e que acessemos memória de outros programas. Então é por isso que tem tantas alocações acontecendo. Isso é normal.

Para conseguirmos focar no que vai ser alocado pelos nossos exemplos, eu vou adicionar também um std::cout <<”____________________” << std:: endl;, de uma linha dividindo as coisas.

Entrou no nosso código executor e isso vai ser exibido. Então tudo que vier depois, eu sei que vai ser a alocação do nosso código. Então vamos nessa! Vamos rodar agora o build um pouco mais rapidamente, as coisas acontecem mais rapidamente.

Então, a partir do momento que entra efetivamente no nosso código, nós não temos mais alocação de memória. Esse é o esperado porque ainda não temos código nenhum.

Então agora que já nós entendemos como verificar a quantidade de memória que está sendo alocada e quando está sendo alocada, no próximo vídeo vamos efetivamente falar sobre strings!

Strings - Small String Optimization

Atenção! No minuto 02:24, o instrutor diz: "E agora, olhe só, eu aloquei 752 bytes, mais de meio MB só para armazenar um parágrafo." . Na verdade, estamos falando de 752 bytes, sendo mais de meio KB, ao invés de MB.

E aí, pessoal! Bem-vindos de volta! Como eu falei, nós queremos entender melhor como funciona a alocação de memória quando trabalhamos com strings. Então, vamos nessa!

Vamos trabalhar com o meu nome completo. Então eu vou ter uma string, que vai ser meuNomeCompleto. Nem preciso colocar uma variável tão grande. Vamos nessa!

Meu nome completo, caso você não saiba, é Carlos Vinícius dos Santos Dias. Então, o que acontece? Eu estou armazenando nessa variável do tipo string. Esse tipo de string é um tipo que faz bastante coisas por baixo dos panos, esse valor ”Carlos Vinícius dos Santos Dias”.

Então vamos entender primeiro o que é esse valor na prática. Esse valor sozinho não é uma string, esse valor sozinho é um ponteiro para caractere. Então o meuNomeCompleto é basicamente um char*.

Só que normalmente quando estamos trabalhando com ponteiro para caractere, trabalhamos com ponteiro constante; porque strings por padrão são imutáveis, via de regra não modificamos string.

Então existem cenários onde podemos tentar burlar isso, mas strings são armazenadas em um espaço de memória de somente leitura, então textos assim são armazenados em locais onde normalmente não podemos alterar.

Mas entendido isso, não vamos entrar em muitos detalhes sobre ponteiro de caracteres e literais de string, nós vamos falar sobre o que normalmente utilizamos, que é a classe string.

Então essa classe string tem um construtor que recebe um ponteiro de caractere e faz o que ela precisa fazer para armazenar isso, de forma que nós conseguimos manipular.

Por exemplo: pegar uma parte dessa string, uma substring dela. Então conseguimos manipular uma string graças a essa classe. Só que como eu disse, essa classe realiza alguns trabalhos. Então quando eu executar isso, vamos ver que acontece.

Executei, e depois que o nosso código começou, tivemos uma alocação de 32 bytes. Então conforme uma string vai crescendo, ela precisa armazenar mais espaço de memória. Então vamos tentar fazer isso ficar grande. Eu vou copiar meu nome e colar várias vezes. Vamos ver se isso faz ela alocar mais do que 32 bytes. Vou executar. Fez o build.

E agora, olhe só, eu aloquei 752 bytes, mais de meio MB só para armazenar um parágrafo. Então, repare que strings consomem bastante memória. E se por algum motivo o que estivermos fazendo é algo que sabemos que não vai manipular?

Por exemplo: se eu sei que meu nome completo só vai ser utilizado para exibir esse meu nome completo. A única coisa que eu vou fazer é exibir esse dado ou passar por parâmetro, mas sem realizar manipulações nesse valor.

Ou seja, sem precisar saber o tamanho disso e pegar uma parte dessa string, sem precisar fazer nada com ela - além de usá-la como ela é. Então nós podemos utilizar um ponteiro para char: const char*.

Isso aqui ajuda um pouco nosso trabalho. Ele está dizendo que eu não estou usando essa variável, então nós vamos exibir: std::cout << mewNomeCompleto << std::endl;.

Então se não vamos manipular essa string, podemos utilizar diretamente um ponteiro para caractere constante; sempre constante. Repare que a partir disso eu não tenho mais alocação de memória.

Só que nem sempre podemos nos dar esse luxo, às vezes precisamos manipular a string. Então eu quero te mostrar uma otimização que a própria STL, ou seja, a Stender Template Library do C++, que a própria classe string faz para nós.

Se tivermos uma string pequena suficiente para ser seguro armazenar na Stack - ou seja, isso não é uma string muito grande, a própria classe string já aloca esse espaço de memória na Stack e não faz nenhuma alocação na heap.

Então, por exemplo: se ao invés do meu nome completo, eu tivesse armazenando apenas Vinícius Dias, que é uma string bem menor. Repare que mesmo utilizando o tipo string, mesmo construindo um objeto do tipo string, o que vai acontecer? Quando eu faço o meu build, nenhuma memória é alocada na heap, nós continuamos alocando na Stack.

Ou seja, isso é o que conhecemos como small string optimization, ou como é bastante conhecido também, SSO. Existem muitos estudos sobre isso, só que o que nós precisamos saber é que esse template de classe string é inteligente suficiente para entender que se uma string é pequena o suficiente para caber em uma Stack. Ela vai fazer isso sem nos preocuparmos.

E quanto é pequeno o suficiente? 15 caracteres? 16 caracteres? 20 caracteres? E a resposta, como para muitas coisas na computação, é: depende! Depende da arquitetura para qual você está compilando. No meu caso eu estou compilando para arquitetura x64, ou seja, 64 bits. Depende do compilador que você estiver utilizando. Se eu não me engano, o XCode utiliza o Clang.

Existem IDE's que utilizam o G++, existem IDE's que utilizam o compilador da Microsoft... Enfim, existem diversos compiladores e cada compilador possui uma implementação. Então depende de vários fatores: o mesmo compilador, a mesma arquitetura, talvez por algum motivo utilize estratégias diferentes e dependendo também de outros fatores.

Então não vamos saber com certeza quando esse tipo de alocação vai acontecer e quando não vai acontecer. A menos que saibamos exatamente para qual arquitetura vamos compilar e usando qual compilador.

Então é por isso que a performance depende muito do conhecimento da plataforma que nós vamos utilizar. Se eu estou criando um jogo, eu vou compilar para uma plataforma. Se eu estou criando uma aplicação para microcontrolador, eu vou compilar para outra plataforma. Então eu vou entender quando esse tipo de alocação acontece em cada caso e saber trabalhar em cima disso.

Por exemplo: para plataforma de 64 bits, usando esse compilador, que é o Clang, eu acredito que eu tenha 23 caracteres antes de colocar na heap. Então vamos fazer esse teste. No final da linha 12 de “main.cpp” eu vou digitar ”1234567890123456789012”.

Vou ter um string com 22 caracteres. Eu espero, se eu não estiver enganado, que não haja nenhuma alocação... Realmente não houve.

Agora, se eu colocar mais um caractere, eu espero que haja uma alocação na heap, porque a partir de 23 caracteres ele já não é mais uma small string para essa arquitetura. Então se em algum cenário eu sei que a minha string vai ser menor do que 23 caracteres, vou usar string porque é mais fácil, eu consigo manipular tranquilamente.

Agora, se eu vou ter 23 caracteres ou mais e eu não vou manipular essa string, eu posso cogitar a possibilidade de trabalhar com char*, ou seja, um ponteiro para caractere. Porque, dessa forma, mesmo com uma string grande, eu não tenho alocação de memória na heap.

Mais um detalhe é que se nós alocarmos muita memória na Stack, podemos ter um stack overflow, que não é só o nome de um site, é um erro real, onde estouramos limite da Stack.

Então precisamos tomar cuidado e conhecer esses cenários. Via de regra, uma Stack tem 2 MB de memória. Se você tem uma string ocupando 1 ou 2 MB, com certeza você não vai querer colocá-la na Stack; você vai mandá-la para a heap, porque lá nós temos muito espaço.

Então nessa pegada de manipulação de string e de entender como strings funcionam, imagine que voltando para o cenário do meuNomeCompleto, eu queira pegar somente Carlos Vinícius, que eu quero pegar somente o primeiro nome. Imagine que eu queira pegar somente o primeiro nome de uma pessoa, nós conseguimos manipular a string.

Ou melhor, imagine que em uma string eu tenho dois nomes. Como eu pego o primeiro? Como eu faço manipulações de string? E como isso tem haver com performance também? Mas vamos falar sobre todas essas coisas no próximo vídeo!

Sobre o curso Avançando com C++: performance e otimização

O curso Avançando com C++: performance e otimização possui 101 minutos de vídeos, em um total de 45 atividades. Gostou? Conheça nossos outros cursos de C e C++ em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda C e C++ acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas