随着无代码/低代码环境的可用性增加以及人工智能应用于编程的出现,已经有许多文章称程序员的末日已经临近,因为最终用户将创建自己的应用程序,或者人工智能将根据描述编写应用程序。也许……但还没有。
在本文中,我通过将 OpenAI ChatGPT和Codex对 Microsoft Copilot的建议与手写代码进行比较来探索 AI 辅助编码的当前状态。 AI 生成的代码要么一般,要么有缺陷。
由于今天的重点是处理大量数据(包括用于 ChatGPT 和 Copilot 的大型语言模型),因此我专注于有效处理数据的关键算法。希望 AI 比一般代码更能“理解”自己的本性!
这些算法是:
路口
叉积
记忆
我为 ChatGPT 提供了表格说明:
显示<一些功能描述>
我向 Codex 提供了输入:
<部分功能说明>
我在 WebStorm IDE 中为 Copilot 提供了评论
// <一些功能描述>
每个的功能描述都是相同的。
我使用 Jasmine 作为单元测试工具,辅以类似于Benchtest中的代码来获得速度以及内存和 CPU 利用率。
展示一个 JavaScript 函数,它接受可变数量的数组并返回它们的交集。
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 假定提供少于一个数组是无效的。我可以想象提供一个论点可能有效的情况。
通过使用Set ,ChatGPT 确保结果中不存在重复项。交集应该是一个集合操作,应该删除重复项。 Copilot 代码将返回一个可能包含重复项的数组。
ChatGPT 和 Copilot 都不按长度升序对原始参数进行排序。这是一个微不足道的优化,可以产生很大的不同!
如果任何参数的长度为零,则没有交集;无论如何,它缩短了循环,因为最大交集与最短数组参数相同。
Codex 提供了复杂的代码,据我所知,这些代码看起来非常高效。然而,它对参数的使用是模糊的,并且涉及一种有点陈旧的编程风格。
请注意,必须提供回调作为最后一个参数以将交集返回给调用代码,但未显示该参数。
然后我要求 ChatGPT 和 Copilot 给我一个高效的功能。
展示一个高效的 JavaScript 函数,它接受可变数量的数组并返回它们的交集。
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 生成了与第一个请求相同的代码。
ChatGPT 还添加了评论,称该功能不会像预期的那样对对象起作用,尽管可以说它会起作用。所以,我要求一个支持对象的函数:
展示一个高效的 JavaScript 函数,它接受可变数量的包含对象或基元的数组并返回它们的交集。
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 将对象字符串化以进行比较。这有很多问题:
具有相同字符串表示的两个对象可能是合法的独立对象。
即使字符串化有效,不同的属性顺序也会导致不同的字符串,并且对象可能代表相同的实体。
JSON.stringify后接字符串比较是一个缓慢的操作。
而现在,真正的考验!下面是使用 ChatGPT、Codex、Copilot 生成的代码以及通过 NPM 提供的两个最快的交集库fastArrayIntersect和intersector生成的速度和内存基准测试结果。
基准测试与 3 个相同的数组相交,每个数组包含 10,000 个整数,最终数组包含 5,000 个整数,样本大小为 100。当源数组中存在重复条目时,还运行测试以确认函数返回集。
来源 | 重复 | 堆使用 | 运维部 | 用户CPU | 系统中央处理器 |
---|---|---|---|---|---|
聊天GPT | 不 | 39768 | 6.65 | 152170 | 780 |
法典 | 不 | 5475888 | 16.00 | 69070 | 160 |
副驾驶 | 是的 | 30768 | 4.16 | 345190 | 940 |
交叉路口 | 不 | 37304 | 73.02 | 21020 | 160 |
快速数组交集 | 不 | 42064 | 20.33 | 18990 | 630 |
Copilot 使用最少的堆,但也是最慢和最占用 CPU 资源的。
ChatGPT 占用的 CPU 最少,但速度也很慢。
Fast Array Intersect 的速度是 Copilot 和 Chat GPT 的 3 到 5 倍,比 Codex 快 20%,同时占用的 CPU 更少。
Intersector 比 ChatGPT 和 Copilot 快 15 倍,比 Codex 快 4 倍,比 Fast Array Intersect 快 3 倍多,同时 CPU 利用率也最低。
总之:
ChatGPT 和 Copilot 都不会生成高效代码。
ChatGPT 做出的假设可能无效。
Copilot 生成的代码在其参数包含重复值时不会生成集合。
Codex 产生合理的性能,尽管晦涩难懂的代码。
程序员赢了。
ChatGPT 和 Copilot 都可以通过应用它们的“智能”来向用户询问一些明确的问题来改进,例如,你是在寻找速度还是内存效率,你是否希望至少需要两个参数?
为了明确本节的目标,我正在寻找数组中项目的所有可能组合,而不是向量的算术叉积。
显示笛卡尔叉积的 JavaScript 函数。
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])); }
不幸的是,ChatGPT 提供了算术叉积函数。在我看来,它应该提供两种类型的功能或要求澄清。
在多次尝试不同的查询后,我能够让 ChatGPT 生成以下所有三个:
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])); }
与交集一样,Codex 提供了一个使用旧编程风格和模糊参数处理的函数。
熟悉笛卡尔积的人会知道,无论从内存利用率还是性能角度来看,它们都是爆炸性的。
朴素的实现将消耗足够的 RAM 来存储所有排列,并且在生成所有排列之前不会返回结果(这可能需要很长时间)。 ChatGPT 和 Copilot 功能都存在这些缺点。
因此,我随后要求为 Cartesian product 提供一个内存高效的 JavaScript 函数。
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 提供了与之前相同的结果。
我假设来自 ChatGPT 的“内存高效”版本使用的 RAM 比第一个版本少。但是,我知道这不是最优的。经过几次尝试,我想出了一个内存高效的 JavaScript 函数,用于使用生成器的笛卡尔积。
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); }
老实说,我对 ChatGPT 的这种回应感到惊喜。但是,运行此代码的尝试失败了,因为yield [item,...result]
不在生成器内,而是在称为recursion
的嵌套函数内。
我还收到了一个细微的变化,我失去了查询:
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]]); }
再一次,ChatGPT 似乎在对争论做出一些假设。
Copilot 返回了与之前相同的代码。它完全忽略了我对发电机的要求。
而现在,真正的考验!以下是使用 ChatGPT、Copilot 和最快、内存效率最高的笛卡尔产品(我知道CXProduct和Lazy Cartesian Product )生成的代码的速度和内存基准测试结果。
还包括一个单行函数,也许 ChatGPT 或 Copilot 应该已经返回(当您查看结果时,您会明白我为什么包括这个)。
//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())));
该测试采用 4 个数组的笛卡尔积,每个数组有 10 个项目。因此,函数应该产生 10 * 10 * 10 * 10 项的笛卡尔积,即 10,000。
来源 | # 结果 | 堆使用 | 运维部 | 用户CPU | 系统中央处理器 |
---|---|---|---|---|---|
聊天GPT1 | 300 | 不适用 | 不适用 | 不适用 | 不适用 |
聊天GPT2 | 50 | 不适用 | 不适用 | 不适用 | 不适用 |
聊天GPT3 | 600 | 不适用 | 不适用 | 不适用 | 不适用 |
ChatGPT 内存高效 | 不适用 | 不适用 | 不适用 | 不适用 | 不适用 |
ChatGPT 生成器 | 1000 | 不适用 | 不适用 | 不适用 | 不适用 |
法典 | 4个 | 不适用 | 不适用 | 不适用 | 不适用 |
副驾驶 | 30 | 不适用 | 不适用 | 不适用 | 不适用 |
一条线 | 10000 | 0 | 78.46 | 18150 | 0 |
CX产品 | 10000 | 0 | 291.50 | 7330 | 0 |
懒人产品 | 10000 | 17824 | 266.65 | 11560 | 150 |
总之:
ChatGPT、Codex 和 Copilot 无法生成为笛卡尔积生成正确结果的代码。
ChatGPT 有时会做出可能无效的假设,例如,需要 2 个参数。
ChatGPT 生成的代码据说内存效率更高,但它甚至无法运行。
程序员赢了!
再一次,ChatGPT 可以通过应用其“智能”向用户询问一些明确的问题来改进,例如,您是在寻找速度还是内存效率,您是否需要至少两个参数?但是,在此之前,它必须生成正确的代码!
在吸取了交集和笛卡尔积的教训后,我立即提出了一个用于记忆的精炼问题。
展示一个高效的 JavaScript 函数,它可以记忆对象和原始参数。
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 生成无效代码; WeakMap
必须使用对象作为键。为了测试,代码修改如下。
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 和 Copilot 生成了相同的函数,鉴于 Copilot 基于 Codex,这并不奇怪。尽管在前面的示例中,它们有所不同。
ChatGPT、Codex 和 Copilot 生成低效的函数, JSON.stringify,然后进行字符串比较,速度慢且大规模消耗内存。
还有一些 JavaScript 值不进行字符串化,例如 Infinity 和 NaN。 (不幸的是,JavaScript JSON 规范是在数据科学和微服务时代之前定义的,并且假定 Infinity 和 NaN 暗示代码中的错误不合法或不需要从一个地方传输到另一个地方.)
所以现在,通过使用此代码生成第 12 个斐波那契数来比较 ChatGPT 和 Copilot 代码与nano-memoize和micro-memoize的效率证明:
const fibonacci = (number) => { return number < 2 ? number : fibonacci(number - 1) + fibonacci(number - 2); };
来源 | 堆使用 | 运维部 | 用户CPU | 系统中央处理器 |
---|---|---|---|---|
ChatGPT(已更正) | 102552 | 45801 | 620 | 0 |
法典 | 17888 | 52238 | 320 | 0 |
副驾驶 | 17888 | 51301 | 320 | 0 |
纳米记忆 | 17576 | 93699 | 470 | 160 |
微记忆 | 18872 | 82833 | 620 | 0 |
Nano-memoize 是最快的,几乎是 ChatGPT、Codex 和 Copilot 代码的两倍。它还使用更少的内存。 Micro-memoize 紧随其后。
虽然nano-memoize
和micro-memoize
的 CPU 使用率略高于 Code 和 Copilot,但性能是值得的,程序员又一次赢了!
尽管使用 Copilot 和 ChatGPT 进行代码生成肯定是有价值的,但应该谨慎使用。两者都不会产生最佳代码,在某些情况下,它只会无效或更糟,不正确。此外,在使用 ChatGPT 时,查询应该非常具体。
ChatGPT 和 Copilot 都可以通过添加一项要求澄清问题的功能来改进。
如果 ChatGPT 真的很智能,它会告诉用户使用其兄弟 Codex 来生成代码,或者只是在后台使用 Codex。
如果它在后台使用 Codex,那么当我向两者提供相同的功能描述并得到不同的结果时,我不确定会发生什么。
尽管我不熟悉这两种工具的内部工作原理,除了知道它们是基于语言模型之外,我假设它们不太可能在不克服这个缺点的情况下生成最佳代码:
在大量未经审查的公开代码上训练的系统将产生代码的平均结果,即平均性能的代码和具有平均错误数量的代码。
为了提供始终如一的准确结果,系统需要:
消费和使用“反样本”数据片段(例如JSON.stringify)的能力可能效率低下。系统可能会通过分析测试结果和代码或被提供具有某种权重的已知最佳代码或简单地通过已知专家对结果的评论来获得这种能力。不幸的是,最佳代码通常不是最流行或最常用的,简单地为模型提供更多示例也无济于事。在理想情况下,一个真正智能的系统将能够生成自己的测试用例。
对编程的更深入、更基本的“理解”,以便分析它生成的代码是否存在效率缺陷,例如,通常倾向于迭代而不是递归以提高运行时效率,通常倾向于递归以提高代码大小和可读性。
至少,代码生成 AI 应该尝试解析它生成的代码并评估其句法有效性。这应该是对 ChatGPT 的简单增强。
理想情况下,AI 还将运行至少一两个简单的测试用例以确保类型有效性。当我创建单元测试时,Copilot 提出了许多可用于此目的的有用的增强代码完成,例如,函数调用和数组查找的参数。我假设可以增强 ChatGPT 和 Codex 来做这样的事情。
我希望你喜欢这篇文章。祝你有美好的一天,让昨天嫉妒你今天(在这里或其他地方)学到的东西!
也发布在这里