您当前的位置: 首页 > 

凌云时刻

暂无认证

  • 1浏览

    0关注

    1437博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

是什么让一段20行代码的性能提升了10倍

凌云时刻 发布时间:2022-09-14 11:23:19 ,浏览量:1

本文作者:金盛杰(司旭)

5 个版本的优化,性能得到了超过 10 倍的提升。

背景

业务背景

支付宝卡包存放着用户的会员卡和优惠券。无论是卡券 cell,还是卡券详情,都是通过静态模板配置加上动态可变数据,最终呈现给终端用户的。

下面【图1】展现了卡券数据在 C 端用户的展现形式,【图2】表示了 C 端数据组装过程。

在这里插入图片描述

图1 卡券数据在 C 端展现形式 在这里插入图片描述

图2 C 端数据组装过程

以【图2】为例,模板中有availableAmount和voucherName两个变量,这两个变量在动态变量数据有对应的值。用动态的值替换掉模板里面对应的这两个变量,最后拼装成“100 元红包名称”。当这个红包被使用了一次,消费了30元后,动态数据里面availableAmount的值就会变成 70。用户再次进入到红包详情页时,展现数据重新组装后就会变成“70 元红包名称”。

问题发现

最近做项目过程中,把卡券组装渲染逻辑好好的梳理了一遍,其中仔细研读了【图3】这段模板变量替换逻辑。这是一段老代码,从卡包产品诞生之日起就存在,差不多有十年的时间了。其作用就是用动态数据替换掉模板里面的变量。这段代码逻辑咋一看,并没有什么问题,就是把模板里面两个$ 之间(包含)的变量,用动态数据进行替换。考虑到这是一段极为核心又高频的调用逻辑,于是看看有没有性能优化的空间。

在这里插入图片描述

图3 模板变量替换代码实现

把替换逻辑厘清了之后,第一感觉就是这段代码有性能提升的空间。主要有两点:

  • 每次 while 循环进行了两次 indexOf 操作
  • 每次 while 循环都进行了 substring 操作

于是,就有了下面两个疑问:

  • 能够减少 indexOf 和 substring 操作吗?
  • 真的每次都要进行模板变量查找吗?
性能优化

带着上面两个问题,逐步进行性能优化并测试。整个优化过程一共迭代了 5 版,并最终取得了性能提升超过 10 倍的效果。下面分别来介绍下不同版本的实现和性能对比。

性能优化 V1

这一版去掉了indexOf和substring操作,转而使用另一种替换方式。

之前的替换逻辑是从头到尾循环模板内容字符串,遇到 $ 之间的变量就进行替换,过程中需要不断的进行indexOf和substring操作。新的实现方式是在进行变量替换之前,通过循环模板内容字符串,利用双指针把模板里面所有变量都提取出来,再对变量集合进行循环,依次替换掉模板内容里面的变量。

在这里插入图片描述

图4 性能优化 V1 代码实现

性能优化 V2

静态模板配置一般情况下不会发生变更。也就意味着,同一个模板对应的变量都是固定不变的。可以将模板 id 和模板变量集合进行一对一的缓存,减少每次替换之前的变量提取。在决定使用缓存之前,要想好怎么实现缓存。有两点需要注意:

用本地缓存代替 TBase,减少大流量场景下对 TBase 的压力 怎么控制本地缓存的有效数量,并在有限的内存占用情况下最大化缓存效率 可以借助 Google Guava 库的缓存类来实现缓存逻辑,示例代码见【图5】

在这里插入图片描述

图5 缓存实现示例代码 在这里插入图片描述

图6 性能优化 V2 代码实现

性能对比(1)

做完上面两步之后进行了性能测试,性能对比如【图7】所示。

在这里插入图片描述

图7 V1、V2 版性能对比

通过性能对比发现,V1 版相对于原始版有性能提升,带缓存的 V2 版相对于不带缓存的 V1 版也有性能提升。但随着流量增大,性能优化效果逐步减弱。说明 V1、V2 版耗时优化的点,在整个模板变量替换耗时中占比并不高。也同时说明,整个模板变量替换逻辑当中,还存在其他更为耗时的点。

回过头来再仔细看一遍变量替换逻辑,突然间意识到遗漏了一个”大问题“。就是这个String.replace方法,该方法有两个耗时点:

  • 每次 replace 都会进行模板编译
  • replace 都是创建一个新的对象进行返回

并且每次replace之后还要进行变量的重新赋值。

在这里插入图片描述

图8 String.replace 代码实现

性能优化 V3

在 V2 版基础上,去掉replace方法,用StringBuilder 来实现。

在这里插入图片描述

图9 性能优化 V3 代码实现

StringBuilder实现过程中有一点要注意。V2 版本中,提取变量返回的是一个Set集合。返回集合中出现变量的顺序和模板中变量顺序会不一致,模板中有多个相同变量的情况下,也只会替换第一个出现的变量。所以要将变量提取返回的结果换成有序可重复的List,才能保证逻辑的正确性。

性能优化 V4

V3 版优化之后,性能提升明显,证明String.replace方法才是整个模板变量替换逻辑中最为耗时的点。于是在原方法上只用StringBuilder来替换String.replace,得到 V4 版。

在这里插入图片描述

图10 性能优化 V4 代码实现

性能对比(2)

在这里插入图片描述

图11 V1、V2、V3、V4 版性能对比

通过【图11】可以明显的发现,在进行StringBuilder实现后,性能提升超过 10 倍,效果十分明显。

V4 版耗时实际上比 V3 版带缓存的还要少,说明 V3 版先提取变量再进行StringBuilder组装的过程,相对来说还是会更耗时一点。但 V4 版的代码可读性是不如 V3 版的,可以把 V3 版和 V4 版相结合,剔除掉缓存依赖,产生一个代码可读性和性能最佳的 V5 版。

性能优化 V5

先提取变量,去掉缓存依赖,用StringBuilder替换掉String.replace,增加代码可读性。

在这里插入图片描述

图12 V5 版代码实现 & 100 万次循环耗时对比

总结

通过上面 5 个版本的性能优化,性能得到了超过 10 倍的提升。

性能由高到低的顺序是 V4 > V3 > V5 > V2 > V1 > 未被优化的原始版。其中 V3、V4、V5 版的性能显著优于 V1 和 V2 版,证明这段模板替换逻辑最为耗时的点为String.replace,V3 > V5 和 V2 > V1表明,引入缓存对性能提升还是有一定帮助的。在代码可读性方面,V4 是不如 V3 和 V5 的。

整个优化总结下来主要有两点:

1、String.replace方法涉及到模板编译和新字符串生成,比较吃资源。

2、StringBuilder代替String.replace,除了能够缩短调用耗时,在空间上也能够减少资源占用。因为StringBuilder.append相对于String.replace来说,能够减少中间大量String对象的创建和销毁,能够减少 GC 的压力,从而降低 CPU 的负载。

性能优化显而易见的好处是能够节约机器资源。如果一个有 2000 台服务器的应用,整体性能提升了 10%,理论上来说,就相当于节省了 200 台的机器。除了节省机器资源外,性能好的应用相对于性能差的应用,在应对流量突增时更不容易达到机器的性能瓶颈,在同样流量场景下进行机器扩容时,也只需要更少的机器,从而能够更快的完成扩容、应急操作。所以,性能好的应用相对于性能差的应用在稳定性方面也更胜一筹。

最后再回到本次文章的主题:是什么让一段 20 行代码的性能提升了 10 倍?

我的回答是:StringBuilder YYDS: )

是什么让一段20行代码的性能提升了10倍

关注
打赏
1663816507
查看更多评论
立即登录/注册

微信扫码登录

0.0481s