去年底在处理项目中一个性能问题的时候碰到一个有意思的Bug。
一段代码大致如下,给定一个类型为Entity
的元素以及相应的key
,以及一个已排序并且可能有空位的数组array
,从给定的下标index
开始(该下标通过二分查找得到),先往头部遍历array
,如果找到已存在的元素或者不存在但找到空位并添加成功则返回(状态为Success
),否则跳出循环(状态为Break
),再从下标开始往尾部遍历尝试同样的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void Find(int index, string key, Entity entity, Entity[] array)
{
for (var i = index - 1; i >= 0; i--)
{
var status = TryAddEntity(array, i, key, entity);
switch (status)
{
case SearchStatus.Success:
return;
case SearchStatus.Break:
break;
}
}
for (var i = index + 1; i < array.Length; i++)
{
var status = TryAddEntity(array, i, key, entity);
switch (status)
{
case SearchStatus.Success:
return;
case SearchStatus.Break:
break;
}
}
////Do something else
}
这段代码是某个装载股票数据的组件的一部分,看上去清晰易懂。然而交易系统在生产环境中偶尔在股市临近关闭的时候性能会下降,交易员直观感受是界面响应速度变慢了。
获取出现问题的交易员日志后发现在股市临近关闭时收到了大量的股票信息更新。组件收到更新会执行上面的代码片段根据关键字(比如股票代码)找到相应对象并更新或找到合适空位添加新对象。用于存储数据的数组已经按升序排列,Find
方法执行速度应该非常快才对,但从症状看似乎算法退化成了线性查找。
代码阅读了很多遍都没觉得有问题,最后调试才发现下面跳出循环(状态为Break
)的方式错了。
1
2
3
4
5
6
7
8
9
......
switch (status)
{
case SearchStatus.Success:
return;
case SearchStatus.Break:
break; //===>只跳出`switch`语句而不是跳出循环
}
......
break
语句用于跳出循环也用于跳出switch
语句。由于这里break
只是跳出switch
语句,包在外层的循环则会继续执行,于是本来应该早早跳出循环的操作需要一直等到数组遍历完了才结束——当数组很长的时候不论从哪个下标开始都退化成线性查找,数据多了之后性能下降就显现出来了。
修补很容易,写法很多不再赘述。我则突发奇想地把AI
拉出来看看它能不能帮上忙,是骡子是马,一试便知。
测试用的是JetBrains AI Pro
(背后提供支持的是OpenAI
[1
])。选中这段代码让AI
给出修改建议,没想到非常好用,下面这条直接指出了问题所在,看来冷冰冰不带感情的机器读起代码来可比人要准确,尤其是这种略有歧义的上下文。
以前说过指望动动嘴皮子让AI
帮你写代码而自己躺平是不可能的,把AI
当做强大的工具使用才是正道,这次碰到的问题就很好地佐证了这一点——人容易出错的地方AI
不容易出错。当然这里说的“动动嘴皮子让AI
帮你写代码”不是“写一个网页登陆界面
”那种烂大街毫无用处的代码,而是“针对业务需求实现一个高速缓存,要求内存占用足够小,查询速度足够快”这样的需求。
关于AI
的好话说完了,再说说问题。还是JetBrains AI Pro
,如果是让AI
描述上面的代码而不是给出修改建议,它是这样说的:
这一次完全忽视了代码可能存在的问题,告诉观众SearchStatus.Break
会导致跳出循环,变成人了——不稳定,也是个不小的问题。
参考资料
- JetBrains AI Service Providers https://www.jetbrains.com/legal/docs/terms/jetbrains-ai/service-providers