como muitos desenvolvedores, estou interessado no Rust há algum tempo. Não apenas porque aparece em tantas manchetes em notícias de hackers, ou por causa da abordagem inovadora que a linguagem leva à segurança e ao desempenho, mas também porque as pessoas parecem falar sobre isso com um senso particular de amor e admiração. Além disso, o Rust é de particular interesse para mim porque compartilha alguns dos mesmos objetivos e recursos do meu idioma favorito: Swift. Como recentemente tirei um tempo para experimentar o Rust em alguns pequenos projetos pessoais, queria dedicar um pouco de tempo para documentar minhas impressões sobre o idioma, especialmente em como ele se compara ao Swift.

o quadro geral

Rust e Swift têm muitas coisas em comum: ambos são linguagens compiladas com sistemas de tipos modernos e poderosos e com foco na segurança. Recursos como tipos algébricos e manuseio de primeira classe de valores opcionais ajudam a mover muitas classes de erros do tempo de execução para o tempo de compilação em ambas as linguagens.

então, como esses idiomas diferem? A melhor maneira de caracterizar a diferença é:

o Swift facilita a gravação de código seguro.
Rust torna difícil escrever código inseguro.

essas duas declarações podem soar equivalentes, mas há uma distinção importante. Ambas as línguas têm ferramentas para alcançar a segurança, mas fazem diferentes compensações para alcançá-la: Swift prioriza a ergonomia em detrimento do desempenho, enquanto Rust prioriza o desempenho em detrimento da ergonomia.

o Trade-off: Desempenho vs ergonomia

a maior maneira de demonstrar essa diferença de prioridade é na abordagem que essas linguagens têm para o gerenciamento de memória. Vou começar com o Rust porque a abordagem da linguagem para o gerenciamento de memória é um de seus pontos de venda exclusivos.

no Rust, a memória é gerenciada principalmente estaticamente (sim, existem outros modos de gerenciamento de memória, como contagem de referência, mas vamos ignorá-los por enquanto). O que isso significa é que o compilador Rust analisa seu programa e, de acordo com um conjunto de regras, decide quando a memória deve ser alocada e liberada.

para garantir a segurança, O Rust usa uma nova estratégia chamada verificação de empréstimos. A maneira como isso funciona na prática é que, como programador, toda vez que você passa uma variável (ou seja, uma referência a um local de memória), você deve especificar se a referência é mutável ou imutável. O compilador então usa um conjunto de regras para garantir que você não possa alterar um único pedaço de memória em dois lugares ao mesmo tempo, tornando possível que seu programa não tenha corridas de dados.

esta abordagem tem algumas propriedades muito benéficas em relação ao uso e desempenho da memória. A verificação de empréstimos pode ser muito parcimoniosa com a memória, pois geralmente evita copiar valores. Também evita a sobrecarga de desempenho de uma solução como coleta de lixo, uma vez que o trabalho está sendo feito em tempo de compilação, em vez de tempo de execução.

no entanto, ele vem com algumas desvantagens, tanto quanto facilidade de uso. Devido à natureza da propriedade em Rust, existem alguns padrões de design que simplesmente não funcionam em Rust. Por exemplo, não é trivial implementar algo como uma lista duplamente vinculada ou uma variável global. Isso provavelmente se torna mais intuitivo com o tempo, e há soluções alternativas para esses problemas, mas o Rust certamente impõe limitações ao programador que não estão presentes em outras linguagens.

embora não seja tão falado como Rust, Swift também tem uma história interessante quando se trata de gerenciamento de memória.

Swift tem dois tipos fundamentais de variáveis: tipos de referência e tipos de valor. Em geral, os tipos de referência são alocados em heap e são gerenciados por Contagem de referência. Isso significa que, em tempo de execução, o número de referências a um objeto contado de referência é rastreado e o objeto é desalocado quando a contagem chega a zero. A contagem de referência no Swift é sempre atômica: isso significa que toda vez que uma contagem de referência muda, deve haver uma sincronização entre todos os threads da CPU. Isso tem o benefício de eliminar a possibilidade de uma referência ser erroneamente liberada em um aplicativo multi-threaded, mas tem um custo de desempenho significativo, pois a sincronização da CPU é muito cara.

Rust também tem ferramentas para contagem de referência e contagem de referência atômica, mas estes são opt-in em vez de ser o padrão.

os tipos de valor, por outro lado, são alocados em pilha em geral e sua memória é gerenciada estaticamente. No entanto, o comportamento dos tipos de valor no Swift é muito diferente de como o Rust lida com a memória. No Swift, os tipos de valor têm o que é chamado de comportamento “copy-on-write” são copiados por padrão, o que significa que toda vez que um tipo de valor é gravado em uma nova variável ou passado para uma função, uma cópia é feita.Copiar por padrão atinge alguns dos mesmos objetivos da verificação de empréstimos: como programador, você geralmente nunca precisa se preocupar com uma mudança de valor misteriosamente devido a algum efeito colateral inesperado em outros lugares do programa. Também requer um pouco menos de carga cognitiva do que a verificação de empréstimos, Uma vez que existem classes inteiras de erros de tempo de compilação relacionados à propriedade no Rust que simplesmente não existem no Swift. No entanto, ele tem um custo: essas cópias adicionais requerem uso adicional de memória e ciclos de CPU para serem concluídos.

no Rust também é possível copiar valores como forma de silenciar erros de verificação de empréstimos, mas isso adiciona ruído visual, pois as cópias devem ser explicitamente especificadas.

portanto, aqui temos um bom exemplo das compensações feitas por esses dois idiomas: o Swift oferece algumas suposições amplas sobre como a memória deve ser gerenciada enquanto ainda mantém um nível de segurança. É um pouco como como um programador C++ pode lidar com a memória de acordo com as melhores práticas antes de pensar muito na otimização. Isso torna muito fácil entrar e escrever código sem pensar muito em detalhes de baixo nível, e também alcançar algumas garantias básicas de segurança e correção em tempo de execução que você não obteria em uma linguagem como Python ou mesmo Golang. No entanto, ele vem com alguns penhascos de desempenho, que é fácil cair sem nem perceber até que você execute seu programa. É possível escrever código Swift de alto desempenho, mas isso geralmente requer um perfil e otimização cuidadosos para alcançar.

Rust, por outro lado, oferece muitas ferramentas específicas para especificar como a memória deve ser gerenciada e, em seguida, coloca algumas restrições rígidas sobre como você as usa para evitar comportamentos inseguros. Isso oferece características de desempenho muito boas imediatamente, mas exige que você assuma a sobrecarga cognitiva adicional de garantir que todas as regras sejam seguidas.

minha conclusão disso é que, embora essas línguas tenham alguns objetivos comuns, elas têm características fundamentalmente diferentes que se prestam a diferentes casos de uso. Rust, por exemplo, parece a escolha clara para algo como desenvolvimento incorporado, onde o uso ideal de memória e ciclos de CPU é extremamente importante, e onde o loop code-compile-run pode ser mais lento, por isso é valioso capturar todos os problemas possíveis em tempo de compilação. Considerando que o Swift pode ser uma escolha melhor para algo como ciência de dados ou lógica sem servidor, onde o desempenho é uma preocupação secundária e é valioso trabalhar mais perto do domínio do problema sem ter que considerar muitos detalhes de baixo nível.

em qualquer caso, estarei muito interessado em seguir ambas as línguas no futuro, e seguirei este post com mais observações sobre a comparação entre Swift e Rust.

Deixe uma resposta

O seu endereço de email não será publicado.