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

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

image.png

作者 | 金盛杰(司旭)
来源 | 阿里开发者公众号

一、背景

1.1 业务背景

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

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

image.png

【图1】卡券数据在C端展现形式



image.png

【图2】C端数据组装过程

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

1.2 问题发现

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

image.png

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

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

1、每次while 循环进行了两次indexOf 操作

2、每次while 循环都进行了substring 操作

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

1、能够减少indexOf 和substring 操作吗?

2、真的每次都要进行模板变量查找吗?

二、性能优化

带着上面两个问题,逐步进行性能优化并测试。

整个优化过程一共迭代了5版,并最终取得了性能提升超过10倍的效果。下面分别来介绍下不同版本的实现和性能对比。

2.1 性能优化V1

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

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

image.png

【图4】性能优化V1代码实现

2.2 性能优化V2

静态模板配置一般情况下不会发生变更。也就意味着,同一个模板对应的变量都是固定不变的。可以将模板id和模板变量集合进行一对一的缓存,减少每次替换之前的变量提取。

在决定使用缓存之前,要想好怎么实现缓存。有两点需要注意:

1、用本地缓存代替TBase,减少大流量场景下对TBase的压力

2、怎么控制本地缓存的有效数量,并在有限的内存占用情况下最大化缓存效率

可以借助Google Guava库的缓存类来实现缓存逻辑,示例代码见【图5】

image.png

【图5】缓存实现示例代码

image.png

【图6】性能优化V2代码实现

2.3 性能对比 (1)

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

image.png

【图7】V1、V2版性能对比

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

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

1、每次replace 都会进行模板编译

2、replace 都是创建一个新的对象进行返回

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

image.png

【图8】String.replace 代码实现

2.4 性能优化V3

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

image.png

【图9】性能优化V3代码实现

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

2.5 性能优化V4

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

image.png

【图10】性能优化V4代码实现

2.6 性能对比(2)

image.png

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

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

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

2.7 性能优化V5

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

image.png

【图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!

推荐阅读

《低代码引擎技术白皮书》

低代码引擎是一款为低代码平台开发者提供的,具备强大定制扩展能力的低代码设计器研发框架。本书从应用、基础协议和原理三个方面对低代码引擎的技术进行了全面的介绍,并在低代码引擎原理篇重点介绍了低代码引擎所需的渲染、入料、编排、出码等核心技术原理,对低代码引擎的生态设计进行了介绍。本书适合于有低代码产品研发诉求的前端开发人员。

相关文章
|
设计模式 Java Maven
一个注解搞定责任链,学还是不学?
在繁琐的业务流程处理中,通常采用面向过程的设计方法将流程拆分成N个步骤,每个步骤执行独立的逻辑。但是这样剥离仍然不彻底,修改其中一个步骤仍然可能影响其他步骤。在这种场景下,有一种经典的设计模式-责任链模式,可以将这些子步骤封装成独立的handler,然后通过pipeline将其串联起来。
1222 173
一个注解搞定责任链,学还是不学?
|
存储 缓存 监控
如何写出一篇好的技术方案?
近期作者在写某个项目的技术方案时,来来回回修改了许多版,很是苦恼。于是,将自己之前写的和别人写的技术方案都翻出来看了几遍,产生了一些思考,分享给大家。
如何写出一篇好的技术方案?
|
缓存 Java 程序员
函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码
本文会以惰性加载为例一步步介绍函数式编程中各种概念,所以读者不需要任何函数式编程的基础,只需要对 Java 8 有些许了解即可。
函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码
|
存储 运维 监控
亿级异构任务调度框架设计与实践
阿里云日志服务作为云原生可观测与分析平台。提供了一站式的数据采集、加工、查询分析、可视化、告警、消费与投递等功能。全面提升用户的研发、运维、运营、安全场景的数字化能力。日志服务平台作为可观测性平台提供了数据导入、数据加工、聚集加工、告警、智能巡检、导出等功能,这些功能在日志服务被称为任务,并且具有大规模的应用,接下来主要介绍下这些任务的调度框架的设计与实践。
亿级异构任务调度框架设计与实践
|
存储 缓存 Java
java应用提速(速度与激情)
本文将阐述通过基础设施与工具的改进,实现从构建到启动全方面大幅提速的实践和理论。
50179 13
java应用提速(速度与激情)
|
设计模式 Java 数据安全/隐私保护
理论与实践:如何写好一个方法
个人认为一个好的方法主要表现在可读性、可维护性、可复用性上,本文通过设计原则和代码规范两章来讲解如何提高方法的可读性、可维护性、可复用性。这些设计原则和代码规范更多的是表现一种思想,不仅仅可以用在方法上,也可以用在类上、模块上。下面通过具体的例子来讲解。
理论与实践:如何写好一个方法
|
设计模式 IDE Java
谈谈过度设计:因噎废食的陷阱
写软件和造楼房一样需要设计,但是和建筑行业严谨客观的设计规范不同,软件设计常常很主观,且容易引发争论。
2986 4
谈谈过度设计:因噎废食的陷阱
|
存储 程序员 项目管理
六年团队Leader实战秘诀|程序员最重要的八种软技能
笔者在带团队的六年中发现,程序员们在职场都有一个共同的困扰:“好像写代码都没什么问题了,日常工作基本上都是应付业务需求的开发,好像找不到其他的更大的附加价值了,我应该找一些什么样的发力点才能让我的价值更突出呢?” 。本文将和大家聊聊程序员的软技能。
六年团队Leader实战秘诀|程序员最重要的八种软技能
|
存储 机器学习/深度学习 编解码
一文读懂字符编码
说起字符编码,让我想起了科幻巨作《三体-黑暗深林》人类遇到外星文明魔戒的画面
一文读懂字符编码
|
敏捷开发 人工智能 开发者
Code Smell 重构你的日常代码-圈复杂度高多层嵌套
圈复杂度(Cyclomatic complexity)[1]是一种代码复杂度的衡量标准,在1976年由Thomas J. McCabe, Sr. 提出。条件分支越多,圈复杂度越高,测试越难覆盖,也越难维护。随着业务的不断演进,代码的不断新增与调整,如果只在原逻辑下加入自己的新逻辑,就会长出一个超高嵌套的“气功波”代码。
1254 7
Code Smell 重构你的日常代码-圈复杂度高多层嵌套