Avec la disponibilité accrue d'environnements sans code/low code et l'avènement de l'IA appliquée à la programmation, un certain nombre d'articles ont été publiés disant que la fin est proche pour les programmeurs, soit parce que les utilisateurs finaux créeront leurs propres applications, soit parce que l'IA écrire des applications à partir de descriptions. Peut-être… mais pas encore.
Dans cet article, j'explore l'état actuel du codage assisté par l'IA en comparant les suggestions d'OpenAI ChatGPT et de Codex à Microsoft Copilot au code écrit à la main. Les IA produisent du code moyen ou défectueux.
Étant donné que l'accent est mis aujourd'hui sur le traitement de gros volumes de données (y compris les grands modèles de langage utilisés pour ChatGPT et Copilot), je me concentre sur les algorithmes qui sont essentiels pour traiter efficacement les données. Espérons que les IA « comprennent » mieux leur propre nature que le code en général !
Ces algorithmes sont :
intersection
produit croisé
mémoriser
J'ai fourni à ChatGPT des instructions pour le formulaire :
Montrez-moi <une description de fonction>
J'ai fourni au Codex des informations :
<une description de fonction>
J'ai fourni à Copilot des commentaires dans un IDE WebStorm
// <une description de fonction>
Les descriptions de fonction étaient les mêmes pour chacun.
J'ai utilisé Jasmine comme harnais de test unitaire complété par un code similaire à celui trouvé dans Benchtest pour obtenir la vitesse ainsi que l'utilisation de la mémoire et du processeur.
Montrez-moi une fonction JavaScript qui prend un nombre variable de tableaux et renvoie leur intersection.
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))); }
ChatGPT suppose que fournir moins d'un tableau n'est pas valide. Je peux imaginer des situations dans lesquelles fournir un argument pourrait être valable.
En utilisant un Set , ChatGPT garantit qu'aucun doublon n'existe dans les résultats. L'intersection est censée être une opération définie et les doublons doivent être supprimés. Le code Copilot renverra un tableau qui peut contenir des doublons.
Ni ChatGPT ni Copilot ne trient les arguments d'origine par ordre croissant de longueur. Il s'agit d'une optimisation triviale qui pourrait faire une grande différence !
Si un argument a une longueur nulle, il n'y a pas d'intersection ; quoi qu'il en soit, cela raccourcit les boucles car l'intersection maximale est la même que l'argument de tableau le plus court.
Le Codex a fourni un code complexe qui, d'après mes connaissances, semblait assez efficace. Cependant, son utilisation d'arguments est obscure et implique un style de programmation quelque peu archaïque.
Notez qu'un rappel doit être fourni comme dernier argument pour renvoyer l'intersection au code appelant, mais l'argument n'est pas affiché.
J'ai alors demandé à ChatGPT et Copilot de me donner une fonction efficace.
Montrez-moi une fonction JavaScript efficace qui prend un nombre variable de tableaux et renvoie leur intersection.
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]; }
Copilot a généré le même code que la première requête.
ChatGPT a également ajouté un commentaire indiquant que la fonction ne fonctionnerait pas comme prévu pour les objets, bien que ce soit sans doute le cas. J'ai donc demandé une fonction qui prendrait en charge les objets :
Montrez-moi une fonction JavaScript efficace qui prend un nombre variable de tableaux contenant des objets ou des primitives et renvoie leur intersection.
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 stringifie les objets pour les comparer. Il y a un certain nombre de problèmes avec ceci :
Deux objets qui ont la même représentation sous forme de chaîne peuvent légitimement être des objets distincts.
Même si la chaîne est valide, différents ordres de propriété entraîneront des chaînes différentes et les objets peuvent représenter la même entité.
JSON.stringify suivi d'une comparaison de chaînes est une opération lente.
Et maintenant, le vrai test ! Vous trouverez ci-dessous des résultats de référence pour la vitesse et la mémoire en utilisant le code généré par ChatGPT, Codex, Copilot et les deux bibliothèques d'intersection les plus rapides disponibles via NPM, fastArrayIntersect et intersector .
Le benchmark a croisé 3 tableaux identiques de 10 000 entiers chacun et un tableau final de 5 000 entiers avec une taille d'échantillon de 100. Un test a également été exécuté pour confirmer que les fonctions renvoyaient des ensembles lorsque des entrées en double existaient dans les tableaux source.
Source | Doublons | Tas utilisé | Sec Ops | UC utilisateur | UC du système |
---|---|---|---|---|---|
ChatGPT | Non | 39768 | 6,65 | 152170 | 780 |
Manuscrit | Non | 5475888 | 16.00 | 69070 | 160 |
Copilote | Oui | 30768 | 4.16 | 345190 | 940 |
Intersecteur | Non | 37304 | 73.02 | 21020 | 160 |
Intersection de réseau rapide | Non | 42064 | 20h33 | 18990 | 630 |
Copilot utilise le moins de tas, mais est également le plus lent et le plus gourmand en CPU.
ChatGPT est le moins gourmand en CPU, mais il est également lent.
Fast Array Intersect est 3 à 5 fois plus rapide que Copilot et Chat GPT et 20 % plus rapide que Codex tout en étant moins gourmand en CPU.
Intersector est 15 fois plus rapide que ChatGPT et Copilot, 4 fois plus rapide que Codex et plus de 3 fois plus rapide que Fast Array Intersect tout en ayant la plus faible utilisation du processeur.
En résumé:
Ni ChatGPT ni Copilot ne génèrent de code efficace.
ChatGPT fait des hypothèses qui peuvent ne pas être valides.
Copilot génère du code qui ne produit pas d'ensemble lorsque ses arguments contiennent des valeurs en double.
Le codex produit un code raisonnablement performant, bien qu'obscur.
Les programmeurs gagnent.
ChatGPT et Copilot pourraient tous deux être améliorés en appliquant leur "intelligence" pour poser à l'utilisateur quelques questions de clarification, par exemple, recherchez-vous la vitesse ou l'efficacité de la mémoire, voulez-vous exiger au moins deux arguments ?
Pour être clair sur l'objectif de cette section, je cherchais toutes les combinaisons possibles d'éléments dans des tableaux, et non un produit croisé arithmétique de vecteurs.
Montrez-moi une fonction JavaScript pour le produit croisé cartésien.
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])); }
Malheureusement, ChatGPT a fourni une fonction pour le produit croisé arithmétique. À mon avis, il aurait dû fournir les deux types de fonctions ou demander des éclaircissements.
Après plusieurs tentatives de requêtes différentes, j'ai réussi à faire en sorte que ChatGPT génère les trois éléments ci-dessous :
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])); }
Comme pour l'intersection, Codex a fourni une fonction utilisant un ancien style de programmation avec une gestion obscure des arguments.
Ceux qui connaissent les produits cartésiens sauront qu'ils peuvent être explosifs à la fois du point de vue de l'utilisation de la mémoire et des performances.
Les implémentations naïves consommeront suffisamment de RAM pour stocker toutes les permutations et ne renverront pas de résultat tant que toutes les permutations n'auront pas été générées (ce qui peut prendre beaucoup de temps). Les fonctions ChatGPT et Copilot souffrent toutes deux de ces lacunes.
J'ai donc demandé par la suite une fonction JavaScript efficace en mémoire pour le produit cartésien .
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 }) }; }
Copilot a fourni les mêmes résultats que précédemment.
J'ai supposé que la version "mémoire efficace" de ChatGPT utilisait moins de RAM que la première. Cependant, je savais que ce n'était pas optimal. Après plusieurs tentatives, j'ai trouvé une fonction JavaScript efficace en mémoire pour le produit cartésien utilisant un générateur .
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); }
Pour être honnête, j'ai été agréablement surpris par cette réponse de ChatGPT. Cependant, les tentatives d'exécution de ce code ont échoué car yield [item,...result]
n'est pas à l'intérieur d'un générateur, il est à l'intérieur d'une fonction imbriquée appelée recursion
.
J'ai également reçu une légère variation à laquelle j'ai perdu la requête :
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]]); }
Encore une fois, ChatGPT semblait faire des hypothèses sur les arguments.
Copilot a renvoyé le même code que précédemment. Il a complètement ignoré ma demande de générateur.
Et maintenant, le vrai test ! Vous trouverez ci-dessous des résultats de référence pour la vitesse et la mémoire en utilisant le code généré par ChatGPT, Copilot et les produits cartésiens les plus rapides et les plus économes en mémoire dont je connais CXProduct et Lazy Cartesian Product .
Une fonction en une ligne est également incluse, que ChatGPT ou Copilot auraient peut-être dû renvoyer (vous verrez pourquoi je l'ai incluse lorsque vous examinerez les résultats).
//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())));
Le test prend le produit cartésien de 4 tableaux de 10 éléments chacun. Par conséquent, les fonctions doivent produire un produit cartésien de 10 * 10 * 10 * 10 éléments, c'est-à-dire 10 000.
Source | # Résultats | Tas utilisé | Sec Ops | UC utilisateur | UC du système |
---|---|---|---|---|---|
ChatGPT1 | 300 | N / A | N / A | N / A | N / A |
ChatGPT2 | 50 | N / A | N / A | N / A | N / A |
ChatGPT3 | 600 | N / A | N / A | N / A | N / A |
Mémoire ChatGPT efficace | N / A | N / A | N / A | N / A | N / A |
Générateur ChatGPT | 1000 | N / A | N / A | N / A | N / A |
Manuscrit | 4 | N / A | N / A | N / A | N / A |
Copilote | 30 | N / A | N / A | N / A | N / A |
Une ligne | 10000 | 0 | 78,46 | 18150 | 0 |
CXProduct | 10000 | 0 | 291,50 | 7330 | 0 |
Produit paresseux | 10000 | 17824 | 266,65 | 11560 | 150 |
En résumé:
ChatGPT, Codex et Copilot sont incapables de générer du code produisant le résultat correct pour un produit cartésien.
ChatGPT fait parfois des hypothèses qui peuvent ne pas être valides, par exemple, nécessitant 2 arguments.
ChatGPT a généré du code qui, selon lui, était plus économe en mémoire, mais il ne fonctionnerait même pas.
Les programmeurs gagnent !
Encore une fois, ChatGPT pourrait être amélioré en appliquant son "intelligence" à poser quelques questions de clarification à l'utilisateur, par exemple, recherchez-vous la vitesse ou l'efficacité de la mémoire, voulez-vous exiger au moins deux arguments ? Cependant, avant cela, il doit générer le bon code !
Ayant appris ma leçon avec l'intersection et les produits cartésiens, j'ai immédiatement posé une question raffinée pour la mémorisation.
Montrez-moi une fonction JavaScript efficace qui mémorise les arguments d'objet et de primitive.
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 a généré un code invalide ; une WeakMap
doit utiliser un objet comme clé. Le code a été modifié comme suit pour les tests.
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; } } }
Codex et Copilot ont généré la même fonction, ce qui n'est pas surprenant étant donné que Copilot est basé sur Codex. Bien que, dans les exemples précédents, ils différaient.
ChatGPT, Codex et Copilot ont généré des fonctions inefficaces, JSON.stringify, suivies d'une comparaison de chaînes, sont lentes et consommatrices de mémoire à grande échelle.
Il existe également des valeurs JavaScript qui ne sont pas stringifiantes, par exemple, Infinity et NaN. (Malheureusement, la spécification JavaScript JSON a été définie avant l'ère de la science des données et des micro-services, et il a été supposé qu'Infinity et NaN impliquaient des erreurs de code qui n'étaient pas légitimes ou n'avaient pas besoin d'être transportées d'un endroit à un autre .)
Alors maintenant, pour la preuve d'efficacité en comparant le code ChatGPT et Copilot à nano-memoize et micro-memoize en générant le 12e nombre de Fibonacci à l'aide de ce code :
const fibonacci = (number) => { return number < 2 ? number : fibonacci(number - 1) + fibonacci(number - 2); };
Source | Tas utilisé | Sec Ops | UC utilisateur | UC du système |
---|---|---|---|---|
ChatGPT (corrigé) | 102552 | 45801 | 620 | 0 |
Manuscrit | 17888 | 52238 | 320 | 0 |
Copilote | 17888 | 51301 | 320 | 0 |
nano-mémoïse | 17576 | 93699 | 470 | 160 |
micro-mémoire | 18872 | 82833 | 620 | 0 |
Nano-memoize est le code le plus rapide et presque deux fois plus rapide que ChatGPT, Codex et Copilot. Il utilise également moins de mémoire. Micro-memoize est sur ses talons.
Bien que l'utilisation du processeur pour nano-memoize
et micro-memoize
soit légèrement supérieure à Code et Copilot, les performances en valent la peine et les programmeurs gagnent une fois de plus !
Bien qu'il y ait certainement une valeur à utiliser à la fois Copilot et ChatGPT pour la génération de code, cela doit être fait avec précaution. Ni l'un ni l'autre ne produira un code optimal et dans certains cas, il sera simplement invalide ou pire, incorrect. De plus, lors de l'utilisation de ChatGPT, les requêtes doivent être assez spécifiques.
ChatGPT et Copilot pourraient tous deux être améliorés par l'ajout d'une fonctionnalité qui poserait des questions de clarification.
ChatGPT, s'il était vraiment intelligent, dirait aux utilisateurs d'utiliser son frère Codex pour la génération de code ou utiliserait simplement Codex en arrière-plan.
S'il utilise Codex en arrière-plan, je ne suis pas sûr de ce qui se passe lorsque je fournis la même description de fonction aux deux et que j'obtiens des résultats différents.
Bien que je ne sois pas familier avec le fonctionnement interne de l'un ou l'autre outil, à part savoir qu'ils sont basés sur un modèle de langage, je suppose qu'il est peu probable qu'ils arrivent à un point où ils peuvent générer un code optimal sans surmonter cette lacune :
Un système formé sur de gros volumes de code publiquement non vérifié va produire des résultats moyens pour le code, c'est-à-dire un code de performance moyenne et un code avec un nombre moyen de bogues.
Pour fournir des résultats toujours précis, le système aura besoin :
La possibilité de consommer et d'utiliser des fragments de données « contre-échantillon », par exemple, JSON.stringify, peut être inefficace. Le système peut acquérir cette capacité en analysant les résultats des tests ainsi que le code ou en recevant un code optimal connu avec un certain type de pondération ou simplement par la critique des résultats par des experts connus. Malheureusement, le code optimal n'est souvent pas le plus répandu ou le plus utilisé, et le simple fait d'alimenter les modèles avec plus d'exemples n'aidera pas. Dans le cas idéal, un système vraiment intelligent serait capable de générer ses propres cas de test.
Une "compréhension" plus profonde et plus première du principe de programmation afin d'analyser le code qu'il génère pour les défauts d'efficacité, par exemple, privilégier généralement l'itération à la récursivité pour l'efficacité de l'exécution, privilégier généralement la récursivité pour la taille et la lisibilité du code.
Au minimum, une IA génératrice de code devrait tenter d'analyser le code qu'elle a généré et d'évaluer sa validité syntaxique. Cela devrait être une simple amélioration de ChatGPT.
Idéalement, l'IA exécuterait également au moins un ou deux cas de test simples pour garantir la validité du type. Pendant que je créais des tests unitaires, Copilot a proposé un certain nombre de complétions de code améliorées utiles qui pourraient être utilisées à cette fin, par exemple, des arguments pour les appels de fonction et les recherches de tableau. Je suppose que ChatGPT et Codex pourraient être améliorés pour faire une telle chose.
J'espère que vous avez apprécié cet article. Bonne journée, et rendez hier jaloux de ce que vous avez appris aujourd'hui (ici ou ailleurs) !
Également publié ici