Quando o ChatGPT me ofereceu uma solução completamente errada
Resumo rápido
Brincando com o ChatGPT, questionei sobre uma implementação utilizando listas em Scala e a resposta oferecida foi totalmente errada. Não só isso, mas a ferramenta defendeu seu uso demosntrando falta de conhecimento (ok... eu sei que é uma ferramenta e não uma pessoa, e vou chegar nesse ponto ao longo do texto... confere aí).
O que é o ChatGPT?
O ChatGPT é uma ferramenta que permite que você converse com um modelo de linguagem natural treinado pela OpenAI. Entre outras coisas, ele é capaz de entender e escrever código em diversas linguagens.
O que aconteceu?
Bom, eu estava brincando com o ChatGPT e quis testar a capacidade dele de entender e escrever código. Como vejo uma situação muito comum em Scala no tratamento de List
, tive essa "conversa":
ricardovm: Em Scala, considerando que eu tenho um List (que é imutável) e eu quero acrescentar um elemento... é mais performático acrescentar no início com o operador :: ou no final da List com :+?
ChatGPT: Em Scala, acrescentar um elemento ao final de uma List é mais performático do que acrescentar no início da lista. Isso ocorre porque as operações de adição no final da lista são mais eficientes do que as operações de adição no início da lista. Para acrescentar um elemento ao final da lista, você pode usar a operação
:+
:(código exemplo omitido)
ChatGPT: Ao contrário, para acrescentar um elemento no início da lista, você pode usar a operação
::
:(código exemplo omitido)
No entanto, é importante lembrar que as operações de adição no início da lista são mais lentas do que as operações de adição no final da lista, portanto, se você precisar adicionar muitos elementos a uma lista e precisar de alta performance, é recomendável usar uma estrutura de dados diferente, como um ArrayBuffer.
O que está errado?
O fato é que a informação gerada pelo ChatGPT está incorreta (ignorando o fato dele tentar mudar para uma implementação mutável como ArrayBuffer
, que ainda assim seria mais lenta.
As List
em Scala funcionam como uma lista ligada imutável, ou seja, cada elemento da lista aponta para o próximo elemento da lista.
Grosso modo, uma List
de um item em Scala é basicamente isso:
// só uma representação simplificada... na real isso geraria uma
// lista com dois elementos: 3 e Nil
List(3, Nil)
Imagine que a lista é composta pelo número 3 e aponta para a lista vazia Nil
... uma lista de um elemento. Como List
é imutável, a forma de eu acrescentar um novo item a ela seria criando uma nova lista com o novo elemento. Então imagine essa representação (mais uma vez, não é assim que se instancia uma lista... é apenas uma representação):
List(2, List(3, Nil))
Voilá! Agora temos uma lista com dois elementos: 2 e 3. E se eu quiser acrescentar um novo elemento? Novamente, eu preciso criar uma nova lista. Então, a representação dessa lista ficaria assim:
List(1, List(2, List(3, Nil)))
Sempre acrescentando ao início da lista. Veja que a lista original nunca é alterada, mas serve de continuação para a nova lista. Cada item acrescentado é um head
e o tail
é apontado para a lista anterior, sem alterar ou apagar nada.
E se eu quiser acrescentar um item ao final da lista? Como ela é imutável, eu não poderia simplesmente trocar o tail
do último elemento para apontar para a nova lista ou item.
List(1, Nil)
Não pode ser alterado. Então eu teria que criar uma nova lista:
List(1, List(2, Nil))
E assim por diante. O que significa que, para acrescentar um item ao final da lista, eu
preciso recriar cada elemento da lista até o final para poder apontar para o novo item...
Aquela primeira lista ainda existe... está lá na memória e em algum momento vai ser eliminada pelo garbage collector, mas ainda está lá e não pôde ser aproveitada.
Dá até um nó só de explicar, mas se você imaginas o trabalho que dá para a máquina criar uma lista de 100.000 elementos acrescentando os itens no final dela, você vai entender o tamanho do problema.
Aprofundando ainda mais o problema
Como isso ficou me perturbando, no dia seguinte resolvi ser mais claro com o ChatGPT e
perguntei:
ricardovm: Qual destes dois códigos em Scala é mais performático ?
object Main { def main(args: Array[String]): Unit = { val inicio = System.currentTimeMillis() val lista = (1 to 100000) .foldLeft(List.empty[Int]) { (acc, x) => x :: acc } val resultado = lista.reverse val fim = System.currentTimeMillis() println(s"Tempo: ${fim - inicio} ms") println(resultado.length) } }
ou
object Main { def main(args: Array[String]): Unit = { val inicio = System.currentTimeMillis() val lista = (1 to 100000) .foldLeft(List.empty[Int]) { (acc, x) => acc :+ x } val resultado = lista val fim = System.currentTimeMillis() println(s"Tempo: ${fim - inicio} ms") println(resultado.length) } }
Dá uma olhada na resposta:
ChatGPT: Ambos os códigos criam uma lista de 100000 inteiros. No primeiro código, a lista é construída usando o método
foldLeft
, adicionando os novos elementos no início da lista (com o operador::
). No segundo código, a lista é construída usando o mesmo métodofoldLeft
, adicionando os novos elementos no final da lista (com o operador:+
).
Impressionante como ele conseguiu interpretar o código! Mas segue o restante da resposta:
ChatGPT: O desempenho dos dois códigos dependerá do tamanho da lista e da capacidade de processamento do computador em que o código estiver sendo executado. No entanto, em geral, espera-se que o segundo código seja mais performático, pois a adição de elementos no final de uma lista é geralmente mais eficiente do que a adição no início. Isso se deve ao fato de que, ao adicionar um elemento no início de uma lista, todos os outros elementos da lista precisam ser deslocados para a direita para fazer lugar para o novo elemento. Já ao adicionar um elemento no final da lista, esse deslocamento não é necessário.
Para ter uma ideia mais precisa do desempenho dos dois códigos, você pode medir o tempo de execução de cada um e comparar os resultados. Além disso, é sempre importante lembrar que o desempenho de um código pode ser afetado por muitos fatores diferentes, como a implementação de linguagem, a configuração do sistema e a carga de trabalho atual do computador. Portanto, é importante avaliar o desempenho de um código em contexto e levar em consideração esses fatores ao avaliar a eficiência de uma implementação.
Resposta interessante e até que parece bem embasada, mas que está fundamentalmente errada!
Provando que o ChatGPT está errado
Se você rodar os dois códigos, vai ver que o primeiro é mais performático, acrescentando itens ao início da linha e invetendo ela no final é muito mais rápido e performático em todos os sentidos.
Código | Tempo | Operações de GC |
---|---|---|
Acrecentando no início | 83 milissegundos | 326 |
Acrescentando no final | 19 segundos | 400.000 |
Teimando com o ChatGPT
Com esses resultados, resolvi cutucar o nosso amigo ChatGPT novamente:
ricardovm: O segundo código rodou em 18 segundos e o primeiro rodou em 83 milissegundos.
E a resposta:
ChatGPT: Os resultados que você obteve são bastante diferentes do que eu havia previsto. Isso pode ser explicado por diferenças nas configurações do sistema ou na carga de trabalho atual do computador, ou por outros fatores que podem afetar o desempenho dos códigos.
Apesar disso, é importante observar que o segundo código demorou muito mais tempo do que o primeiro para executar. Isso pode ser devido a alguma otimização que o compilador ou o interpretador da linguagem estejam fazendo no primeiro código, ou a algum outro fator.
Uma maneira de avaliar o desempenho de um código é medir o tempo de execução de diferentes implementações e comparar os resultados. Isso pode ajudar a identificar quais trechos de código estão consumindo mais tempo e onde é possível fazer otimizações. Além disso, é sempre importante lembrar que o desempenho de um código pode ser afetado por muitos fatores diferentes e que é preciso levar esses fatores em consideração ao avaliar a eficiência de uma implementação.
Continua errando...
Conclusão
Muito se fala de as ferramentas de inteligência artificial serem capazes de substituir programadores. Eu as vejo como... ferramentas que podem (e devem) ser usadas por nós, desenvolvedores. Mas nunca poderemos abrir mão do conhecimento.
Fico imaginando alguém que procura uma resposta em uma IA como essas e sai usando o resultado, sem sequer conferir se está, de fato, correto. É capaz de querer dicutir com quem discorda. Fico imaginando pessoas abrindo mão do conhecimento
e delegando isso a IAs.
No fundo, acho que ainda vão continuar existindo dois tipos de desenvolvedores. Hoje mesmo, muitos abrem mão do conhecimento e vivem de copiar e colar do Google/Stackoverflow. Esses são os que vão conseguir melhorar seus códigos usando IA, mas ainda serão os mesmos copiadores de código. E tem aquele que usa Google e Stackoverflow para entender melhor um problema ou para buscar uma ajuda pontual em algo que não conhece ou mesmo não lembra naquele momento, mas que tem o conhecimento teórico necessário para desenvolver bem, e que vai saber aproveitar ainda mais os resultados gerados por IA. IA é uma ferramente e, como tal, deve ser vista... está a serviço de pessoas, não o contrário. Mas, sem dúvida, uma ferramenta com um enorme potencial.