Com o aumento da disponibilidade de ambientes no-code/low-code e o advento da IA aplicada à programação, vários artigos foram publicados dizendo que o fim está próximo para os programadores, seja porque os usuários finais criarão seus próprios aplicativos ou a IA escreva aplicativos a partir de descrições. Talvez... mas ainda não.
Neste artigo, exploro o estado atual da codificação assistida por IA comparando as sugestões do OpenAI ChatGPT e do Codex para o Microsoft Copilot com o código escrito à mão. Os AIs produzem código mediano ou defeituoso.
Uma vez que tanto foco hoje é colocado no processamento de grandes volumes de dados (incluindo os grandes modelos de linguagem usados para ChatGPT e Copilot), concentro-me em algoritmos que são essenciais para processar dados com eficiência. Esperançosamente, as IAs “entendem” sua própria natureza melhor do que o código em geral!
Esses algoritmos são:
interseção
produto cruzado
memorizando
Forneci ao ChatGPT as instruções para o formulário:
Mostre-me <alguma descrição de função>
Forneci ao Codex a entrada:
<alguma descrição de função>
Forneci comentários ao Copilot em um WebStorm IDE
// <descrição de alguma função>
As descrições das funções eram as mesmas para cada um.
Usei o Jasmine como o equipamento de teste de unidade complementado por código semelhante ao encontrado no Benchtest para obter velocidade mais memória e utilização da CPU.
Mostre-me uma função JavaScript que recebe um número variável de arrays e retorna sua interseção.
function intersection(...arrays) { if (arrays.length < 2) { throw new Error("At least 2 arrays are required!"); } let result = new Set(arrays.shift()); for (let arr of arrays) { result = new Set([...result].filter(x => arr.includes(x))); } return [...result]; }
function intersection(arrays) { var result = []; var args = Array.prototype.slice.call(arguments); var arrays = args.slice(0, args.length - 1); var callback = args[args.length - 1]; var minLength = Math.min.apply(null, arrays.map(function(arr) { return arr.length; })); for (var i = 0; i < minLength; i++) { var item = arrays[0][i]; var isInAll = arrays.every(function(arr) { return arr.indexOf(item) !== -1; }); if (isInAll) { result.push(item); } } return callback(result); }
const intersection = (...arrays) => { const [first, ...rest] = arrays; return first.filter(x => rest.every(y => y.includes(x))); }
O ChatGPT assumiu que fornecer menos de um array não é válido. Posso imaginar situações em que fornecer um argumento pode ser válido.
Ao usar um Set , o ChatGPT garante que não existam duplicatas nos resultados. A interseção deve ser uma operação definida e as duplicatas devem ser removidas. O código Copilot retornará uma matriz que pode conter duplicatas.
Nem o ChatGPT nem o Copilot classificam os argumentos originais em ordem crescente de comprimento. Esta é uma otimização trivial que pode fazer uma grande diferença!
Se algum argumento tiver comprimento zero, não haverá interseção; independentemente disso, ele encurta os loops porque a interseção máxima é igual ao argumento de matriz mais curto.
O Codex fornecia um código complexo que, com base no meu conhecimento, parecia bastante eficiente. No entanto, seu uso de argumentos é obscuro e envolve um estilo de programação um tanto arcaico.
Observe que um retorno de chamada deve ser fornecido como o último argumento para retornar a interseção ao código de chamada, mas o argumento não é mostrado.
Então, pedi ao ChatGPT e ao Copilot para me fornecer uma função eficiente.
Mostre-me uma função JavaScript eficiente que recebe um número variável de arrays e retorna sua interseção.
function intersection(...arrays) { if (arrays.length < 2) { throw new Error("At least 2 arrays are required!"); } let result = new Set(); for (let item of arrays[0]) { let include = true; for (let i = 1; i < arrays.length; i++) { if(!arrays[i].includes(item)){ include = false; break; } } if(include){ result.add(item); } } return [...result]; }
O Copilot gerou o mesmo código da primeira solicitação.
O ChatGPT também adicionou comentários dizendo que a função não funcionaria como esperado para os objetos, embora provavelmente funcione. Então, pedi uma função que suportasse objetos:
Mostre-me uma função JavaScript eficiente que recebe um número variável de arrays contendo objetos ou primitivos e retorna sua interseção.
function intersection(...arrays) { if (arrays.length < 2) { throw new Error("At least 2 arrays are required!"); } let result = new Set(); for (let item of arrays[0]) { let include = true; for (let i = 1; i < arrays.length; i++) { if(!arrays[i].find(x => JSON.stringify(x) === JSON.stringify(item))){ include = false; break; } } if(include){ result.add(item); } } return [...result]; } This function takes an arbitrary number of arrays as arguments and first checks if at least 2 arrays are passed. Then it uses a nested loop. The outer loop iterates over the first array and the inner loop iterates over the remaining arrays. It compares each item of the first array with each item of the remaining arrays using Array.prototype.find() method and JSON.stringify() to check if the objects are the same. If an item is found in all arrays, it is added to
ChatGPT stringifica objetos para compará-los. Há uma série de problemas com isso:
Dois objetos que têm a mesma representação de string podem ser legitimamente objetos separados.
Mesmo que a stringificação seja válida, ordens de propriedade diferentes resultarão em strings diferentes e os objetos podem estar representando a mesma entidade.
JSON.stringify seguido por uma comparação de string é uma operação lenta.
E agora, o verdadeiro teste! Abaixo, estão os resultados de referência para velocidade e memória usando código gerado por ChatGPT, Codex, Copilot e as duas bibliotecas de interseção mais rápidas disponíveis via NPM, fastArrayIntersect e intersector .
O benchmark cruzou 3 arrays idênticos de 10.000 inteiros cada e um array final de 5.000 inteiros com um tamanho de amostra de 100. Um teste também foi executado para confirmar os conjuntos de funções retornados quando existiam entradas duplicadas nos arrays de origem.
Fonte | duplicados | Pilha Usada | Operações de segurança | CPU do usuário | CPU do sistema |
---|---|---|---|---|---|
ChatGPT | Não | 39768 | 6,65 | 152170 | 780 |
Códice | Não | 5475888 | 16h00 | 69070 | 160 |
Co-piloto | Sim | 30768 | 4.16 | 345190 | 940 |
Intersetorial | Não | 37304 | 73.02 | 21020 | 160 |
Interseção Rápida de Matriz | Não | 42064 | 20.33 | 18990 | 630 |
O Copilot usa menos heap, mas também é o mais lento e o que mais consome CPU.
ChatGPT é o menos intensivo de CPU, mas também é lento.
O Fast Array Intersect é 3 a 5 vezes mais rápido que o Copilot e o Chat GPT e 20% mais rápido que o Codex, embora seja menos intensivo em CPU.
O Intersector é 15 vezes mais rápido que o ChatGPT e o Copilot, 4 vezes mais rápido que o Codex e mais de 3 vezes mais rápido que o Fast Array Intersect, além de ter a menor utilização de CPU.
Resumindo:
Nem o ChatGPT nem o Copilot geram código eficiente.
O ChatGPT faz suposições que podem não ser válidas.
Copilot gera código que não produz um conjunto quando seus argumentos contêm valores duplicados.
Codex produz desempenho razoável, embora código obscuro.
Os programadores ganham.
Tanto o ChatGPT quanto o Copilot podem ser aprimorados aplicando sua “inteligência” para fazer ao usuário algumas perguntas esclarecedoras, por exemplo, você está procurando velocidade ou eficiência de memória, deseja exigir pelo menos dois argumentos?
Para esclarecer o objetivo desta seção, eu estava procurando todas as combinações possíveis de itens em arrays, não um produto cruzado aritmético de vetores.
Mostre-me uma função JavaScript para produto cruzado cartesiano.
function cartesianCrossProduct(v1, v2) { var x = v1.y*v2.z - v1.z*v2.y; var y = v1.z*v2.x - v1.x*v2.z; var z = v1.x*v2.y - v1.y*v2.x; return {x: x, y: y, z: z}; }
function crossProduct(arrays) { var result = []; var args = Array.prototype.slice.call(arguments); var arrays = args.slice(0, args.length - 1); var callback = args[args.length - 1]; var maxLength = Math.max.apply(null, arrays.map(function(arr) { return arr.length; })); for (var i = 0; i < maxLength; i++) { var item = arrays[0][i]; var isInAll = arrays.every(function(arr) { return arr.indexOf(item) !== -1; }); if (isInAll) { result.push(item); } } return callback(result); }
const crossProduct = (...arrays) => { const [first, ...rest] = arrays; return first.flatMap(x => rest.map(y => [x, y])); }
Infelizmente, o ChatGPT forneceu uma função para produto cruzado aritmético. Na minha opinião, deveria ter fornecido os dois tipos de funções ou solicitado esclarecimentos.
Depois de várias tentativas em diferentes consultas, consegui que o ChatGPT gerasse todos os três itens abaixo:
function crossProduct(...arrays) { if (arrays.length < 2) { throw new Error("At least 2 arrays are required!"); } let result = arrays[0].reduce((acc, x) => { return acc.concat(arrays.slice(1).reduce((acc, y) => { return acc.concat(y.map(z => [x].concat(z))); }, [])) }, []); return result; }
function allCombinations(...args) { var result = []; var f = function(prefix, arr) { for (var i = 0; i < arr.length; i++) { var newPrefix = prefix.concat(arr[i]); result.push(newPrefix); if (args.length > 0) { f(newPrefix, args.shift()); } } } f([], args[0]); return result; }
function crossProduct(...args) { var result = []; for(let i = 0; i < args.length; i++){ for(let j = i + 1; j < args.length; j++){ for(let ii = 0; ii < args[i].length; ii++){ for(let jj = 0; jj < args[j].length; jj++){ result.push([args[i][ii], args[j][jj]]); } } } } return result; }
const crossProduct = (...arrays) => { const [first, ...rest] = arrays; return first.flatMap(x => rest.map(y => [x, y])); }
Assim como a interseção, o Codex forneceu uma função usando um estilo de programação antigo com tratamento de argumentos obscuros.
Aqueles familiarizados com os produtos cartesianos saberão que eles podem ser explosivos tanto do ponto de vista da utilização da memória quanto do desempenho.
As implementações ingênuas consumirão RAM suficiente para armazenar todas as permutações e não retornarão um resultado até que todas as permutações tenham sido geradas (o que pode levar muito tempo). As funções ChatGPT e Copilot sofrem dessas deficiências.
Então, subseqüentemente, solicitei uma função JavaScript com eficiência de memória para o produto cartesiano .
function cartesianProduct(...args) { const iterators = args.map(arg => arg[Symbol.iterator]()); const next = () => { const values = iterators.map(iterator => iterator.next().value); return values.length > 1 ? values : null; }; return { [Symbol.iterator]: () => ({ next }) }; }
O copiloto forneceu os mesmos resultados anteriores.
Presumi que a versão “com eficiência de memória” do ChatGPT usava menos RAM do que a primeira. No entanto, eu sabia que não era o ideal. Depois de várias tentativas, criei uma função JavaScript com eficiência de memória para produtos cartesianos usando um gerador .
function* cartesianProduct(...args) { const recursion = (arrays, i = 0) => { if (i === arrays.length - 1) { yield* arrays[i]; } else { for (let item of arrays[i]) { for (let result of recursion(arrays, i + 1)) { yield [item, ...result]; } } } } yield* recursion(args); }
Para ser sincero, fiquei agradavelmente surpreendido com esta resposta do ChatGPT. No entanto, as tentativas de executar esse código falharam porque yield [item,...result]
não está dentro de um gerador, está dentro de uma função aninhada chamada recursion
.
Também recebi uma pequena variação para a qual perdi a consulta:
function* crossProduct(...arrays) { if (arrays.length < 2) { throw new Error("At least 2 arrays are required!"); } function* recurse(depth, current) { if (depth === arrays.length) { yield current; } else { for (let item of arrays[depth]) { yield* recurse(depth + 1, current.concat(item)); } } } yield* recurse(1, [arrays[0]]); }
Mais uma vez, o ChatGPT parecia estar fazendo algumas suposições sobre os argumentos.
O copiloto retornou o mesmo código anterior. Ele ignorou completamente meu pedido de um gerador.
E agora, o verdadeiro teste! Abaixo estão os resultados de referência para velocidade e memória usando código gerado por ChatGPT, Copilot e os produtos cartesianos mais rápidos e com maior eficiência de memória, dos quais tenho conhecimento CXProduct e Lazy Cartesian Product .
Também está incluída uma função de uma linha que talvez o ChatGPT ou o Copilot devessem ter retornado (você verá por que incluí isso quando revisar os resultados).
//https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript const cartesian = (...a) => a.reduce((a, b) => a.flatMap(d => b.map(e => [d, e].flat())));
O teste considera o produto cartesiano de 4 arrays com 10 itens cada. Portanto, as funções devem produzir um produto cartesiano de 10 * 10 * 10 * 10 itens, ou seja, 10.000.
Fonte | # Resultados | Pilha Usada | Operações de segurança | CPU do usuário | CPU do sistema |
---|---|---|---|---|---|
ChatGPT1 | 300 | N / D | N / D | N / D | N / D |
ChatGPT2 | 50 | N / D | N / D | N / D | N / D |
ChatGPT3 | 600 | N / D | N / D | N / D | N / D |
Memória ChatGPT Eficiente | N / D | N / D | N / D | N / D | N / D |
Gerador ChatGPT | 1000 | N / D | N / D | N / D | N / D |
Códice | 4 | N / D | N / D | N / D | N / D |
Co-piloto | 30 | N / D | N / D | N / D | N / D |
Uma linha | 10000 | 0 | 78,46 | 18150 | 0 |
CXProduct | 10000 | 0 | 291,50 | 7330 | 0 |
LazyProduct | 10000 | 17824 | 266,65 | 11560 | 150 |
Resumindo:
ChatGPT, Codex e Copilot são incapazes de gerar código que produza o resultado correto para um produto cartesiano.
Às vezes, o ChatGPT faz suposições que podem não ser válidas, por exemplo, exigindo 2 argumentos.
O ChatGPT gerou um código que dizia ser mais eficiente em termos de memória, mas nem sequer rodava.
Os programadores ganham!
Mais uma vez, o ChatGPT pode ser aprimorado aplicando sua “inteligência” para fazer algumas perguntas esclarecedoras ao usuário, por exemplo, você está procurando velocidade ou eficiência de memória, deseja exigir pelo menos dois argumentos? Porém, antes disso, ele deve gerar o código correto!
Tendo aprendido minha lição com interseção e produtos cartesianos, imediatamente fiz uma pergunta refinada para memoização.
Mostre-me uma função JavaScript eficiente que memoriza objetos e argumentos primitivos.
function memoize(fn) { let cache = new WeakMap(); return function(obj, ...args) { let key; if (obj) { if (!cache.has(obj)) { cache.set(obj, {}); } key = JSON.stringify([obj, ...args]); } else { key = JSON.stringify(args); } if (cache.has(key)) { return cache.get(key); } let result = fn.apply(this, [obj, ...args]); cache.set(key, result); return result; } }
ChatCPT gerou código inválido; um WeakMap
deve usar um objeto como uma chave. O código foi modificado da seguinte forma para teste.
function memoize(fn) { let cache = new Map(); return function(obj, ...args) { let key; if (obj!=null) { if (!cache.has(obj)) { cache.set(obj, {}); } key = JSON.stringify([obj, ...args]); } else { key = JSON.stringify(args); } if (cache.has(key)) { return cache.get(key); } let result = fn.apply(this, [obj, ...args]); cache.set(key, result); return result; } }
function memoize(func) { var cache = {}; return function() { var args = Array.prototype.slice.call(arguments); var key = JSON.stringify(args); if (cache[key]) { return cache[key]; } else { var result = func.apply(null, args); cache[key] = result; return result; } } }
const memoize = (fn, cache = new Map()) => { return (...args) => { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } else { const result = fn(...args); cache.set(key, result); return result; } } }
O Codex e o Copilot geraram a mesma função, o que não deve ser surpreendente, visto que o Copilot é baseado no Codex. Embora, nos exemplos anteriores, eles diferissem.
ChatGPT, Codex e funções ineficientes geradas pelo Copilot, JSON.stringify, seguidas por uma comparação de strings, são lentas e consomem muita memória em escala.
Também existem valores JavaScript que não são stringify, por exemplo, Infinity e NaN. (Infelizmente, a especificação JavaScript JSON foi definida antes da era da ciência de dados e microsserviços, e presumiu-se que Infinity e NaN implicavam erros no código que não eram legítimos ou não precisavam ser transportados de um lugar para outro .)
Então agora, para a prova de eficiência comparando o código ChatGPT e Copilot para nano-memoize e micro-memoize gerando o 12º número de Fibonacci usando este código:
const fibonacci = (number) => { return number < 2 ? number : fibonacci(number - 1) + fibonacci(number - 2); };
Fonte | Pilha Usada | Operações de segurança | CPU do usuário | CPU do sistema |
---|---|---|---|---|
ChatGPT (corrigido) | 102552 | 45801 | 620 | 0 |
Códice | 17888 | 52238 | 320 | 0 |
Co-piloto | 17888 | 51301 | 320 | 0 |
nano-memoize | 17576 | 93699 | 470 | 160 |
micro-memoize | 18872 | 82833 | 620 | 0 |
Nano-memoize é o mais rápido e quase duas vezes mais rápido que o código ChatGPT, Codex e Copilot. Ele também usa menos memória. Micro-memoize está próximo.
Embora a utilização da CPU para nano-memoize
e micro-memoize
seja um pouco maior do que Code e Copilot, o desempenho vale a pena e os programadores vencem mais uma vez!
Embora certamente haja valor no uso do Copilot e do ChatGPT para geração de código, isso deve ser feito com cuidado. Nenhum dos dois produzirá código ideal e, em alguns casos, será simplesmente inválido ou pior, incorreto. Além disso, ao usar o ChatGPT, as consultas devem ser bastante específicas.
O ChatGPT e o Copilot poderiam ser aprimorados com a adição de um recurso que faria perguntas esclarecedoras.
O ChatGPT, se fosse realmente inteligente, diria aos usuários para empregar seu irmão Codex para geração de código ou simplesmente usaria o Codex em segundo plano.
Se estiver usando o Codex em segundo plano, não tenho certeza do que está acontecendo quando forneço a mesma descrição de função para ambos e obtenho resultados diferentes.
Embora eu não esteja familiarizado com o funcionamento interno de nenhuma das ferramentas, além de saber que elas são baseadas em modelos de linguagem, imagino que seja improvável que elas cheguem a um ponto em que possam gerar código ideal sem superar essa deficiência:
Um sistema treinado em grandes volumes de código não controlado publicamente produzirá resultados médios para código, ou seja, código de desempenho médio e código com um número médio de bugs.
Para fornecer resultados consistentemente precisos, o sistema precisará de:
A capacidade de consumir e usar fragmentos de dados de “amostra de contador”, por exemplo, JSON.stringify, pode ser ineficiente. O sistema pode obter essa capacidade analisando os resultados do teste, bem como o código, ou sendo alimentado com um código ideal conhecido com algum tipo de ponderação ou simplesmente por meio da crítica dos resultados por especialistas conhecidos. Infelizmente, o código ideal geralmente não é o mais prevalente ou o mais usado, e simplesmente alimentar os modelos com mais exemplos não ajudará. No caso ideal, um sistema realmente inteligente seria capaz de gerar seus próprios casos de teste.
Um “entendimento” mais profundo e mais básico da programação para analisar o código gerado em busca de falhas de eficiência, por exemplo, geralmente favorece a iteração sobre a recursão para eficiência de tempo de execução, geralmente favorece a recursão para tamanho e legibilidade do código.
No mínimo, uma IA geradora de código deve tentar analisar o código que gerou e avaliar sua validade sintática. Isso deve ser um aprimoramento simples do ChatGPT.
Idealmente, a IA também executaria pelo menos um ou dois casos de teste simples para garantir a validade do tipo. Enquanto eu criava testes de unidade, o Copilot propôs uma série de conclusões de código aprimoradas úteis que poderiam ser usadas para essa finalidade, por exemplo, argumentos para chamadas de função e pesquisas de array. Presumo que o ChatGPT e o Codex possam ser aprimorados para fazer isso.
Espero que você tenha gostado deste artigo. Tenha um ótimo dia e faça ontem ciúmes do que você aprendeu hoje (aqui ou em outro lugar)!
Também publicado aqui